<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/**
 * Partuza's implementation of OAuthLookupService, which does all the heavy lifting for the oauth protocol handling
 * and authentication matching
 */
class PartuzaOAuthLookupService extends OAuthLookupService {

  /**
   * Partuza's implementation of the OAuth Lookup service. Partuza supports all currently existing forms of
   * OAuth signatures: 3 legged, 2 legged and body_hash's
   *
   * @param RequestItem $oauthRequest
   * @param string $appUrl
   * @param string $userId
   * @return SecurityToken or null
   */
  public function getSecurityToken($oauthRequest, $appUrl, $userId, $contentType) {
    try {
      // Incomming requests with a POST body can either have an oauth_body_hash, or include the post body in the main oauth_signature; Also for either of these to be valid
      // we need to make sure it has a proper the content-type; So the below checks if it's a post, if so if the content-type is supported, and if so deals with the 2
      // post body signature styles
      $includeRawPost = false;
      $acceptedContentTypes = array('application/atom+xml', 'application/xml', 'application/json');
      if (isset($GLOBALS['HTTP_RAW_POST_DATA']) && ! empty($GLOBALS['HTTP_RAW_POST_DATA'])) {
        if (! in_array($contentType, $acceptedContentTypes)) {
          // This is rather double (since the ApiServlet does the same check), but for us to do a meaninful processing of a post body, this has to be correct
          throw new Exception("Invalid Content-Type specified for this request, only 'application/atom+xml', 'application/xml' and 'application/json' are accepted");
        } else {
          if (isset($_GET['oauth_body_hash'])) {
            // this request uses the oauth_body_hash spec extension. Check the body hash and if it fails return 'null' (oauth signature failure)
            // otherwise continue on to the regular oauth signature verification, without including the post body in the main oauth_signature calculation
            if (! $this->verifyBodyHash($GLOBALS['HTTP_RAW_POST_DATA'], $_GET['oauth_body_hash'])) {
              return null;
            }
          } else {
            // use the (somewhat oauth spec invalid) raw post body in the main oauth hash calculation
            $includeRawPost = $GLOBALS['HTTP_RAW_POST_DATA'];
          }
        }
      }
      $dataStore = new PartuzaOAuthDataStore();
      if ($includeRawPost) {
        // if $includeRawPost has been set above, we need to include the post body in the main oauth_signature
        $oauthRequest->set_parameter($includeRawPost, '');
      }
      if (! isset($oauthRequest->parameters['oauth_token'])) {
        // No oauth_token means this is a 2 legged OAuth request
        $ret = $this->verify2LeggedOAuth($oauthRequest, $userId, $appUrl, $dataStore);
      } else {
        // Otherwise it's a clasic 3 legged oauth request
        $ret = $this->verify3LeggedOAuth($oauthRequest, $userId, $appUrl, $dataStore);
      }
      if ($includeRawPost) {
        unset($oauthRequest->parameters[$includeRawPost]);
      }
      return $ret;
    } catch (OAuthException $e) {
      return null;
    }
  }

  /**
   * Verfies the oauth_body_hash signature, for more information on this oauth spec extension see:
   * http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html
   */
  private function verifyBodyHash($postBody, $oauthBodyHash) {
    return base64_encode(sha1($postBody, true)) == $oauthBodyHash;
  }

  /**
   * Verfies a 2 legged OAuth signature. 2 legged OAuth means the security context is of the application,
   * and no specific user is associated with it. Most of the logic is done manually and not through the OAuth
   * library, since it has no knowledge of- / support for 2 legged OAuth.
   */
  private function verify2LeggedOAuth($oauthRequest, $userId, $appUrl, $dataStore) {
    $consumerToken = $dataStore->lookup_consumer($oauthRequest->parameters['oauth_consumer_key']);
    $signature_method = new OAuthSignatureMethod_HMAC_SHA1();
    $signature_valid = $signature_method->check_signature($oauthRequest, $consumerToken, null, $_GET["oauth_signature"]);
    if (! $signature_valid) {
      // signature did not check out, abort
      return null;
    }
    return new OAuthSecurityToken($userId, $appUrl, $dataStore->get_app_id($consumerToken), "partuza");
  }

  /**
   * The 'clasic' 3 legged OAuth, where the user went through the OAuth dance and granted the remote app
   * access to his/her data.
   */
  private function verify3LeggedOAuth($oauthRequest, $userId, $appUrl, $dataStore) {
    $server = new OAuthServer($dataStore);
    $server->add_signature_method(new OAuthSignatureMethod_HMAC_SHA1());
    $server->add_signature_method(new OAuthSignatureMethod_PLAINTEXT());
    list($consumer, $token) = $server->verify_request($oauthRequest);
    $oauthUserId = $dataStore->get_user_id($token);
    if ($userId && $oauthUserId && $oauthUserId != $userId) {
      return null; // xoauth_requestor_id was provided, but does not match oauth token -> fail
    } else {
      $userId = $oauthUserId; // use userId from oauth token
      return new OAuthSecurityToken($userId, $appUrl, 0, "partuza");
    }
  }
}
