<?php
/*
 * This file is part of MODX Revolution.
 *
 * Copyright (c) MODX, LLC. All Rights Reserved.
 *
 * For complete copyright and license information, see the COPYRIGHT and LICENSE
 * files found in the top-level directory of this distribution.
 */

/**
 * Represents a virtual site context within a modX repository.
 *
 * @property string $key The key of the context
 * @property string $description The description of the context
 *
 * @package modx
 */
class modContext extends modAccessibleObject {
    /**
     * An array of configuration options for this context
     * @var array $config
     */
    public $config= null;
    /**
     * The alias map for this context
     * @var array $aliasMap
     */
    public $aliasMap= null;
    /**
     * The resource map for all resources in this context
     * @var array $resourceMap
     */
    public $resourceMap= null;
    /**
     * A map of WebLink Resources with their target URLs
     * @var array $webLinkMap
     */
    public $webLinkMap= null;
    /**
     * The event map for all events being executed in this context
     * @var array $eventMap
     */
    public $eventMap= null;
    /**
     * The plugin cache array for all plugins being fired in this context
     * @var array $pluginCache
     */
    public $pluginCache= null;
    /**
     * The key for the cache for this context
     * @var string $_cacheKey
     */
    protected $_cacheKey= '[contextKey]/context';

    /**
     * Prepare and execute a PDOStatement to retrieve data needed for $aliasMap and $resourceMap.
     *
     * @static
     * @param modContext &$context A reference to a specific modContext instance.
     * @return PDOStatement|bool A PDOStatement, prepared and executed, with the map data, or false
     * if the statement could not be prepared or executed.
     */
    public static function getResourceCacheMapStmt(&$context) {
        return false;
    }

    /**
     * Prepare and execute a PDOStatement to retrieve data needed for $webLinkMap.
     *
     * @static
     * @param modContext &$context A reference to a specific modContext instance.
     * @return PDOStatement|bool A PDOStatement, prepared and executed, with the map data, or false
     * if the statement could not be prepared or executed.
     */
    public static function getWebLinkCacheMapStmt(&$context) {
        return false;
    }

    /**
     * Prepare a context for use.
     *
     * @uses modCacheManager::generateContext() This method is responsible for
     * preparing the context for use.
     * {@internal You can override this behavior here, but you will only need to
     * override the modCacheManager::generateContext() method in most cases}}
     * @access public
     * @param boolean $regenerate If true, the existing cache file will be ignored
     * and regenerated.
     * @return boolean Indicates if the context was successfully prepared.
     */
    public function prepare($regenerate= false, array $options = array()) {
        $prepared= false;
        if ($this->config === null || $regenerate) {
            if ($this->xpdo->getCacheManager()) {
                $context = array();
                if ($regenerate || !($context = $this->xpdo->cacheManager->get($this->getCacheKey(), array(
                    xPDO::OPT_CACHE_KEY => $this->xpdo->getOption('cache_context_settings_key', null, 'context_settings'),
                    xPDO::OPT_CACHE_HANDLER => $this->xpdo->getOption('cache_context_settings_handler', null, $this->xpdo->getOption(xPDO::OPT_CACHE_HANDLER, null, 'cache.xPDOFileCache')),
                    xPDO::OPT_CACHE_FORMAT => (integer) $this->xpdo->getOption('cache_context_settings_format', null, $this->xpdo->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)),
                )))) {
                    $context = $this->xpdo->cacheManager->generateContext($this->get('key'), $options);
                }
                if (!empty($context)) {
                    foreach ($context as $var => $val) {
                        if ($var === 'policies') {
                            $this->setPolicies($val);
                            continue;
                        }
                        $this->$var = $val;
                    }
                    $prepared= true;
                }
            }
        } else {
            $prepared= true;
        }
        return $prepared;
    }

    /**
     * Returns a context-specific setting value.
     *
     * @param string $key The option key to check.
     * @param string $default A default value to use if not found.
     * @param array $options An array of additional options to merge over top of
     * the context settings.
     * @return mixed The option value or the provided default.
     */
    public function getOption($key, $default = null, $options = null) {
        if (is_array($options)) {
            $options = array_merge($this->config, $options);
        } else {
            $options =& $this->config;
        }
        return $this->xpdo->getOption($key, $options, $default);
    }

    /**
     * Returns the file name representing this context in the cache.
     *
     * @access public
     * @return string The cache filename.
     */
    public function getCacheKey() {
        if ($this->get('key')) {
            $this->_cacheKey= str_replace('[contextKey]', $this->get('key'), $this->_cacheKey);
        } else {
            $this->_cacheKey= str_replace('[contextKey]', uniqid('ctx_'), $this->_cacheKey);
        }
        return $this->_cacheKey;
    }

    /**
     * Loads the access control policies applicable to this element.
     *
     * {@inheritdoc}
     */
    public function findPolicy($context = '') {
        $policy = array();
        $enabled = true;
        $context = !empty($context) ? $context : $this->xpdo->context->get('key');
        if (!is_object($this->xpdo->context) || $context === $this->xpdo->context->get('key')) {
            $enabled = (boolean) $this->xpdo->getOption('access_context_enabled', null, true);
        } elseif ($this->xpdo->getContext($context)) {
            $enabled = (boolean) $this->xpdo->contexts[$context]->getOption('access_context_enabled', true);
        }
        if ($enabled) {
            if (empty($this->_policies) || !isset($this->_policies[$context])) {
                $c = $this->xpdo->newQuery('modAccessContext');
                $c->leftJoin('modAccessPolicy','Policy');
                $c->select(array(
                    'modAccessContext.id',
                    'modAccessContext.target',
                    'modAccessContext.principal',
                    'modAccessContext.authority',
                    'modAccessContext.policy',
                    'Policy.data',
                ));
                $c->where(array(
                    'modAccessContext.principal_class' => 'modUserGroup',
                    'modAccessContext.target' => $this->get('key'),
                ));
                $c->sortby('modAccessContext.target,modAccessContext.principal,modAccessContext.authority,modAccessContext.policy');
                $acls = $this->xpdo->getCollection('modAccessContext',$c);
                foreach ($acls as $acl) {
                    $policy['modAccessContext'][$acl->get('target')][] = array(
                        'principal' => $acl->get('principal'),
                        'authority' => $acl->get('authority'),
                        'policy' => $acl->get('data') ? $this->xpdo->fromJSON($acl->get('data'), true) : array(),
                    );
                }
                $this->_policies[$context] = $policy;
            } else {
                $policy = $this->_policies[$context];
            }
        }
        return $policy;
    }

    /**
     * Generates a URL representing a specified resource in this context.
     *
     * Note that if this method is called from a context other than the one
     * initialized for the modX instance, and the scheme is not specified, an
     * empty string, or abs, the method will force the scheme to full.
     *
     * @access public
     * @param integer $id The id of a resource.
     * @param string $args A query string to append to the generated URL.
     * @param mixed $scheme The scheme indicates in what format the URL is generated.<br>
     * <pre>
     *      -1 : (default value) URL is relative to site_url
     *       0 : see http
     *       1 : see https
     *    full : URL is absolute, prepended with site_url from config
     *     abs : URL is absolute, prepended with base_url from config
     *    http : URL is absolute, forced to http scheme
     *   https : URL is absolute, forced to https scheme
     * </pre>
     * @param array $options An array of options for generating the Resource URL.
     * @return string The URL for the resource.
     */
    public function makeUrl($id, $args = '', $scheme = -1, array $options = array()) {
        $url = '';
        $found = false;
        if ($id= intval($id)) {
            if ($this->config === null) {
                $this->prepare();
            }
            if (is_object($this->xpdo->context) && $this->get('key') !== $this->xpdo->context->get('key')) {
                $config = array_merge($this->xpdo->_systemConfig, $this->config, $this->xpdo->_userConfig, $options);
                if ($scheme === -1 || $scheme === '-1' || $scheme === '' || strpos($scheme, 'abs') !== false) {
                    $scheme= 'full';
                }
            } else {
                $config = array_merge($this->xpdo->config, $options);
            }

            if ($config['friendly_urls'] == 1) {
                if ((integer) $id === (integer) $config['site_start']) {
                    $alias= ($scheme === '' || $scheme === -1 || $scheme === '-1') ? $config['base_url'] : '';
                    $found= true;
                } else {
                    $alias= $this->getResourceURI($id);
                    if (!$alias) {
                        $alias= '';
                        $this->xpdo->log(xPDO::LOG_LEVEL_WARN, '`' . $id . '` was requested but no alias was located.');
                    } else {
                        $found= true;
                    }
                }
            } elseif (array_keys(array((string) $id), $this->resourceMap, true) !== false) {
                $found= true;
            }

            if ($found) {
                $target = null;
                if (isset($config['use_weblink_target']) && !empty($config['use_weblink_target'])) {
                    if (array_key_exists($id, $this->webLinkMap)) {
                        $target = $this->webLinkMap[$id];
                        if (!empty($target)) {
                            $alias = $target;
                        }
                    }
                }
                $targetHasQS = (empty($config['friendly_urls']) || strpos($alias, '?') !== false);
                if (is_array($args)) {
                    $args = modX::toQueryString($args);
                }
                if ($args != '') {
                    if (!$targetHasQS) {
                        /* add ? to $args if missing */
                        $c= substr($args, 0, 1);
                        if ($c == '&') {
                            $args= '?' . substr($args, 1);
                        } elseif ($c != '?') {
                            $args= '?' . $args;
                        }
                    } elseif ($args != '') {
                        /* add & to $args if missing */
                        $c= substr($args, 0, 1);
                        if ($c == '?')
                            $args= '&' . substr($args, 1);
                        elseif ($c != '&') $args= '&' . $args;
                    }
                }
                if ($config['friendly_urls'] == 1 || $target !== null) {
                    $url= $alias . $args;
                } else {
                    $url= $config['request_controller'] . '?' . $config['request_param_id'] . '=' . $id . $args;
                }

                $host= '';
                if ($target === null && $scheme !== -1 && $scheme !== '') {
                    if ($scheme === 1 || $scheme === 0) {
                        $https_port= $this->getOption('https_port',$config,443);
                        $isSecure= ($_SERVER['SERVER_PORT'] == $https_port || (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS'])=='on')) ? 1 : 0;
                        if ($scheme != $isSecure) {
                            $scheme = $isSecure ? 'http' : 'https';
                        }
                    }
                    switch ($scheme) {
                        case 'full':
                            $host= $config['site_url'];
                            break;
                        case 'abs':
                        case 'absolute':
                            $host= $config['base_url'];
                            break;
                        case 'https':
                        case 'http':
                            $host= $scheme . '://' . $config['http_host'] . $config['base_url'];
                            break;
                    }
                    $url= $host . $url;
                }
            } else {
                $this->xpdo->log(
                    xPDO::LOG_LEVEL_INFO,
                    "Resource with id {$id} was not found in context {$this->key}",
                    '',
                    __METHOD__,
                    $this->xpdo->resource ? "resource {$this->xpdo->resource->id}" : __FILE__,
                    $this->xpdo->resource ? '' : __LINE__
                );
            }
        }
        if ($this->xpdo->getDebug() === true) {
            $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "modContext[" . $this->get('key') . "]->makeUrl({$id}) = {$url}");
        }
        return $url;
    }

    /**
     * Overrides xPDOObject::remove to fire modX-specific events
     *
     * {@inheritDoc}
     */
    public function remove(array $ancestors = array()) {
        if ($this->xpdo instanceof modX) {
            $this->xpdo->invokeEvent('OnContextBeforeRemove',array(
                'context' => &$this,
                'ancestors' => $ancestors,
            ));
        }

        $removed = parent :: remove($ancestors);

        if ($removed && $this->xpdo instanceof modX) {
            $this->xpdo->invokeEvent('OnContextRemove',array(
                'context' => &$this,
                'ancestors' => $ancestors,
            ));
        }
        return $removed;
    }

    /**
     * Overrides xPDOObject::save to fire modX-specific events.
     *
     * {@inheritDoc}
     */
    public function save($cacheFlag= null) {
        $isNew = $this->isNew();

        if ($this->xpdo instanceof modX) {
            $this->xpdo->invokeEvent('OnContextBeforeSave',array(
                'context' => &$this,
                'mode' => $isNew ? modSystemEvent::MODE_NEW : modSystemEvent::MODE_UPD,
                'cacheFlag' => $cacheFlag,
            ));
        }

        $saved = parent :: save($cacheFlag);

        if ($saved && $this->xpdo instanceof modX) {
            $this->xpdo->invokeEvent('OnContextSave',array(
                'context' => &$this,
                'mode' => $isNew ? modSystemEvent::MODE_NEW : modSystemEvent::MODE_UPD,
                'cacheFlag' => $cacheFlag,
            ));
        }
        return $saved;
    }

    /**
     * Get and execute a PDOStatement representing data for the aliasMap and resourceMap.
     *
     * @return PDOStatement|null
     */
    public function getResourceCacheMap() {
        return $this->xpdo->call('modContext', 'getResourceCacheMapStmt', array(&$this));
    }

    /**
     * Get and execute a PDOStatement representing data for the webLinkMap.
     *
     * @return PDOStatement|null
     */
    public function getWebLinkCacheMap() {
        return $this->xpdo->call('modContext', 'getWebLinkCacheMapStmt', array(&$this));
    }

    /**
     * Get a Resource URI in this Context by id.
     *
     * @param string|integer $id The integer id of the Resource.
     * @return string|bool The URI of the Resource, or false if not found in this Context.
     */
    public function getResourceURI($id) {
        if ($this->getOption('cache_alias_map') && isset($this->aliasMap)) {
            $uri = array_search($id, $this->aliasMap);
        } else {
            $query = $this->xpdo->newQuery('modResource', array(
                'id' => $id,
                'deleted' => false,
                'context_key' => $this->get('key')
            ));
            $query->select($this->xpdo->getSelectColumns('modResource', '', '', array('uri')));
            $uri = $this->xpdo->getValue($query->prepare());
        }
        return $uri;
    }
}
