<?php
/**
 * This class is in charge of reading and saving cookies from a file.
 * @package Helium10
 */
class CookiesManager
{
    private $cookiesFilePath;

    /**
     * CookiesManager constructor.
     * @param string $storageFilePath a file in which cookies will be read from and written in.
     */
    public function __construct($storageFilePath)
    {
        $this->cookiesFilePath = $storageFilePath;
    }


    /**
     * Returns an array in which each key represents a cookie name that contains another array in which there is its value and attributes. If no cookie was found
     * it returns an empty array.
     * #### Note:
     * each entry is an array with the following keys:
     * * *value* a string representing the cookie value. This key is mandatory since a cookie always has a value.
     * * *expires* a string representing the value of the expires attribute. This key is optional.
     * * *max-age* a string representing the value of the Max-Age attribute. This key is optional.
     * * *domain* a string representing the value of the domain attribute. This key is optional.
     * * *path* a string representing the value of the path attribute. This key is optional.
     * * *samesite* a string representing the value of the SameSite attribute. This key is optional.
     * #### Example:
     * ``
     * $rawCookies = ["login_session=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/;"];
     * $manager = new CookiesManager("A_FILE_PATH");
     * $extractedCookies = $manager->extract($rawCookies);
     * // The return array is ["login_session" => ["value" => "deleted", "expires" => "Thu, 01-Jan-1970 00:00:01 GMT", "max-age" => "0", "path" => "/"] ]
     * ``
     * @param array $rawCookies an array containing each cookie and its attributes in a string.
     * @return array
     */
    public function extract($rawCookies) {
        $allCookies = [];
        $counter = 1;

        if (count($rawCookies) == 0){
            return [];
        }

        $size = count($rawCookies);
        for ($i = 0; $i < $size; $i++) {
            $currentCookie = $rawCookies[$i];
            if (strlen($currentCookie) > 0) {
                $cookieDetails = [];
                $infos = self::extractNameAndValue($currentCookie);
                //Check that name and value were found
                if (is_bool($infos)) {
                    continue;
                }
                $cookieDetails['value'] = $infos['value'];

                if (self::hasMultipleAttributes($currentCookie)) {
                    $attributesList = ['expires','max-age','domain','path','samesite'];
                    foreach ($attributesList as $key => $attribName) {
                        $extractedValue = self::extractAttributeValue($currentCookie, $attribName);
                        if (is_string($extractedValue)) {
                            $cookieDetails[$attribName] = $extractedValue;
                        }
                    }
                }


                $allCookies[$infos['name']] = $cookieDetails;
                $counter++;
            }
        }

        return $allCookies;
    }

    /**
     * Returns true in case a serialized array of cookies in the file ,which path was provided in the constructor, were updated successfully;
     * otherwise it returns false.
     * #### Note:
     * It's important to note that in case a cookie exists in the file its value and attributes are updated; if it does not exists it's added to file file.
     * @param array $rawCookies an array containing each cookie and its attributes in a string.
     * @return bool
     */
    public function update($rawCookies)
    {

        $newCookies = $this->extract($rawCookies);
        if (count($newCookies) == 0)
            return false;

        $oldCookies = @unserialize(@file_get_contents($this->cookiesFilePath));

        if (! is_array($oldCookies)) {
            return boolval(@file_put_contents($this->cookiesFilePath, serialize($newCookies)));
        }

        foreach ($newCookies as $cookieName => $cookieDetails) {
            $oldCookies[$cookieName]['value'] = $newCookies[$cookieName]['value'];
            $attributesList = ['expires','max-age','domain','path','samesite'];
            foreach ($attributesList as $key => $attribName) {
                if (isset($newCookies[$cookieName][$attribName])) {
                    $oldCookies[$cookieName][$attribName] = $newCookies[$cookieName][$attribName];
                }
            }
        }

        return boolval(@file_put_contents($this->cookiesFilePath, serialize($oldCookies)));
    }

    /**
     * Returns a list of cookies, saved in the file which path was provided in the constructor, ready to be sent to a web server; otherwise it
     * returns an empty string.
     * @return string
     */
    public function get()
    {
        $existingCookies = @unserialize(@file_get_contents($this->cookiesFilePath));
        if (! is_array($existingCookies))
            return "";

        $returnedValue = "";

        $counter = 1;
        $max = count($existingCookies);
        foreach ($existingCookies as $cookieName => $cookieDetails) {
            if ($counter == $max) {
                $returnedValue .= $cookieName . "=" . $existingCookies[$cookieName]["value"] . ";";
            } else {
                $returnedValue .= $cookieName . "=" . $existingCookies[$cookieName]["value"] . "; ";
            }
            $counter++;
        }

        return $returnedValue;
    }

    /**
     * Returns a list of cookies, saved in the file which path was provided in the constructor, ready to be sent to a web server; otherwise it
     * returns an empty string.
     * @return string
     */
    public function getObj($domain = "")
    {

        $existingCookies = @json_decode(@file_get_contents(str_replace(".file",".json",$this->cookiesFilePath)));
        if (! is_array($existingCookies))
            return "";
        $obj = (object)[];
        for ($i = 0; $i < count($existingCookies); $i++) {
          if ($domain && $domain != "") {
            if (isset($existingCookies[$i]->domain) && strstr($existingCookies[$i]->domain, $domain)) {
              $obj->{$existingCookies[$i]->name} = $existingCookies[$i];
            }
          } else {
            $obj->{$existingCookies[$i]->name} = $existingCookies[$i];
          }
        }
        return $obj;
    }
    public function getSetCookie()
    {
        $existingCookies = @unserialize(@file_get_contents($this->cookiesFilePath));
        if (! is_array($existingCookies))
            return "";

        $returnedValues = [];
        //login_session=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/;
        $rels = [
          "max-age" => "Max-Age",
          "samesite" => "sameSite"
        ];
        foreach ($existingCookies as $cookieName => $cookieDetails) {
          if (isset($cookieDetails['expires'])) {
            $thisCookie = $cookieName."=".$cookieDetails['value'];
            foreach ($cookieDetails as $detailName => $detailValue) {
              if ($detailName != "value")
                $thisCookie .= "; ".(isset($rels[$detailName]) ? $rels[$detailName] : $detailName)."=".$detailValue;
            }
            $returnedValues[] = $thisCookie;
          }
        }
        return $returnedValues;
    }

    /**
     * Returns true in case the file (containing the cookies) was emptied; otherwise it returns false.
     * @return bool
     */
    public function empty()
    {
        return boolval(@file_put_contents($this->cookiesFilePath, ""));
    }

    /**
     * Returns true in case a cookie contains many attributes; otherwise it returns false.
     * @param string $rawCookie a string representing the details of a given cookie.
     * @return bool
     */
    public static function hasMultipleAttributes($rawCookie)
    {
        preg_match_all('/;/', $rawCookie, $matches);
        return count($matches[0]) > 1;
    }

    /**
     * In case of success it returns an array containing the value and name of a cookie; otherwise it returns false.
     * ### Note:
     * the returned array contains the following keys: *name* and *value*
     * @param string $rawCookie a string representing the details of a given cookie.
     * @return array|bool
     */
    public static function extractNameAndValue($rawCookie)
    {
        $currentCookie = preg_replace("/;\s.+/i", "", $rawCookie);
        $currentCookie = preg_replace("/;/i", "", $currentCookie);
        $exploded = explode('=', $currentCookie);
        //if (count($exploded) != 2)
        //    return false;

        //Check that the name is not an attribute
        $check = preg_match("/^Expires$|^Max-Age$|^Domain$|^Path$|^Secure$|^HttpOnly$|^SameSite$/i", $exploded[0]);
        if ($check)
            return false;

        return [
            'name' => $exploded[0],
            'value' => $exploded[1]
        ];
    }

    /**
     * Returns a string representing the value of a given cookie attribute,in case of success; otherwise it returns false.
     * @param string $rawCookie a string representing the details of a given cookie.
     * @param string $attributeName a string representing the attribute which value must be extracted.
     * Only the following attributes are supported *expires*, *max-age*, *domain*, *path*, and *SameSite*
     * @return string|bool
     */
    public static function extractAttributeValue($rawCookie, $attributeName)
    {
        $check = boolval(preg_match("/^Expires$|^Max-Age$|^Domain$|^Path$|^SameSite$/i", $attributeName));

        if (! $check)
            return false;

        preg_match("/" . $attributeName ."=.+;*/i", $rawCookie, $matches);

        if (count($matches) == 0)
            return false;

        $exploded = explode('=', $matches[0]);

        return preg_replace("/;.*/", "", $exploded[1]);
    }
}
