<?PHP

class BrowserCapabilities extends Plugin
{

    /**
     * Register information about this plugin.
     */
    public function Register()
    {
        $this->Name = "Browser Capabilities";
        $this->Version = "1.0.1";
        $this->Description = "A wrapper to the
             <a href=\"https://github.com/garetjax/phpbrowscap\">phpbrowscap</a>
             library, which is a standalone class for PHP5 that gets around the
             limitations of
             <a href=\"http://php.net/manual/en/function.get-browser.php\">get_browser()</a>
             and manages the whole thing. It offers methods to update, cache,
             adapt and get details about every supplied user agent on a
             standalone basis.";
        $this->Author = "Internet Scout";
        $this->Url = "http://scout.wisc.edu/";
        $this->Email = "scout@scout.wisc.edu";
        $this->Requires = array("CWISCore" => "2.1.0");
        $this->EnabledByDefault = TRUE;
    }

    /**
     * Re-initialize on wakeup.
     */
    public function __wakeup()
    {
        $this->Initialize();
    }

    /**
     * Create the cache path if necessary.
     * @return NULL on success, error message on error
     */
    public function Install()
    {
        $Result = $this->CheckCache();

        if (!is_null($Result))
        {
            return $Result;
        }

        return NULL;
    }

    /**
     * Upgrade from a previous version.
     * @param string $PreviousVersion previous version
     * @return stringnull error message on error or NULL if no errors occurred
     */
    public function Upgrade($PreviousVersion)
    {
        # ugprade from versions < 1.0.1 to 1.0.1
        if (version_compare($PreviousVersion, "1.0.1", "<"))
        {
            $OldCachePath = getcwd() . "/tmp/caches/BrowserCapabilities";

            # see if the old cache directory still exists
            if (file_exists($OldCachePath))
            {
                # remove the old cache directory
                $Result = RemoveFromFilesystem($OldCachePath);

                # could not remove the old cache directory
                if (!$Result)
                {
                    $Message = "Could not remove the old cache directory (";
                    $Message .= $OldCachePath . ").";

                    return $Message;
                }
            }
        }

        return NULL;
    }

    /**
     * Uninstall the plugin.
     * @return NULL|string NULL if successful or an error message otherwise
     */
    public function Uninstall()
    {
        if (!RemoveFromFilesystem($this->GetCachePath()))
        {
            return "Could not delete the cache directory.";
        }
    }

    /**
     * Make sure the phpbrowscap library is loaded properly and that the cache
     * directory is useable.
     * @return NULL on success, error message on error
     */
    public function Initialize()
    {
        $LibraryFilesToLoad = array("phpbrowscap/Browscap.php");

        foreach ($LibraryFilesToLoad as $Path)
        {
            $Result = @include_once($Path);

            if (FALSE === $Result)
            {
                return "Could not load <i>" . $Path . "</i>.";
            }
        }

        # make sure the Browscap class was successfully loaded
        if (!class_exists("Browscap"))
        {
            return "The Browscap class has not been loaded.";
        }

        # check the cache directory and files
        $Result = $this->CheckCache();

        if (!is_null($Result))
        {
            return $Result;
        }

        return NULL;
    }

    /**
     * Declare the events this plugin provides to the application framework.
     * @return an array of the events this plugin provides
     */
    public function DeclareEvents()
    {
        return array(
            "BROWSCAP_GET_BROWSER"
              => ApplicationFramework::EVENTTYPE_FIRST,
            "BROWSCAP_BROWSER_CHECK"
              => ApplicationFramework::EVENTTYPE_FIRST);
    }

    /**
     * Return event hooks to the application framework.
     * @return an array of events to be hooked into the application framework
     */
    public function HookEvents()
    {
        return array(
            "EVENT_PAGE_LOAD" => "Bootstrap",
            "EVENT_MONTHLY" => "MonthlyUpdate",
            "BROWSCAP_GET_BROWSER" => "GetBrowser",
            "BROWSCAP_BROWSER_CHECK" => "BrowserCheck");
    }

    /**
     * Inject a callback into the application framework.
     */
    public function Bootstrap()
    {
        global $AF;

        $AF->SetBrowserDetectionFunc(array($this, "BrowserDetectionFunc"));
    }

    /**
     * Update the browser list cache every month.
     * @param string $LastRunAt datetime string the method was last called
     */
    public function MonthlyUpdate($LastRunAt)
    {
        try
        {
            $Browscap = new Browscap($this->GetCachePath());
            $Browscap->updateMethod = Browscap::UPDATE_FSOCKOPEN;
            $Browscap->updateCache();
        } catch (Exception $Exception) {}
    }

    /**
     * Inject browser names into the application framework.
     * @param $UserAgent custom user agent string to use
     * @return array of browser names
     */
    public function BrowserDetectionFunc($UserAgent=NULL)
    {
        $Browsers = array();
        $Capabilities = $this->GetBrowser($UserAgent, TRUE);

        # add browser name
        if (isset($Capabilities["Browser"]))
        {
            $Name = $Capabilities["Browser"];
            $Browsers[] = $Name;

            # add version-specific name too
            if (isset($Capabilities["MajorVer"]))
            {
                $VersionedName = $Name . $Capabilities["MajorVer"];
                $Browsers[] = $VersionedName;
            }
        }

        return $Browsers;
    }

    /**
     * Get the user agent's name only.
     * @param $UserAgent custom user agent string to use
     * @param $ReturnArray TRUE to return an array instead of an object
     * @return an object of capabilities or an empty object on error
     */
    public function GetBrowserName($UserAgent=NULL)
    {
        $Capabilities = $this->GetBrowser($UserAgent, TRUE);

        return isset($Capabilities["Browser"]) ? $Capabilities["Browser"] : NULL;
    }

    /**
     * Get the user agent's capabilities. Updates the cache after 30 days.
     * @param $UserAgent custom user agent string to use
     * @param $ReturnArray TRUE to return an array instead of an object
     * @return an object of capabilities or an empty object on error
     */
    public function GetBrowser($UserAgent=NULL, $ReturnArray=FALSE)
    {
        try
        {
            $Browscap = new Browscap($this->GetCachePath());
            $Browscap->doAutoUpdate = FALSE;

            $Capabilities = @$Browscap->getBrowser($UserAgent, $ReturnArray);
        }

        catch (Browscap_Exception $Exception)
        {
            # should use "new stdClass;" instead of type casting, but it's
            # unclear when stdClass was introduced to PHP, so take the safer
            # route instead
            $Capabilities = ($ReturnArray) ? array() : (object) array();
        }

        return $Capabilities;
    }

    /**
     * Check if the given constraints are true for the user agent.
     * @param $Constraints constraints to test for
     * @param $UserAgent custom user agent string to use
     */
    public function BrowserCheck(array $Constraints, $UserAgent=NULL)
    {
        static $CapabilityMap;

        if (!is_array($CapabilityMap) || !array_key_exists($UserAgent, $CapabilityMap))
        {
            $CapabilityMap[$UserAgent] = $this->GetBrowser($UserAgent);
        }

        $Capabilities = $CapabilityMap[$UserAgent];

        foreach ($Constraints as $Key => $Value)
        {
            $CapabilityExists = property_exists($Capabilities, $Key);

            if (!$CapabilityExists || $Capabilities->$Key != $Value)
            {
                return FALSE;
            }
        }

        return TRUE;
    }

    /**
     * Get the path of the cache directory.
     * @return the path of the cache directory
     */
    private function GetCachePath()
    {
        $CachePath = getcwd() . "/local/data/caches/BrowserCapabilities";

        return $CachePath;
    }

    /**
     * Check both the cache directory and the cache files
     * @param NULL on success, error message on error
     */
    private function CheckCache()
    {
        $Result = $this->CheckCacheDirectory();

        if (!is_null($Result))
        {
            return $Result;
        }

        $Result = $this->CheckCacheFiles();

        if (!is_null($Result))
        {
            return $Result;
        }

        return NULL;
    }

    /**
     * Check the cache directory, creating it if necessary and allowed to.
     * @param NULL on success, error message on error
     */
    private function CheckCacheDirectory()
    {
        $Path = $this->GetCachePath();

        # the cache directory doesn't exist, try to create it
        if (!file_exists($Path))
        {
            $Result = @mkdir($Path, 0777, TRUE);

            if (FALSE === $Result)
            {
                return "The cache directory (".$Path.") could not be created.";
            }
        }

        # exists, but is not a directory
        if (!is_dir($Path))
        {
            return "(".$Path.") is not a directory.";
        }

        # exists and is a directory, but is not writeable
        if (!is_writeable($Path))
        {
            return "The cache directory (".$Path.") is not writeable.";
        }

        return NULL;
    }

    /**
     * Check that the cache files either don't exist or are writeable.
     * @param NULL on success, error message on error
     */
    private function CheckCacheFiles()
    {
        $CachePath = $this->GetCachePath();
        $CacheFiles = array("browscap.ini", "cache.php");

        foreach ($CacheFiles as $Filename)
        {
            $FilePath = $CachePath . "/" . $Filename;
            $IsWriteableFile = (is_writeable($FilePath) && is_file($FilePath));

            # the files should either be writeable or not exist
            if (!$IsWriteableFile && file_exists($FilePath))
            {
                return "Cache file (".$FilePath.") is unwriteable.";
            }
        }

        return NULL;
    }

}
