<?php

/**
 * @author W-Shadow
 * @copyright 2008-2012
 */
 
//Load JSON functions for PHP < 5.2
if ( !(function_exists('json_encode') && function_exists('json_decode')) && !class_exists('Services_JSON') ){
	$class_json_path = ABSPATH . WPINC . '/class-json.php';
	if ( file_exists($class_json_path) ){
		require $class_json_path;
	}
}

class MenuEd_ShadowPluginFramework {
	public static $framework_version = '0.4.1';
	
	public $is_mu_plugin = null; //True if installed in the mu-plugins directory, false otherwise
	
	protected $options = array();
	public $option_name = ''; //should be set or overridden by the plugin
	protected $defaults = array(); //should be set or overridden by the plugin
	protected $sitewide_options = false; //WPMU only : save the setting in a site-wide option
	protected $serialize_with_json = false; //Use the JSON format for option storage
	protected $zlib_compression = false;
	
	public $plugin_file = ''; //Filename of the plugin.
	public $plugin_basename = ''; //Basename of the plugin, as returned by plugin_basename().
	public $plugin_dir_url = ''; //The URL of the plugin's folder
	
	protected $magic_hooks = false; //Automagically set up hooks for all methods named "hook_[hookname]" .
	protected $magic_hook_priority = 10; //Priority for magically set hooks.
	
	protected $settings_link = ''; //If set, this will be automatically added after "Deactivate"/"Edit". 
	
  /**
   * Class constructor. Populates some internal fields, then calls the plugin's own 
   * initializer (if any).
   *
   * @param string $plugin_file Plugin's filename. Usually you can just use __FILE__.
   * @param string $option_name
   */
	function __construct( $plugin_file = '', $option_name = null ){
		if ($plugin_file == ''){
			//Try to guess the name of the file that included this file.
			//Not implemented yet.
		}
		$this->option_name = $option_name;
		
		if ( is_null($this->is_mu_plugin) )
			$this->is_mu_plugin = $this->is_in_wpmu_plugin_dir($plugin_file);
		
		$this->plugin_file = $plugin_file;
		$this->plugin_basename = plugin_basename($this->plugin_file);
		
		$this->plugin_dir_url = rtrim(plugin_dir_url($this->plugin_file), '/');

		/************************************
				Add the default hooks
		************************************/
		add_action('activate_'.$this->plugin_basename, array($this,'activate'));
		add_action('deactivate_'.$this->plugin_basename, array($this,'deactivate'));
		
		$this->init();        //Call the plugin's init() function
		$this->init_finish(); //Complete initialization by loading settings, etc
	}
	
	/**
	 * Init the plugin. Should be overridden in a sub-class.
	 * Called by the class constructor.
	 * 
	 * @return void
	 */
	function init(){
		//Do nothing.
	}
	
	/**
	 * Initialize settings and set up magic hooks. 
	 * 
	 * @return void
	 */
	function init_finish(){
		/************************************
				Load settings
		************************************/
		//The provided $option_name overrides the default only if it is set to something useful
		if ( $this->option_name == '' )  {
			//Generate a unique name 
			$this->option_name = 'plugin_'.md5($this->plugin_basename);
		}
		
		//Do we need to load the plugin's settings?
		if ($this->option_name != null){
			$this->load_options();
		}
		
		//Add a "Settings" action link
		if ($this->settings_link)
			add_filter('plugin_action_links', array($this, 'plugin_action_links'), 10, 2);
		
		if ($this->magic_hooks)
			$this->set_magic_hooks();
	}
	
  /**
   * Load the plugin's configuration.
   * Loads the specified option into $this->options, substituting defaults where necessary.
   *
   * @param string $option_name Optional. The slug of the option to load. If not set, the value of $this->option_name will be used instead.
   * @return boolean TRUE if options were loaded okay and FALSE otherwise. 
   */
	function load_options($option_name = null){
		if ( empty($option_name) ){
			$option_name = $this->option_name;
		}
		
		if ( $this->sitewide_options ) {
			$this->options = get_site_option($option_name);
		} else {
			$this->options = get_option($option_name);
		}

		$prefix = 'gzcompress:';
		if (
			is_string($this->options)
			&& (substr($this->options, 0, strlen($prefix)) === $prefix)
			&& function_exists('gzuncompress')
		) {
			//TODO: Maybe this would be faster if we stored the flag separately?
			$this->options = unserialize(gzuncompress(base64_decode(substr($this->options, strlen($prefix)))));
		}

		if ( $this->serialize_with_json || is_string($this->options) ){
			$this->options = $this->json_decode($this->options, true);
		}
		
		if(!is_array($this->options)){
			$this->options = $this->defaults;
			return false;
		} else {
			$this->options = array_merge($this->defaults, $this->options);
			return true;
		}
	}
	
  /**
   * ShadowPluginFramework::save_options()
   * Saves the $options array to the database.
   *
   * @return bool
   */
	function save_options(){
		if ($this->option_name) {
			$stored_options = $this->options;
			if ( $this->serialize_with_json ){
				$stored_options = $this->json_encode($stored_options);
			}

			if ( $this->zlib_compression && function_exists('gzcompress') ) {
				$stored_options = 'gzcompress:' . base64_encode(gzcompress(serialize($stored_options)));
			}

			if ( $this->sitewide_options && is_multisite() ) {
				return self::atomic_update_site_option($this->option_name, $stored_options);
			} else {
				return update_option($this->option_name, $stored_options);
			}
		}
		return false;
	}

	/**
	 * Like update_site_option, but simulates record locking by using the MySQL GET_LOCK() function.
	 *
	 * The goal is to reduce the risk of triggering a race condition in update_site_option.
	 * It would be better to use real transactions, but many (most?) WordPress sites use storage engines
	 * that don't support transactions, like MyISAM.
	 *
	 * @param string $option_name
	 * @param mixed $data
	 * @return bool
	 */
	public static function atomic_update_site_option($option_name, $data) {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery -- WPDB doesn't have utility methods for locking, so direct queries are needed.
		global $wpdb; /** @var wpdb $wpdb */
		$lock = 'ame.' . (is_multisite() ? $wpdb->sitemeta : $wpdb->options ) . '.' . $option_name;

		//Lock. Note that we're being really optimistic and not checking the return value.
		$wpdb->query($wpdb->prepare("SELECT GET_LOCK(%s, %d)", $lock, 5));
		//Update.
		$updated = update_site_option($option_name, $data);
		//Unlock.
		$wpdb->query($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock));

		return $updated;
		// phpcs:enable
	}
	
	
  /**
   * Backwards compatible json_decode.
   *
   * @param string $data
   * @param bool $assoc Decode objects as associative arrays.
   * @return mixed
   */
    function json_decode($data, $assoc=false){
    	if ( function_exists('json_decode') ){
    		return json_decode($data, $assoc);
    	}
    	if ( class_exists('Services_JSON') ){
    		$flag = $assoc?SERVICES_JSON_LOOSE_TYPE:0;
	        $json = new Services_JSON($flag);
	        return( $json->decode($data) );
    	} else {
			throw new RuntimeException('No JSON parser available');
    	}
    }

  /**
   * Backwards compatible json_encode.
   *
   * @param mixed $data
   * @return string
   */
    function json_encode($data) {
	    if ( function_exists('wp_json_encode') ) {
		    return wp_json_encode($data);
	    } else if ( function_exists('json_encode') ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Fallback.
		    return json_encode($data);
	    }

    	if ( class_exists('Services_JSON') ){
    		$json = new Services_JSON();
        	return( $json->encodeUnsafe($data) );
    	} else {
		    throw new RuntimeException('No JSON parser available');
   		}
    }    

	
  /**
   * ShadowPluginFramework::set_magic_hooks()
   * Automagically sets up hooks for all methods named "hook_[tag]". Uses the Reflection API.
   *
   * @return void
   */
	function set_magic_hooks(){
		$class = new ReflectionClass(get_class($this));
		$methods = $class->getMethods();
		
		foreach ($methods as $method){ /** @var ReflectionMethod $method */
			//Check if the method name starts with "hook_"
			if (strpos($method->name, 'hook_') === 0){
				//Get the hook's tag from the method name 
				$hook = substr($method->name, 5);
				//Add the hook. Uses add_filter because add_action is simply a wrapper of the same.
				add_filter($hook, array($this, $method->name),
					$this->get_magic_hook_priority(), $method->getNumberOfParameters());
			}
		}
		
		unset($class);
	}

	public function get_magic_hook_priority() {
		return $this->magic_hook_priority;
	}
	

  /**
   * ShadowPluginFramework::activate()
   * Stub function for the activation hook.
   *
   * @return void
   */
	function activate(){

	}
	
  /**
   * ShadowPluginFramework::deactivate()
   * Stub function for the deactivation hook. Does nothing. 
   *
   * @return void
   */
	function deactivate(){
		
	}
	
  /**
   * ShadowPluginFramework::plugin_action_links()
   * Adds a "Settings" link to the plugin's action links. Default handler for the 'plugin_action_links' hook. 
   *
   * @param array $links
   * @param string $file
   * @return array
   */
	function plugin_action_links($links, $file) {
        if (($file == $this->plugin_basename) && is_array($links)) {
	        $links[] = "<a href='" . $this->settings_link . "'>" . __('Settings') . "</a>";
        }
        return $links;
    }
    
  /**
   * ShadowPluginFramework::uninstall()
   * Default uninstaller. Removes the plugins configuration record (if available). 
   *
   * @return void
   */
    function uninstall(){
		if ($this->option_name)
			delete_option($this->option_name);
	}
	
  /**
   * Checks if the specified file is inside the mu-plugins directory.
   *
   * @param string $filename The filename to check. Leave blank to use the current plugin's filename. 
   * @return bool
   */
	function is_in_wpmu_plugin_dir( $filename = '' ){
		if ( !defined('WPMU_PLUGIN_DIR') ) {
			return false;
		}
		
		if ( empty($filename) ){
			$filename = $this->plugin_file;
		}

		$normalizedMuPluginDir = realpath(WPMU_PLUGIN_DIR);
		$normalizedFileName = realpath($filename);

		//If realpath() fails, just normalize the syntax instead.
		if ( empty($normalizedFileName) || empty($normalizedMuPluginDir) ) {
			$normalizedMuPluginDir = wp_normalize_path(WPMU_PLUGIN_DIR);
			$normalizedFileName = wp_normalize_path($filename);
		}
		//Yet another fallback if the above also fails.
		if ( !is_string($normalizedMuPluginDir) || empty($normalizedMuPluginDir) ) {
			if ( is_string(WPMU_PLUGIN_DIR) ) {
				$normalizedMuPluginDir = WPMU_PLUGIN_DIR;
			} else {
				return false;
			}
		}
		return (strpos( $normalizedFileName, $normalizedMuPluginDir ) !== false);
	}
	
	/**
	 * Check if the plugin is active for the entire network.  
	 * Will return true when the plugin is installed in /mu-plugins/ (WPMU, pre-3.0)
	 * or has been activated via "Network Activate" (WP 3.0+).
	 * 
	 * Blame the ridiculous blog/site/network confusion perpetrated by 
	 * the WP API for the silly name.
	 * 
	 * @return bool
	 */
	function is_super_plugin(){
		if ( is_null($this->is_mu_plugin) ){
			$this->is_mu_plugin = $this->is_in_wpmu_plugin_dir($this->plugin_file);
		}
		
		if ( $this->is_mu_plugin ){
			return true;
		} else {
			return $this->is_plugin_active_for_network($this->plugin_basename);
		}
	}
	
	/**
	 * Check whether the plugin is active for the entire network.
	 * 
	 * Silly WP doesn't load the file that contains this native function until *after* 
	 * all plugins are loaded, so until then we use a copy-pasted version of the same.
	 * 
	 * @param string $plugin
	 * @return bool
	 */
	function is_plugin_active_for_network( $plugin ) {
		if ( function_exists('is_plugin_active_for_network') ){
			return 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;
	}

	/**
	 * Check whether the plugin is active.
	 *
	 * @see self::is_plugin_active_for_network
	 *
	 * @param string $plugin
	 * @return bool
	 */
	function is_plugin_active($plugin) {
		if ( function_exists('is_plugin_active') ) {
			return is_plugin_active($plugin);
		}
		return in_array( $plugin, (array) get_option('active_plugins', array()) ) || $this->is_plugin_active_for_network($plugin);
	}
	
}