<?php
/**
 * GitHub Updater
 *
 * @author    Andy Fragen
 * @license   GPL-2.0+
 * @link      https://github.com/afragen/github-updater
 * @package   github-updater
 */

namespace Fragen\GitHub_Updater;

use Fragen\Singleton;

/*
 * Exit if called directly.
 */
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class Theme
 *
 * Update a WordPress theme from a GitHub repo.
 *
 * @author    Andy Fragen
 * @author    Seth Carstens
 * @link      https://github.com/WordPress-Phoenix/whitelabel-framework
 * @author    UCF Web Communications
 * @link      https://github.com/UCF/Theme-Updater
 */
class Theme extends Base {
	/**
	 * Rollback variable.
	 *
	 * @var number
	 */
	protected $tag = false;

	/**
	 * Constructor.
	 */
	public function __construct() {
		parent::__construct();
		$this->load_options();

		// Get details of installed git sourced themes.
		$this->config = $this->get_theme_meta();

		if ( null === $this->config ) {
			return;
		}
	}

	/**
	 * Returns an array of configurations for the known themes.
	 *
	 * @return array
	 */
	public function get_theme_configs() {
		return $this->config;
	}

	/**
	 * Delete cache of current theme.
	 * This is needed in case `wp_get_theme()` is called in earlier or in a mu-plugin.
	 * This action results in the extra headers not being added.
	 *
	 * @link https://github.com/afragen/github-updater/issues/586
	 */
	private function delete_current_theme_cache() {
		$cache_hash = md5( get_stylesheet_directory() );
		wp_cache_delete( 'theme-' . $cache_hash, 'themes' );
	}

	/**
	 * Reads in WP_Theme class of each theme.
	 * Populates variable array.
	 *
	 * @return array Indexed array of associative arrays of theme details.
	 */
	protected function get_theme_meta() {
		$this->delete_current_theme_cache();
		$git_themes = [];
		$themes     = wp_get_themes( [ 'errors' => null ] );

		/**
		 * Filter to add themes not containing appropriate header line.
		 *
		 * @since   5.4.0
		 * @access  public
		 *
		 * @param array $additions Listing of themes to add.
		 *                         Default null.
		 * @param array $themes    Listing of all themes.
		 * @param         string        'theme'    Type being passed.
		 */
		$additions = apply_filters( 'github_updater_additions', null, $themes, 'theme' );

		foreach ( (array) $themes as $theme ) {
			$git_theme = [];

			foreach ( (array) static::$extra_headers as $value ) {
				$header   = null;
				$repo_uri = $theme->get( $value );

				/**
				 * Get $repo_uri from themes added to GitHub Updater via hook.
				 */
				foreach ( (array) $additions as $addition ) {
					if ( $theme->stylesheet === $addition['slug'] ) {
						if ( ! empty( $addition[ $value ] ) ) {
							$repo_uri = $addition[ $value ];
							break;
						}
					}
				}

				if ( empty( $repo_uri ) || false === stripos( $value, 'Theme' ) ) {
					continue;
				}

				$header_parts = explode( ' ', $value );
				$repo_parts   = $this->get_repo_parts( $header_parts[0], 'theme' );

				if ( $repo_parts['bool'] ) {
					$header = $this->parse_header_uri( $repo_uri );
					if ( empty( $header ) || $theme->stylesheet !== $header['repo'] ) {
						continue;
					}
				}

				$header         = $this->parse_extra_headers( $header, $theme, $header_parts, $repo_parts );
				$current_branch = "current_branch_{$header['repo']}";
				$branch         = isset( static::$options[ $current_branch ] )
					? static::$options[ $current_branch ]
					: false;

				$git_theme['type']                    = 'theme';
				$git_theme['git']                     = $repo_parts['git_server'];
				$git_theme['uri']                     = "{$header['base_uri']}/{$header['owner_repo']}";
				$git_theme['enterprise']              = $header['enterprise_uri'];
				$git_theme['enterprise_api']          = $header['enterprise_api'];
				$git_theme['owner']                   = $header['owner'];
				$git_theme['slug']                    = $header['repo'];
				$git_theme['file']                    = "{$header['repo']}/style.css";
				$git_theme['name']                    = $theme->get( 'Name' );
				$git_theme['theme_uri']               = $theme->get( 'ThemeURI' );
				$git_theme['homepage']                = $theme->get( 'ThemeURI' );
				$git_theme['author']                  = $theme->get( 'Author' );
				$git_theme['local_version']           = strtolower( $theme->get( 'Version' ) );
				$git_theme['sections']['description'] = $theme->get( 'Description' );
				$git_theme['local_path']              = get_theme_root() . '/' . $git_theme['slug'] . '/';
				$git_theme['branch']                  = $branch ?: 'master';
				$git_theme['languages']               = $header['languages'];
				$git_theme['ci_job']                  = $header['ci_job'];
				$git_theme['release_asset']           = $header['release_asset'];
				$git_theme['broken']                  = ( empty( $header['owner'] ) || empty( $header['repo'] ) );

				break;
			}

			// Exit if not git hosted theme.
			if ( empty( $git_theme ) ) {
				continue;
			}

			$git_themes[ $git_theme['slug'] ] = (object) $git_theme;
		}

		return $git_themes;
	}

	/**
	 * Get remote theme meta to populate $config theme objects.
	 * Calls to remote APIs to get data.
	 */
	public function get_remote_theme_meta() {
		$themes = [];
		foreach ( (array) $this->config as $theme ) {
			/**
			 * Filter to set if WP-Cron is disabled or if user wants to return to old way.
			 *
			 * @since  7.4.0
			 * @access public
			 *
			 * @param bool
			 */
			if ( ! $this->waiting_for_background_update( $theme ) || static::is_wp_cli()
				|| apply_filters( 'github_updater_disable_wpcron', false )
			) {
				$this->get_remote_repo_meta( $theme );
			} else {
				$themes[ $theme->slug ] = $theme;
			}

			/*
			 * Add update row to theme row, only in multisite.
			 */
			if ( is_multisite() ) {
				add_action( 'after_theme_row', [ $this, 'remove_after_theme_row' ], 10, 2 );
				if ( ! $this->tag ) {
					add_action( "after_theme_row_{$theme->slug}", [ $this, 'wp_theme_update_row' ], 10, 2 );
					if ( ! $theme->release_asset ) {
						add_action( "after_theme_row_{$theme->slug}", [ $this, 'multisite_branch_switcher' ], 15, 2 );
					}
				}
			}
		}

		$schedule_event = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ? is_main_site() : true;

		if ( $schedule_event ) {
			if ( ! wp_next_scheduled( 'ghu_get_remote_theme' ) &&
			! $this->is_duplicate_wp_cron_event( 'ghu_get_remote_theme' ) &&
			! apply_filters( 'github_updater_disable_wpcron', false )
			) {
				wp_schedule_single_event( time(), 'ghu_get_remote_theme', [ $themes ] );
			}
		}

		if ( ! static::is_wp_cli() ) {
			$this->load_pre_filters();
		}
	}

	/**
	 * Load pre-update filters.
	 */
	public function load_pre_filters() {
		if ( ! is_multisite() ) {
			add_filter( 'wp_prepare_themes_for_js', [ $this, 'customize_theme_update_html' ] );
		}
		add_filter( 'themes_api', [ $this, 'themes_api' ], 99, 3 );
		add_filter( 'site_transient_update_themes', [ $this, 'update_site_transient' ], 15, 1 );
	}

	/**
	 * Put changelog in themes_api, return WP.org data as appropriate.
	 *
	 * @param bool      $false
	 * @param string    $action
	 * @param \stdClass $response
	 *
	 * @return mixed
	 */
	public function themes_api( $false, $action, $response ) {
		if ( ! ( 'theme_information' === $action ) ) {
			return $false;
		}

		$theme = isset( $this->config[ $response->slug ] ) ? $this->config[ $response->slug ] : false;

		// Skip if waiting for background update.
		if ( $this->waiting_for_background_update( $theme ) ) {
			return $false;
		}

		// wp.org theme.
		if ( ! $theme ) {
			return $false;
		}

		$response->slug         = $theme->slug;
		$response->name         = $theme->name;
		$response->homepage     = $theme->homepage;
		$response->donate_link  = $theme->donate_link;
		$response->version      = $theme->remote_version;
		$response->sections     = $theme->sections;
		$response->description  = implode( "\n", $theme->sections );
		$response->author       = $theme->author;
		$response->preview_url  = $theme->theme_uri;
		$response->requires     = $theme->requires;
		$response->tested       = $theme->tested;
		$response->downloaded   = $theme->downloaded;
		$response->last_updated = $theme->last_updated;
		$response->rating       = $theme->rating;
		$response->num_ratings  = $theme->num_ratings;

		return $response;
	}

	/**
	 * Add custom theme update row, from /wp-admin/includes/update.php
	 * Display update details or rollback links for multisite installation.
	 *
	 * @param string $theme_key
	 * @param array  $theme
	 *
	 * @author Seth Carstens
	 */
	public function wp_theme_update_row( $theme_key, $theme ) {
		$current = get_site_transient( 'update_themes' );

		$themes_allowedtags = [
			'a'       => [
				'href'  => [],
				'title' => [],
			],
			'abbr'    => [ 'title' => [] ],
			'acronym' => [ 'title' => [] ],
			'code'    => [],
			'em'      => [],
			'strong'  => [],
		];
		$theme_name         = wp_kses( $theme['Name'], $themes_allowedtags );
		// $wp_list_table      = _get_list_table( 'WP_MS_Themes_List_Table' );
		$details_url       = esc_attr(
			add_query_arg(
				[
					'tab'       => 'theme-information',
					'theme'     => $theme_key,
					'TB_iframe' => 'true',
					'width'     => 270,
					'height'    => 400,
				],
				self_admin_url( 'theme-install.php' )
			)
		);
		$nonced_update_url = wp_nonce_url(
			$this->get_update_url( 'theme', 'upgrade-theme', $theme_key ),
			'upgrade-theme_' . $theme_key
		);
		$enclosure         = $this->update_row_enclosure( $theme_key, 'theme' );

		if ( isset( $current->response[ $theme_key ] ) ) {
			$response = $current->response[ $theme_key ];
			echo $enclosure['open'];

			printf(
				/* translators: %s: theme name */
				esc_html__( 'There is a new version of %s available.', 'github-updater' ),
				$theme_name
			);
			printf(
				/* translators: %s: details URL, theme name */
				' <a href="%s" class="thickbox" title="%s"> ',
				$details_url,
				$theme_name
			);
			if ( empty( $response['package'] ) ) {
				printf(
					/* translators: %s: theme version */
					esc_html__( 'View version %s details.', 'github-updater' ),
					$response['new_version']
				);
				echo '</a>&nbsp;<em>';
				esc_html_e( 'Automatic update is unavailable for this theme.', 'github-updater' );
				echo '</em>';
			} else {
				printf(
					/* translators: 1: version number, 2: closing anchor tag, 3: update URL */
					esc_html__( 'View version %1$s details%2$s or %3$supdate now%2$s.', 'github-updater' ),
					$response['new_version'],
					'</a>',
					sprintf(
						/* translators: %s: theme name */
						'<a href="' . $nonced_update_url . '" class="update-link" aria-label="' . esc_html__( 'Update %s now', 'github-updater' ) . '">',
						$theme_name
					)
				);
			}
			echo $enclosure['close'];

			do_action( "in_theme_update_message-$theme_key", $theme, $response );
		}
	}

	/**
	 * Create branch switcher row for multisite installation.
	 *
	 * @param string $theme_key
	 * @param array  $theme
	 *
	 * @return bool
	 */
	public function multisite_branch_switcher( $theme_key, $theme ) {
		if ( empty( static::$options['branch_switch'] ) ) {
			return false;
		}

		$enclosure         = $this->update_row_enclosure( $theme_key, 'theme', true );
		$id                = $theme_key . '-id';
		$branches          = isset( $this->config[ $theme_key ]->branches )
			? $this->config[ $theme_key ]->branches
			: null;
		$nonced_update_url = wp_nonce_url(
			$this->get_update_url( 'theme', 'upgrade-theme', $theme_key ),
			'upgrade-theme_' . $theme_key
		);

		// Get current branch.
		$repo   = $this->config[ $theme_key ];
		$branch = Singleton::get_instance( 'Branch', $this )->get_current_branch( $repo );

		$branch_switch_data                      = [];
		$branch_switch_data['slug']              = $theme_key;
		$branch_switch_data['nonced_update_url'] = $nonced_update_url;
		$branch_switch_data['id']                = $id;
		$branch_switch_data['branch']            = $branch;
		$branch_switch_data['branches']          = $branches;

		/*
		 * Create after_theme_row_
		 */
		echo $enclosure['open'];
		$this->make_branch_switch_row( $branch_switch_data );
		echo $enclosure['close'];

		return true;
	}

	/**
	 * Remove default after_theme_row_$stylesheet.
	 *
	 * @author @grappler
	 *
	 * @param string $theme_key
	 * @param array  $theme
	 */
	public function remove_after_theme_row( $theme_key, $theme ) {
		$themes = $this->get_theme_configs();

		foreach ( static::$git_servers as $server ) {
			$repo_header = $server . ' Theme URI';
			$repo_uri    = $theme->get( $repo_header );

			/**
			 * Filter to add themes not containing appropriate header line.
			 *
			 * @since   5.4.0
			 * @access  public
			 *
			 * @param array $additions Listing of themes to add.
			 *                         Default null.
			 * @param array $themes    Listing of all themes.
			 * @param         string        'theme'    Type being passed.
			 */
			$additions = apply_filters( 'github_updater_additions', null, $themes, 'theme' );
			foreach ( (array) $additions as $addition ) {
				if ( $theme_key === $addition['slug'] ) {
					if ( ! empty( $addition[ $server . ' Theme URI' ] ) ) {
						$repo_uri = $addition[ $server . ' Theme URI' ];
						break;
					}
				}
			}
			if ( empty( $repo_uri ) ) {
				continue;
			}
			break;
		}
		if ( array_key_exists( $theme_key, $themes ) ) {
			remove_action( "after_theme_row_$theme_key", 'wp_theme_update_row' );
		}
	}

	/**
	 * Call theme messaging for single site installation.
	 *
	 * @author Seth Carstens
	 *
	 * @param array $prepared_themes
	 *
	 * @return mixed
	 */
	public function customize_theme_update_html( $prepared_themes ) {
		foreach ( (array) $this->config as $theme ) {
			if ( empty( $prepared_themes[ $theme->slug ] ) ) {
				continue;
			}

			if ( ! empty( $prepared_themes[ $theme->slug ]['hasUpdate'] ) ) {
				$prepared_themes[ $theme->slug ]['update'] = $this->append_theme_actions_content( $theme );
			} else {
				$prepared_themes[ $theme->slug ]['description'] .= $this->append_theme_actions_content( $theme );
			}
			if ( ! $theme->release_asset ) {
				$prepared_themes[ $theme->slug ]['description'] .= $this->single_install_switcher( $theme );
			}
		}

		return $prepared_themes;
	}

	/**
	 * Create theme update messaging for single site installation.
	 *
	 * @author Seth Carstens
	 *
	 * @access protected
	 *
	 * @param \stdClass $theme
	 *
	 * @return string (content buffer)
	 */
	protected function append_theme_actions_content( $theme ) {
		$details_url       = esc_attr(
			add_query_arg(
				[
					'tab'       => 'theme-information',
					'theme'     => $theme->slug,
					'TB_iframe' => 'true',
					'width'     => 270,
					'height'    => 400,
				],
				self_admin_url( 'theme-install.php' )
			)
		);
		$nonced_update_url = wp_nonce_url(
			$this->get_update_url( 'theme', 'upgrade-theme', $theme->slug ),
			'upgrade-theme_' . $theme->slug
		);

		$current = get_site_transient( 'update_themes' );

		/**
		 * Display theme update links.
		 */
		ob_start();
		if ( isset( $current->response[ $theme->slug ] ) ) {
			?>
			<p>
				<strong>
					<?php
					printf(
						/* translators: %s: theme name */
						esc_html__( 'There is a new version of %s available.', 'github-updater' ),
						$theme->name
					);
					printf(
						' <a href="%s" class="thickbox open-plugin-details-modal" title="%s">',
						$details_url,
						esc_attr( $theme->name )
					);
					printf(
						/* translators: 1: version number, 2: closing anchor tag, 3: update URL */
						esc_html__( 'View version %1$s details%2$s or %3$supdate now%2$s.', 'github-updater' ),
						$theme->remote_version = isset( $theme->remote_version ) ? $theme->remote_version : null,
						'</a>',
						sprintf(
							/* translators: %s: theme name */
							'<a aria-label="' . esc_html__( 'Update %s now', 'github-updater' ) . '" id="update-theme" data-slug="' . $theme->slug . '" href="' . $nonced_update_url . '">',
							$theme->name
						)
					);
					?>
				</strong>
			</p>
			<?php
		}

		return trim( ob_get_clean(), '1' );
	}

	/**
	 * Display rollback/branch switcher for single site installation.
	 *
	 * @access protected
	 *
	 * @param \stdClass $theme
	 *
	 * @return string
	 */
	protected function single_install_switcher( $theme ) {
		$nonced_update_url = wp_nonce_url(
			$this->get_update_url( 'theme', 'upgrade-theme', $theme->slug ),
			'upgrade-theme_' . $theme->slug
		);
		$rollback_url      = sprintf( '%s%s', $nonced_update_url, '&rollback=' );

		if ( ! isset( static::$options['branch_switch'] ) ) {
			return;
		}

		ob_start();
		if ( '1' === static::$options['branch_switch'] ) {
			printf(
				/* translators: 1: branch name, 2: jQuery dropdown, 3: closing tag */
				'<p>' . esc_html__( 'Current branch is `%1$s`, try %2$sanother version%3$s', 'github-updater' ),
				$theme->branch,
				'<a href="#" onclick="jQuery(\'#ghu_versions\').toggle();return false;">',
				'</a>.</p>'
			);
			?>
			<div id="ghu_versions" style="display:none; width: 100%;">
				<label><select style="width: 60%;" onchange="if(jQuery(this).val() != '') { jQuery(this).parent().next().show(); jQuery(this).parent().next().attr('href','<?php echo esc_url( $rollback_url ); ?>'+jQuery(this).val()); } else jQuery(this).parent().next().hide();">
				<option value=""><?php esc_html_e( 'Choose a Version', 'github-updater' ); ?>&#8230;</option>
			<?php
			if ( isset( $theme->branches ) ) {
				foreach ( array_keys( $theme->branches ) as $branch ) {
					echo '<option>' . $branch . '</option>';
				}
			}
			if ( ! empty( $theme->rollback ) ) {
				$rollback = array_keys( $theme->rollback );
				usort( $rollback, 'version_compare' );
				krsort( $rollback );
				$rollback = array_splice( $rollback, 0, 4, true );
				array_shift( $rollback ); // Dump current tag.
				foreach ( $rollback as $tag ) {
					echo '<option>' . $tag . '</option>';
				}
			}
			if ( empty( $theme->rollback ) ) {
				echo '<option>' . esc_html__( 'No previous tags to rollback to.', 'github-updater' ) . '</option></select></label>';
			}
			?>
					</select></label>
				<a style="display: none;" class="button-primary" href="?"><?php esc_html_e( 'Install', 'github-updater' ); ?></a>
			</div>
			<?php
		}

		return trim( ob_get_clean(), '1' );
	}

	/**
	 * Hook into site_transient_update_themes to update.
	 * Finds newest tag and compares to current tag.
	 *
	 * @param array $transient
	 *
	 * @return array|\stdClass
	 */
	public function update_site_transient( $transient ) {
		foreach ( (array) $this->config as $theme ) {
			if ( $this->can_update_repo( $theme ) ) {
				$response = [
					'theme'       => $theme->slug,
					'new_version' => $theme->remote_version,
					'url'         => $theme->uri,
					'package'     => $theme->download_link,
					'branch'      => $theme->branch,
					'branches'    => array_keys( $theme->branches ),
					'type'        => "{$theme->git}-{$theme->type}",
				];

				// Skip on RESTful updating.
				if ( isset( $_GET['action'], $_GET['theme'] ) &&
					'github-updater-update' === $_GET['action'] &&
					$response['theme'] === $_GET['theme']
				) {
					continue;
				}

				// Pull update from dot org if not overriding.
				if ( ! $this->override_dot_org( 'theme', $theme ) ) {
					continue;
				}

				$transient->response[ $theme->slug ] = $response;
			} else {
				/**
				 * Filter to return array of overrides to dot org.
				 *
				 * @since 8.5.0
				 * @return array
				 */
				$overrides = apply_filters( 'github_updater_override_dot_org', [] );
				if ( isset( $transient->response[ $theme->slug ] ) && in_array( $theme->slug, $overrides, true ) ) {
					unset( $transient->response[ $theme->slug ] );
				}
			}

			// Set transient for rollback.
			if ( isset( $_GET['theme'], $_GET['rollback'] ) && $theme->slug === $_GET['theme']
			) {
				$transient->response[ $theme->slug ] = $this->set_rollback_transient( 'theme', $theme );
			}
		}

		return $transient;
	}
}
