<?PHP

class BrowserCapabilities extends Plugin
{

    /**
     * Register information about this plugin.
     */
    public function Register()
    {
        $this->Name = "Browser Capabilities";
        $this->Version = "1.0.4";
        $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;

        $this->CfgSetup["EnableDeveloper"] = array(
            "Type" => "Flag",
            "Label" => "Enable Developer Interface",
            "Help" => "Enable an additional developer interface to aid in debugging the plugin.",
            "OnLabel" => "Yes",
            "OffLabel" => "No");

        $this->CfgSetup["MemLimitForUpdate"] = array(
            "Type" => "Number",
            "Label" => "Memory Limit for Updates",
            "Help" => "Currently, the monthly update of browser data from browscap.org can"
                      ."use quite a bit of memory.  To keep from running out, the PHP memory limit"
                      ."is temporarily increased to this value while the update runs.",
            "Units" => "MB",
            "Default" => 256);

        $this->CfgSetup["INIType"] = array(
             "Type" => "Option",
             "Label" => "Browscap Database Type",
             "Help" => "Browscap.org offers 'lite', 'full', and 'standard' versions."
               ."The 'lite' version contains only major browsers and search engines, and "
               ."'should be adequate for most websites'.  The 'standard' version contains "
               ."browscap.org's full list of browsers, with basic information about them. "
               ."The full version extends the properties of the 'standard' version with "
               ."additional detail (e.g., details of rendering engine types and versions).",
             "Options" => Array(
               "Lite_PHP_BrowsCapINI" => "Lite",
               "PHP_BrowsCapINI" => "Standard",
               "Full_PHP_BrowsCapINI" => "Full"),
             "Default" => "Lite");
    }

    /**
     * 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()
    {
        $this->ConfigSetting("EnableDeveloper", FALSE);

        $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;
                }
            }
        }

        if (version_compare($PreviousVersion, "1.0.2", "<"))
        {
            $this->ConfigSetting("EnableDeveloper", FALSE);
        }

        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("lib/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,
            "BROWSCAP_FORCE_UPDATE"
              => ApplicationFramework::EVENTTYPE_DEFAULT,
            "BROWSCAP_GET_UPDATE_TIME"
              => 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()
    {
        $Events= array(
            "EVENT_PAGE_LOAD" => "Bootstrap",
            "EVENT_MONTHLY" => "MonthlyUpdate",
            "BROWSCAP_GET_BROWSER" => "GetBrowser",
            "BROWSCAP_BROWSER_CHECK" => "BrowserCheck");
        if ($this->ConfigSetting("EnableDeveloper"))
        {
            $Events["BROWSCAP_FORCE_UPDATE"] = "MonthlyUpdate";
            $Events["BROWSCAP_GET_UPDATE_TIME"] = "GetUpdateTime";
            $Events["EVENT_DEVELOPER_SUPPORT_MENU"] = "DeclareDevSupportPages";
        }

        return $Events;
    }


    /**
     * Add page hooks for the system administration section. This should only
     * be called if EnableDeveloper is TRUE.
     * @return map page name to page title for the application framework
     */
    function DeclareDevSupportPages()
    {
        return array(
            "Developer" => "BrowserCapabilities Developer Page",
            );
    }

    /**
     * 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
       {
            ini_set("memory_limit",
                $this->ConfigSetting("MemLimitForUpdate")."M");
            $Browscap = new Browscap($this->GetCachePath());
            $Browscap->remoteIniUrl = 'http://browscap.org/stream?q='
                .$this->ConfigSetting("INIType");
            $Browscap->updateMethod = Browscap::UPDATE_FSOCKOPEN;
            $Browscap->updateCache();
        } catch (Exception $Exception) {}
    }

    /**
     * Determine when the browscap file was last update.
     * @return string Date of last update.
     */
    public function GetUpdateTime()
    {
        $BrowscapFile = $this->GetCachePath()."/browscap.ini";
        if (is_file($BrowscapFile))
            return date("Y-m-d H:i:s", filemtime( $BrowscapFile ));
        else
            return "NEVER";
    }

    /**
     * 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 (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];

        if (count($Capabilities)==0)
        {
            return FALSE;
        }

        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;
    }

}
