<?php

/*
Plugin: Vendi Abandoned Plugin Check
Description: Provides information about abandoned plugins.
Version: 3.1.1
License: GPLv2
Author: Vendi Advertising (Chris Haas)
*/

class MainWP_Child_Plugins_Check {

	public static $instance = null;

	private $cron_name_watcher = 'mainwp_child_cron_plugin_health_check_watcher';

	private $cron_name_daily = 'mainwp_child_cron_plugin_health_check_daily';

	private $cron_name_batching = 'mainwp_child_cron_plugin_health_check_batching';

	private $tran_name_plugin_timestamps = 'mainwp_child_tran_name_plugin_timestamps';

	private $tran_name_plugins_to_batch = 'mainwp_child_tran_name_plugins_to_batch';

	private $option_name_last_daily_run = 'mainwp_child_plugin_last_daily_run';

	public static function Instance() {
		if ( null === MainWP_Child_Plugins_Check::$instance ) {
			MainWP_Child_Plugins_Check::$instance = new MainWP_Child_Plugins_Check();
		}

		return MainWP_Child_Plugins_Check::$instance;
	}

	public function __construct() {
		$this->schedule_watchdog();

		add_action( $this->cron_name_batching, array( $this, 'run_check' ) );
		add_action( $this->cron_name_daily, array( $this, 'run_check' ) );

		add_action( $this->cron_name_watcher, array( $this, 'perform_watchdog' ) );

		//add_filter( 'plugin_row_meta', array( $this, 'change_plugin_row_meta' ), 10, 4 );

		add_filter( 'plugins_api_args', array( $this, 'modify_plugin_api_search_query' ), 10, 2 );

		add_action( 'mainwp_child_deactivation', array( $this, 'cleanup_deactivation' ) );

	}

	private function cleanup_basic() {
		wp_clear_scheduled_hook( $this->cron_name_daily );
		wp_clear_scheduled_hook( $this->cron_name_batching );
		delete_transient( $this->tran_name_plugins_to_batch );
	}


	public function cleanup_deactivation( $del = true ) {
		$this->cleanup_basic();
		wp_clear_scheduled_hook( $this->cron_name_watcher );
		delete_option( $this->option_name_last_daily_run );
		if ( $del ) {
			delete_transient( $this->tran_name_plugin_timestamps );
		}
	}


	public function modify_plugin_api_search_query( $args, $action ) {
		if ( isset( $action ) && 'query_plugins' === $action ) {

			if ( ! is_object( $args ) ) {
				$args = new \stdClass();
			}

			if ( ! property_exists( $args, 'fields' ) ) {
				$args->fields = array();
			}

			$args->fields = array_merge( $args->fields, array( 'last_updated' => true ) );
		}

		return $args;
	}

	public function perform_watchdog() {
		if ( false === wp_next_scheduled( $this->cron_name_daily ) && false === wp_next_scheduled( $this->cron_name_batching ) ) {
			$last_run = get_option( $this->option_name_last_daily_run );

			if ( false === $last_run || ! is_integer( $last_run ) ) {
				$last_run = false;
			} else {
				$last_run = new \DateTime( '@' . $last_run );
			}

			//Get now
			$now = new \DateTime();

			if ( false === $last_run || (int) $now->diff( $last_run )->format( '%h' ) >= 24 ) {
				$this->cleanup_basic();

				wp_schedule_event( time(), 'daily', $this->cron_name_daily );

				update_option( $this->option_name_last_daily_run, $now->getTimestamp() );

			}
		}
	}

	public function schedule_watchdog() {
		//For testing
		//$this->cleanup_deactivation();

		//Schedule a global watching cron just in case both other crons get killed
		if ( ! wp_next_scheduled( $this->cron_name_watcher ) ) {
			wp_schedule_event( time(), 'hourly', $this->cron_name_watcher );
		}

	}

	public function get_plugins_outdate_info() {
		$plugins_outdate = get_transient( $this->tran_name_plugin_timestamps );
		if ( ! is_array( $plugins_outdate ) ) {
			$plugins_outdate = array();
		}
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . '/wp-admin/includes/plugin.php';
		}
		$plugins = get_plugins();
		$update  = false;
		foreach ( $plugins_outdate as $slug => $v ) {
			if ( ! isset( $plugins[ $slug ] ) ) {
				unset( $plugins_outdate[ $slug ] );
				$update = true;
			}
		}
		if ( $update ) {
			set_transient( $this->tran_name_plugin_timestamps, $plugins_outdate, DAY_IN_SECONDS );
		}

		return $plugins_outdate;

	}

	// for testing
	public function change_plugin_row_meta( $plugin_meta, $plugin_file, $plugin_data, $status ) {
		//Grab our previously stored array of known last modified dates
		//Requires WP 2.8.0
		$plugin_info = get_transient( $this->tran_name_plugin_timestamps );

		//Sanity check the response
		if( false === $plugin_info || ! is_array( $plugin_info ) && 0 === count( $plugin_info ) )
		{
			return $plugin_meta;
		}

		//See if this specific plugin is in the known list
		if( array_key_exists( $plugin_file, $plugin_info ) )
		{
			//Get now
			$now = new \DateTime();
			$last_updated = $plugin_info[ $plugin_file ]['last_updated'];

			//Last updated is stored as timestamp, get a real date
			$plugin_last_updated_date = new \DateTime( '@' . $last_updated );

			//Compute days between now and plugin last updated
			$diff_in_days = $now->diff( $plugin_last_updated_date )->format( '%a' );

			//Customizable number of days for tolerance
			$tolerance_in_days = get_option( 'mainwp_child_plugintheme_days_outdate', 365 );

			//If we're outside the window for tolerance show a message
			if( $diff_in_days > $tolerance_in_days )
			{
				$plugin_meta[] = sprintf( '<strong style="color: #f00;">This plugin has not been updated by the author in %1$d days!</strong>', $diff_in_days );
			}
			else
			{
				$plugin_meta[] = sprintf( '<span style="color: #090;">This plugin was last updated by the author in %1$d days ago.</span>', $diff_in_days );
			}
		}

		return $plugin_meta;
	}

	public function run_check() {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . '/wp-admin/includes/plugin.php';
		}

		//Get our previous results
		$responses = get_transient( $this->tran_name_plugin_timestamps );

		if ( false === $responses || ! is_array( $responses ) ) {
			$responses = array();
		}

		//Get our previous cache of plugins for batching
		$all_plugins = get_transient( $this->tran_name_plugins_to_batch );

		//If there wasn't a previous cache
		if ( false === $all_plugins || ! is_array( $all_plugins ) ) {
			$plugins = get_plugins();
			if ( is_array( $plugins ) ) {
				foreach ( $plugins as $slug => $plugin ) {
					if ( isset( $plugin['Name'] ) && ! empty( $plugin['Name'] ) ) {
						$all_plugins[ $slug ] = array(
							'Name'      => $plugin['Name'],
							'PluginURI' => $plugin['PluginURI'],
							'Version'   => $plugin['Version'],
						);

					}
				}
			}
			$responses = array();
		}

		$avoid_plugins = array( 'sitepress-multilingual-cms/sitepress.php' );
		//Grab a small number of plugins to scan
		$plugins_to_scan = array_splice( $all_plugins, 0, apply_filters( 'mainwp_child_plugin_health_check_max_plugins_to_batch', 10 ) );
		$tolerance_in_days = get_option( 'mainwp_child_plugintheme_days_outdate', 365 );

		//Loop through each known plugin
		foreach ( $plugins_to_scan as $slug => $v ) {
			if ( in_array( $slug, $avoid_plugins ) ) {
				continue;
			}
			//Try to get the raw information for this plugin
			$body = $this->try_get_response_body( $slug, false );

			//We couldn't get any information, skip this plugin
			if ( false === $body ) {
				continue;
			}

			//Deserialize the response
			$obj = maybe_unserialize( $body );

			$now = new \DateTime();

			//Sanity check that deserialization worked and that our property exists
			if ( false !== $obj && is_object( $obj ) && property_exists( $obj, 'last_updated' ) ) {
				if ( version_compare( $v['Version'], $obj->version, '>' ) ) {
					continue;
				}
				$last_updated             = strtotime( $obj->last_updated );
				$plugin_last_updated_date = new \DateTime( '@' . $last_updated );

				$diff_in_days = $now->diff( $plugin_last_updated_date )->format( '%a' );

				if ( $diff_in_days < $tolerance_in_days ) {
					continue;
				}
				$v['last_updated']  = $last_updated;
				$responses[ $slug ] = $v;
			}
		}

		if ( ! defined( 'MINUTE_IN_SECONDS' ) ) {
			define( 'MINUTE_IN_SECONDS', 60 );
		}

		if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
			define( 'HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS );
		}

		if ( ! defined( 'DAY_IN_SECONDS' ) ) {
			define( 'DAY_IN_SECONDS', 24 * HOUR_IN_SECONDS );
		}

		//Store the master response for usage in the plugin table
		set_transient( $this->tran_name_plugin_timestamps, $responses, DAY_IN_SECONDS );

		if ( 0 === count( $all_plugins ) ) {
			delete_transient( $this->tran_name_plugins_to_batch );
			//wp_schedule_single_event( time() + DAY_IN_SECONDS, $this->cron_name_daily );
		} else {
			set_transient( $this->tran_name_plugins_to_batch, $all_plugins, DAY_IN_SECONDS );
			wp_schedule_single_event( time(), $this->cron_name_batching );
		}
	}

	private function try_get_response_body( $plugin, $second_pass ) {
		//Some of this code is lifted from class-wp-upgrader

		//Get the WordPress current version to be polite in the API call
		include( ABSPATH . WPINC . '/version.php' );

		if ( ! defined( 'MINUTE_IN_SECONDS' ) ) {
			define( 'MINUTE_IN_SECONDS', 60 );
		}

		if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
			define( 'HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS );
		}
		global $wp_version;

		//General options to be passed to wp_remote_get
		$options = array(
			'timeout'    => HOUR_IN_SECONDS,
			'user-agent' => 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ),
		);

		//The URL for the endpoint
		$url = $http_url = 'http://api.wordpress.org/plugins/info/1.0/';

		//If we support SSL
		//Requires WP 3.2.0
		if ( $ssl = wp_http_supports( array( 'ssl' ) ) ) {
			//Requires WP 3.4.0
			$url = set_url_scheme( $url, 'https' );
		}

		$plugin_dir = $plugin;
		if ( strpos( $plugin, '/' ) !== false ) {
			$plugin_dir = dirname( $plugin );
		}

		//Try to get the response (usually the SSL version)
		//Requires WP 2.7.0
		$raw_response = wp_remote_get( $url . $plugin_dir, $options );

		//If we don't have an error and we received a valid response code
		//Requires WP 2.7.0
		if ( ! is_wp_error( $raw_response ) && 200 === (int) wp_remote_retrieve_response_code( $raw_response ) ) {
			//Get the actual body
			//Requires WP 2.7.0
			$body = wp_remote_retrieve_body( $raw_response );

			//Make sure that it isn't empty and also not an empty serialized object
			if ( '' !== $body && 'N;' !== $body ) {
				//If valid, return that
				return $body;
			}
		}

		//The above valid
		//If we previously tried an SSL version try without SSL
		//Code below same as above block
		if ( $ssl ) {
			$raw_response = wp_remote_get( $http_url . $plugin, $options );
			if ( ! is_wp_error( $raw_response ) && 200 === (int) wp_remote_retrieve_response_code( $raw_response ) ) {
				$body = wp_remote_retrieve_body( $raw_response );
				if ( '' !== $body && 'N;' !== $body ) {
					return $body;
				}
			}
		}

		//The above failed
		//If we're on a second pass already then there's nothing left to do but bail
		if ( true === $second_pass ) {
			return false;
		}

		//We're still on the first pass, try to get just the name of the directory of the plugin
		$parts = explode( '/', $plugin );

		//Sanity check that we have two parts, a directory and a file name
		if ( 2 === count( $parts ) ) {
			//Try this entire function using just the directory name
			return $this->try_get_response_body( $parts[0], true );
		}

		//Everything above failed, bail
		return false;
	}
}

