<?php
require_once __DIR__ . '/AmazonAWS.class.inc';

/**
 * \file AmazonS3.php
 * \brief Class to upload and get file in a bucket S3
 *
 * \author Frederic Carretero <frederic.carretero@veremes.com>
 * \author Anthony Borghi <anthony.borghi@veremes.com>
 */

Class AmazonS3 extends AmazonAWS {
    const API_COMPONENT_SERVICE = 's3';
    
    /**
     * construct
     * @param {string} $sAwsAccessKeyId Access key ID.
     * @param {string} $sAwsSecretAccessKey Secret access key.
     * @param {string} $sAwsRegion Region.
     */
    function __construct ($sAwsAccessKeyId, $sAwsSecretAccessKey, $sAwsRegion) {
        // Paramètres obligatoires de la classe mère AmazonAWS.
        parent::__construct($sAwsAccessKeyId, $sAwsSecretAccessKey, $sAwsRegion);
        /*
        if($sAwsRegion == 'us-east-1')
            $this->sHost = trim('s3.amazonaws.com');
        else
            $this->sHost =  trim('s3-' . $sAwsRegion . '.amazonaws.com');
        */
        // Url racine de l'API.
        $this->sHost =  trim(self::API_COMPONENT_SERVICE . '-' . $sAwsRegion . '.amazonaws.com');
    }
    
    /**
     * Generating all the headers for a GET request. deprecated
     * @param {string} $sBucket A container for objects stored in Amazon S3.
     * @param {string} $sKey The unique identifier for an object within a bucket.
     * @return Headers for request.
     */
    function generateRequestHeaders($sBucket, $sKey, $sQueryParameter = '', $sRequestPayload = 'UNSIGNED-PAYLOAD') {
        // 
        $sServiceEndpoint = $sBucket . '.' . $this->sHost;
        $query_string = "";
        $sTimestamp = time();
        // Entêtes obligatoires.
        $sXAmzDate = gmdate('Ymd\THis\Z', $sTimestamp);
        $sDate = gmdate('Ymd', $sTimestamp);
        //$sRequestPayload = '$sRequestPayload ';
        
        // 1. Création d'une demande canonique pour Signature V4.
        $sHttpRequestMethod = 'GET';
        $sCanonicalUri = str_replace('%2F', '/', rawurlencode('/' . $sKey));
        $sCanonicalQueryString = $this->prepareQueryString($sQueryParameter);
        $sCanonicalHeaders = 'host:' . $sServiceEndpoint . chr(10) .
                            'x-amz-content-' . self::HASHING_ALGORITHM . ':' . $sRequestPayload . chr(10) .
                            'x-amz-date:' . $sXAmzDate . chr(10);
        $sSignedHeaders = 'host;x-amz-content-' . self::HASHING_ALGORITHM . ';x-amz-date';
        $sHashedPayload = $this->createHashedPayload($sRequestPayload);
        //$sHashedPayload = $sRequestPayload;
        $sCanonicalRequest = $sHttpRequestMethod . chr(10) .
                            $sCanonicalUri . chr(10) .
                            $sCanonicalQueryString . chr(10) .
                            $sCanonicalHeaders . chr(10) .
                            $sSignedHeaders . chr(10) .
                            $sHashedPayload;

        // 2. Création d'une chaîne à signer pour Signature V4.
        $sStringToSign = $this->createStringToSign($sDate, $sTimestamp, $sCanonicalRequest, self::API_COMPONENT_SERVICE);

        // 3. Calcul de la signature pour AWS Signature V4.
        $sSigningKey = $this->createSigningKey(self::API_COMPONENT_SERVICE, $sDate);
        $sSignature = $this->calculateSignature($sStringToSign, $sSigningKey);

        // 4. Création de l'entête "Authorization" pour la requête.
        $sCredentialScope = $this->createCredentialScope($sDate, self::API_COMPONENT_SERVICE);
        $sAuthorizationHeader = $this->generateAuthorizationHeader($this->sHashingAlgorithmString, $sCredentialScope, $sSignedHeaders, $sSignature);

        // Entêtes pour la requete.
        $aHeaders = array(
            'host: ' . $sServiceEndpoint,
            'x-amz-content-' . self::HASHING_ALGORITHM . ': ' . $sHashedPayload,
            'x-amz-date: ' . $sXAmzDate,
            'Authorization: ' . $sAuthorizationHeader
        );

        return $aHeaders;
    }
    
    function generateS3requestHeaders($sMethod, $sUrl, $sBucket, $aHeaders = array(), $sRequestPayload = 'UNSIGNED-PAYLOAD'){
        // build bucket hostname
        $sBucketHost = $sBucket . "." . $this->sHost;

        $sUrlParameter = "/";

        if(strpos($sUrl, "/") === 1){
            $sUrlParameter = "";
        }

        // build sFullUrl
        if(strpos($sUrl, $sBucketHost) === -1){
            $sUrl = 'https://' . $sBucketHost . $sUrlParameter . $sUrl;
        }

        $sFullUrl = $sMethod . ' ' . $sUrl . ' HTTP/1.1';
        return $this->generateAwsSignedHeaders($sFullUrl, $sBucketHost, self::API_COMPONENT_SERVICE, $aHeaders, $sRequestPayload, true, 'Now', true);
    }

    /**
     * Generating all the data for a POST request.
     * @param {string} $sBucket A container for objects stored in Amazon S3.
     * @param {string} $sKey The unique identifier for an object within a bucket.
     * @return Headers for request.
     */
    function generatePostData($sBucket, $sKey, $sAcl = 'public-read') {
        $sTimestamp = time();
        $sXAmzDate = gmdate('Ymd\THis\Z', $sTimestamp);
        $sDate = gmdate('Ymd', $sTimestamp);
        //$sAcl = 'public-read';
        $sCredential = $this->sAwsAccessKeyId . '/' . $sDate . '/' . $this->sAwsRegion . '/' . self::API_COMPONENT_SERVICE . '/aws4_request';
        // 1. Création d'une chaîne à signer pour Signature V4
        $aPolicy = array(
            'expiration' => gmdate('Y-m-d\TH:i:s.000\Z', $sTimestamp + 86400),
            'conditions' => array(
                array('acl' => $sAcl),
                array('bucket' => $sBucket),
                array('starts-with', '$Content-Type', ''),
                array('starts-with', '$key', ''),
                array('x-amz-algorithm' => $this->sHashingAlgorithmString),
                array('x-amz-credential' => $sCredential),
                array('x-amz-date' => $sXAmzDate)
            )
        );
        $sStringToSign = base64_encode(json_encode($aPolicy));

        // 2. Calcul de la signature pour AWS Signature V4.
        $sSigningKey = hash_hmac(self::HASHING_ALGORITHM, 'aws4_request', hash_hmac(self::HASHING_ALGORITHM,  self::API_COMPONENT_SERVICE, hash_hmac(self::HASHING_ALGORITHM, $this->sAwsRegion, hash_hmac(self::HASHING_ALGORITHM, $sDate, 'AWS4' . $this->sAwsSecretAccessKey, true), true), true), true);
        $sSignature = hash_hmac(self::HASHING_ALGORITHM, $sStringToSign, $sSigningKey);

        // 3. Données (post).
        $aPostData = array(
            'key' => $sKey,
            'acl' => $sAcl,
            'Content-Type' => '',
            'X-Amz-Credential' => $sCredential,
            'X-Amz-Algorithm' => $this->sHashingAlgorithmString,
            'X-Amz-Date' => $sXAmzDate,
            'Policy' => $sStringToSign,
            'X-Amz-Signature' => $sSignature
        );
        return $aPostData;
    }
    
    /**
     * Downloads a file.
     * @param {string} $sBucket Name of a container for objects.
     * @param {string} $sKey Name of a unique identifier for an object within a bucket.
     * @param {string} $sFilePath Path of the file to save.
     * @return File content
     */
    function downloadFile($sBucket, $sKey, $sFilePath = null) {
        // Url vers le fichier.
        $sUrl = 'https://' . $sBucket . '.' . $this->sHost . '/' . $sKey;
        $sMethod = 'GET';
        // Génération des entêtes pour la requete.
        $aHeaders = $this->generateS3requestHeaders($sMethod, $sUrl, $sBucket);

        $aHeaders[] = 'Accept: application/octet-stream';
        // Transfert cURL.
        $sRequestResult = $this->curlRequest($sUrl, $sMethod, null, $aHeaders);
        //
        if ($this->aLastCurlRequestInfo['http_code'] != 200) {
            $this->writeToErrorLog($this->formatS3ErrorMessage($sRequestResult));
            return false;
        }
        else {
            if (!empty($sFilePath))
                file_put_contents($sFilePath, $sRequestResult);
            else
                return $sRequestResult;
        }
    }
    
    /**
     * Upload a file.
     * @param {string} $sBucket Name of a container for objects.
     * @param {string} $sKey Name of a unique identifier for an object within a bucket.
     * @param {string} $sFilePath Path of the file to save.
     * @return File content
     */
    function uploadFile($sBucket, $sKey, $sFilePath) {
        if (file_exists($sFilePath)) {
            $sFileName = pathinfo($sFilePath, PATHINFO_BASENAME);
            // Url vers le fichier.
            $sUrl = 'https://' . $sBucket . '.' . $this->sHost;
            // Génération des entêtes pour la requete.
            $aPostData = $this->generatePostData($sBucket, $sKey);
            $aPostData['Content-Type'] = mime_content_type($sFilePath);
            $aPostData['file'] = new CurlFile(realpath($sFilePath), $aPostData['Content-Type'], $sFileName);
            $aHeaders = array(
                'Content-Type: multipart/form-data',
            );
            $sRequestResult = $this->curlRequest($sUrl, 'post', $aPostData, $aHeaders, true);
            if ($this->aLastCurlRequestInfo['http_code'] != 204) {
                $this->writeToErrorLog($this->formatS3ErrorMessage($sRequestResult));
                return false;
            }
        }
        else
            return false;
    }

    /**
     * ScanDir in a bucket
     * @param {string} $sBucket Name of a container for objects.
     * @param {string} $sPrefix Path of the folder to scan
     * @param {boolean} $bWithFileInfos boolean to add fileInfo with filename
     * @return Array  associative array describing directory scanned
     */
    function scanDir($sBucket, $sPrefix, $bWithFileInfos = false){
        $oFiles = false;
        $sQueryString = '?list-type=2&prefix=' . $sPrefix;
        //URL to send request
        $sUrl = 'https://' . $sBucket . '.' . $this->sHost . "/" . $sQueryString;
        $sMethod = 'GET';

        // Génération des entêtes pour la requete.
        $aHeaders = $this->generateS3requestHeaders($sMethod, $sUrl, $sBucket, array('Content-Type: application/x-www-form-urlencoded'), '');
        // Transfert cURL.
        $sRequestResult = $this->curlRequest($sUrl, $sMethod, null, $aHeaders);
        if ($this->aLastCurlRequestInfo['http_code'] != 200) {
            $this->writeToErrorLog("ERROR: ScanDir can't scan this prefix or this bucket");
            $this->writeToErrorLog($this->formatS3ErrorMessage($sRequestResult));
            return false;
        }
        else {
            $oXMLContent= json_decode(json_encode(simplexml_load_string($sRequestResult)), true);
            $oFiles = $this->awsObjectListXmlToTree($oXMLContent, $bWithFileInfos);
        }

        return $oFiles;
    }

    /**
     * ScanDir in a bucket
     * @param {object} $oXMLObject Result of the list object request AWS
     * @param {boolean} $bWithFileInfos boolean to add fileInfo with filename
     * @return Array  associative array describing directory scanned
     */
    function awsObjectListXmlToTree ($oXMLObject, $bWithFileInfos){

        $aSave = array();

        $sPrefix = (count($oXMLObject["Prefix"]) === 0) ? "" : $oXMLObject["Prefix"];
        
        if(substr($sPrefix, -1) !== "/"){
            $sPrefix .= "/";
        }

        $oFiles = array(
            "bucket" => $oXMLObject["Name"], 
            "sPath"=> $sPrefix, 
            "aContent" => array()
        );

        if($oXMLObject["KeyCount"] == 1){
            $sFinalKey = $oXMLObject["Contents"]["Key"];
            if($sPrefix !== "/"){
                $sFinalKey = str_replace($sPrefix, "", $sFinalKey);
            }
            $aPath = explode("/", $sFinalKey);
            $sObjectName = $aPath[0];

            $oFile = $sObjectName;

            if($bWithFileInfos){
                    $oFile = array(
                        "name"=> $sObjectName,
                        "isDirectory"=> (count($aPath) > 1)? 1 : 0,
                        "size" => $oXMLObject["Contents"]["Size"],
                        "lastModification" => $oXMLObject["Contents"]["LastModified"],
                        "storageType" => $oXMLObject["Contents"]["StorageClass"]
                    );
                }

                array_push($oFiles["aContent"], $oFile);
                array_push($aSave, $sObjectName);
        }else{
            for($i = 0; $i < count($oXMLObject["Contents"]); $i++){
                $sFinalKey = $oXMLObject["Contents"][$i]["Key"];
                if($sPrefix !== "/"){
                    $sFinalKey = str_replace($sPrefix, "", $sFinalKey);
                }
                $aPath = explode("/", $sFinalKey);
                $sObjectName = $aPath[0];

                $oFile = $sObjectName;
                // check si existe déja
                if(!in_array($sObjectName, $aSave) && $sObjectName !== ""){
                    if($bWithFileInfos){
                        $oFile = array(
                            "name"=> $sObjectName,
                            "isDirectory"=> (count($aPath) > 1)? 1 : 0,
                            "size" => $oXMLObject["Contents"][$i]["Size"],
                            "lastModification" => $oXMLObject["Contents"][$i]["LastModified"],
                            "storageType" => $oXMLObject["Contents"][$i]["StorageClass"]
                        );
                    }
                    
                    array_push($oFiles["aContent"], $oFile);
                    array_push($aSave, $sObjectName);
                } 
            }
        }
        return $oFiles;
    }

    /**
     * Delete a file.
     * @param {string} $sBucket Name of the container for remove an objects.
     * @param {string} $sKey Object's identifier to delete it.
     * @return boolean true if removed false else
     */
    function deleteFile($sBucket, $sKey){
        $bReturn = false;
        //URL to send request
        $sUrl = 'https://' . $sBucket . '.' . $this->sHost . "/" . $sKey;
        $sMethod = 'DELETE';

        // Génération des entêtes pour la requete.
        $aHeaders = $this->generateS3requestHeaders($sMethod, $sUrl, $sBucket, array(''), '');
  
        // Transfert cURL.
        $sRequestResult = $this->curlRequest($sUrl, $sMethod, null, $aHeaders);
        
        if ($this->aLastCurlRequestInfo['http_code'] != 200) {
            $this->writeToErrorLog("ERROR: it's impossible to delete this key in this bucket");
            $this->writeToErrorLog($this->formatS3ErrorMessage($sRequestResult));
        } else
            $bReturn = true;

        return $bReturn;
    }
    
    /**
     * Convert an error message returned by s3 (xml to object).
     * @param {string} $sXmlErrorMessage Error message returned by s3 (xml format).
     * @return object S3 Error 
     */
    function getS3ErrorObject($sXmlErrorMessage) {
        $oSimpleXmlElement = simplexml_load_string($sXmlErrorMessage);
        if (is_object($oSimpleXmlElement)) {
            $oErrorMessage = new stdClass();
            foreach ($oSimpleXmlElement as $sProperty => $oValue) {
                $oErrorMessage->$sProperty = $oValue->__toString();
            }
            return $oErrorMessage;
        }
        else
            return false;
    }
    
    /**
     * Format an error message returned by s3.
     * @param {string} $sXmlErrorMessage Error message returned by s3 (xml format).
     * @return string Error message
     */
    function formatS3ErrorMessage($sXmlErrorMessage) {
        $oErrorMessage = $this->getS3ErrorObject($sXmlErrorMessage);
        if ($oErrorMessage !== false)
            return ($oErrorMessage->Code . ' : ' . $oErrorMessage->Message);
        else
            return $sXmlErrorMessage;
    }
}
?>