unit Sparkle.Mac.HttpEngine;

{$I Sparkle.Inc}

interface

{$IFDEF MACOS}

uses
  Generics.Collections, SysUtils, Classes,
  {$IFDEF IOS}
  iOSApi.Foundation,
  {$ELSE}
  MacApi.Foundation,
  {$ENDIF}
  Sparkle.Http.Engine,
  Sparkle.Http.Headers,
  Sparkle.Uri;

type
  EMacHttpNoDataException = class(EHttpEngineException)
  public
    constructor Create(NSErrorPtr: Pointer);
  end;

  EMacHttpNoResponseException = class(EHttpEngineException)
  public
    constructor Create(NSErrorPtr: Pointer);
  end;

  TMacHttpResponse = class;

  TMacHttpResponseStream = class(THttpResponseStream)
  strict private
    FResponse: TMacHttpResponse;
  strict protected
    function HttpRead(var Buffer; Count: Longint): Longint; override;
  public
    constructor Create(AResponse: TMacHttpResponse);
  end;

  TMacHttpResponse = class(THttpResponse)
  strict private
    FNData: NSData;
    FNResponse: NSHTTPURLResponse;
    FContentLength: Int64;
    FContentLengthLoaded: boolean;
  protected
    function GetContentLength: Int64; override;
    function CreateContentStream: TStream; override;
    function GetContentType: string; override;
    function GetContentEncoding: string; override;
    function GetStatusCode: integer; override;
    function GetStatusReason: string; override;
    procedure UpdateHeaders(AHeaders: THttpHeaders); override;
    property NData: NSData read FNData;
  public
    constructor Create(AResponse: NSHTTPURLResponse; AData: NSData);
    destructor Destroy; override;
  end;

  TMacHttpEngine = class(THttpEngine)
  strict protected
    function DoCreateRequest: THttpRequest; override;
    function DoSend(Request: THttpRequest): THttpResponse; override;
  public
    constructor Create; override;
    destructor Destroy; override;
  end;

  TTranslateNSErrorMessageProc = procedure(Error: NSError; var Msg: string);

var
  OnTranslateNSErrorMessage: TTranslateNSErrorMessageProc;

{$ENDIF}

implementation

{$IFDEF MACOS}

uses
  {$IFDEF iOS}
  iOSApi.CocoaTypes,
  {$ELSE}
  MacApi.CocoaTypes,
  {$ENDIF}
  MacApi.ObjectiveC,
  {$IFDEF DELPHIXE6_LVL}
  Macapi.Helpers,
  {$ENDIF}
  Sparkle.Utils;

function ToNSSTR(const Str: string): NSString;
begin
  {$IFDEF DELPHIXE6_LVL}
  Result := StrToNSStr(Str);
  {$ELSE}
  Result := NSStr(Str);
  {$ENDIF}
end;

type
  { Rebuild NSURLConnection class because the "returningResponse" parameter in
    sendSynchronousRequest method is not declared correctly in the Delphi library }

  NSURLConnectionClass = interface(NSObjectClass)
    ['{55FC6176-D0A8-4416-B11E-973162AD7E72}']
    {class} function canHandleRequest(request: NSURLRequest): Boolean; cdecl;
    {class} function connectionWithRequest(request: NSURLRequest; delegate: Pointer): Pointer; cdecl;
    {class} function sendSynchronousRequest(request: NSURLRequest; var returningResponse: Pointer; error: PPointer): NSData; cdecl;
  end;

  NSURLConnection = interface(NSObject)
    ['{306B2D7B-9B1D-4B5E-AA7D-6643FBE65494}']
    procedure cancel; cdecl;
    function initWithRequest(request: NSURLRequest; delegate: Pointer): Pointer; cdecl; overload;
    function initWithRequest(request: NSURLRequest; delegate: Pointer; startImmediately: Boolean): Pointer; cdecl; overload;
    procedure start; cdecl;
  end;

  TNSURLConnection = class(TOCGenericImport<NSURLConnectionClass, NSURLConnection>)  end;

function NSErrorMessage(Error: NSError): string;
begin
  if Error.localizedDescription <> nil then
    Result := Format('%s (%d)', [Error.localizedDescription.UTF8String, Error.code])
  else
    Result := Format('Error code: %d', [Error.code]);

  case Error.code of
    NSURLErrorBadURL:
      Result := Result + sLineBreak + 'Bad URL';
    NSURLErrorTimedOut:
      Result := Result + sLineBreak + 'Timeout';
    NSURLErrorUnsupportedURL:
      Result := Result + sLineBreak + 'Unsupported URL';
    NSURLErrorCannotFindHost:
      Result := Result + sLineBreak + 'Cannot find host';
    NSURLErrorCannotConnectToHost:
      Result := Result + sLineBreak + 'Cannot connect to host';
    NSURLErrorNetworkConnectionLost:
      Result := Result + sLineBreak + 'Network connection lost';
    NSURLErrorDNSLookupFailed:
      Result := Result + sLineBreak + 'DNS Lookup failed';
    NSURLErrorHTTPTooManyRedirects:
      Result := Result + sLineBreak + 'HTTP too many redirects';
    NSURLErrorResourceUnavailable:
      Result := Result + sLineBreak + 'Resource unavailable';
    NSURLErrorNotConnectedToInternet:
      Result := Result + sLineBreak + 'Not connected to internet';
    NSURLErrorRedirectToNonExistentLocation:
      Result := Result + sLineBreak + 'Redirect to non existent location';
    NSURLErrorBadServerResponse:
      Result := Result + sLineBreak + 'Bad server response';
    NSURLErrorUserCancelledAuthentication:
      Result := Result + sLineBreak + 'User cancelled authentication';
    NSURLErrorUserAuthenticationRequired:
      Result := Result + sLineBreak + 'User authentication required';
    NSURLErrorZeroByteResource:
      Result := Result + sLineBreak + 'Zero byte resource' ;
    NSURLErrorCannotDecodeRawData:
      Result := Result + sLineBreak + 'Cannot decode raw data';
    NSURLErrorCannotDecodeContentData:
      Result := Result + sLineBreak + 'Cannot decode content data';
    NSURLErrorCannotParseResponse:
      Result := Result + sLineBreak + 'Cannot parse response';
    NSURLErrorSecureConnectionFailed:
      Result := Result + sLineBreak + 'Secure connection failed';
    NSURLErrorServerCertificateHasBadDate:
      Result := Result + sLineBreak + 'Server certificate has bad date';
    NSURLErrorServerCertificateUntrusted:
      Result := Result + sLineBreak + 'Server certificate untrusted';
    NSURLErrorServerCertificateHasUnknownRoot:
      Result := Result + sLineBreak + 'Server certificate has unknown root';
    NSURLErrorServerCertificateNotYetValid:
      Result := Result + sLineBreak + 'Server certificate not yet valid';
    NSURLErrorClientCertificateRejected:
      Result := Result + sLineBreak + 'Client certificate rejected';
    NSURLErrorClientCertificateRequired:
      Result := Result + sLineBreak + 'Client certificate required';
    NSURLErrorCannotLoadFromNetwork:
      Result := Result + sLineBreak + 'Cannot load from network';
  end;

  if Error.localizedFailureReason <> nil then
    Result := Result + sLineBreak + 'Failure reason: ' + string(Error.localizedFailureReason.UTF8String);

  if Error.localizedRecoverySuggestion <> nil then
    Result := Result + sLineBreak + 'Recovery suggestion: ' + string(Error.localizedRecoverySuggestion.UTF8String);

  if Error.domain <> nil then
    Result := Result + sLineBreak + 'Domain: ' + string(Error.domain.UTF8String);

  if Assigned(OnTranslateNSErrorMessage) then
    OnTranslateNSErrorMessage(Error, Result);
end;

{ TMacHttpEngine }

constructor TMacHttpEngine.Create;
begin
end;

function TMacHttpEngine.DoCreateRequest: THttpRequest;
begin
  Result := THttpRequest.Create;
end;

destructor TMacHttpEngine.Destroy;
begin
  inherited;
end;

function TMacHttpEngine.DoSend(Request: THttpRequest): THttpResponse;
var
  NUrl: NSUrl;
  NRequest: NSMutableURLRequest;
  NRequestDataPointer: Pointer;
  NRequestData: NSData;
  NResponseData: NSData;
  NResponsePointer: Pointer;
  NErrorPointer: Pointer;
  HeaderInfo: THttpHeaderInfo;
begin
  NUrl := TNSURL.Wrap(TNSURL.OCClass.URLWithString(ToNSStr(Request.Uri)));

  // Create Request
  NRequest := TNSMutableURLRequest.Wrap(TNSMutableURLRequest.OCClass.requestWithURL(NUrl));
  if Request.Method <> '' then
    NRequest.setHTTPMethod(ToNSSTR(Request.Method));
  NRequest.setTimeoutInterval(Request.Timeout / 1000);

  // Build http body NSData
  if Request.ContentLength > 0 then
  begin
    NRequestDataPointer := TNSData.OCClass.dataWithBytesNoCopy(Request.ContentBuffer, Request.ContentLength, false);
    if NRequestDataPointer = nil then
      raise EHttpEngineException.Create('Could not create Http body internal data object.');
    NRequestData := TNSData.Wrap(NRequestDataPointer);
    NRequest.setHTTPBody(NRequestData);
  end;

  // Set headers
  for HeaderInfo in Request.Headers.AllHeaders do
    NRequest.addValue(ToNSStr(HeaderInfo.Value), ToNSStr(HeaderInfo.Name));

  // make Mac/iOS client compatible with other platforms:
  // if content-type is not set in the request, client shuold send it blank
  // if we don't do this, NSURLConnection will set it to the default value
  // which is x-www-form-urlencoded
  if not Request.Headers.Exists('content-type') then
    NRequest.addValue(ToNSStr(''), ToNSStr('content-type'));

  // Send request and receive response
  NResponsePointer := nil;
  NErrorPointer := nil;
  NResponseData := TNSURLConnection.OCClass.sendSynchronousRequest(NRequest, NResponsePointer, @NErrorPointer);

  // Check for error
  if NResponseData = nil then
    raise EMacHttpNoDataException.Create(NErrorPointer);
  if NResponsePointer = nil then
    raise EMacHttpNoResponseException.Create(NErrorPointer);

  // Create response
  Result := TMacHttpResponse.Create(TNSHTTPURLResponse.Wrap(NResponsePointer), NResponseData);
end;

{ TMacHttpResponse }

constructor TMacHttpResponse.Create(AResponse: NSHTTPURLResponse; AData: NSData);
begin
  inherited Create;
  FNResponse := AResponse;
  FNData := AData;
end;

destructor TMacHttpResponse.Destroy;
begin
  inherited;
end;

function TMacHttpResponse.CreateContentStream: TStream;
begin
  Result := TMacHttpResponseStream.Create(Self);
end;

function TMacHttpResponse.GetContentEncoding: string;
begin
  Result := Headers.Get('content-encoding');
end;

function TMacHttpResponse.GetContentLength: Int64;
begin
  if not FContentLengthLoaded then
  begin
    if Chunked then
      FContentLength := -1
    else
      FContentLength := FNResponse.expectedContentLength;
    FContentLengthLoaded := true;
  end;
  Result := FContentLength;
end;

function TMacHttpResponse.GetContentType: string;
begin
  Result := Headers.Get('content-type');
end;

function TMacHttpResponse.GetStatusCode: integer;
begin
  Result := FNResponse.statusCode;
end;

function TMacHttpResponse.GetStatusReason: string;
begin
  Result := TSparkleUtils.GetStatusReason(StatusCode);
end;

procedure TMacHttpResponse.UpdateHeaders(AHeaders: THttpHeaders);
var
  Keys: NSArray;
  Key: NSString;
  Value: NSString;
  I: integer;
  HeaderName, HeaderValue: string;
begin
  AHeaders.Clear;
  Keys := FNResponse.allHeaderFields.allKeys;
  for I := 0 to Keys.count - 1 do
  begin
    Key := TNSString.Wrap(Keys.objectAtIndex(I));
    Value := TNSString.Wrap(FNResponse.allHeaderFields.objectForKey(Keys.objectAtIndex(I)));
    HeaderName := string(Key.UTF8String);
    HeaderValue := string(Value.UTF8String);
    AHeaders.AddValue(HeaderName, HeaderValue);
  end;
end;

{ TWinHttpResponseStream }

constructor TMacHttpResponseStream.Create(AResponse: TMacHttpResponse);
begin
  inherited Create;
  FResponse := AResponse;
end;

function TMacHttpResponseStream.HttpRead(var Buffer; Count: Longint): Longint;
var
  Range: NSRange;
begin
  // define location
  Range.location := Self.ContentSize;
  if Range.location > FResponse.NData.length then
    Exit(0);

  // define length
  Range.length := Count;
  if (Range.location + Range.length) > FResponse.NData.length then
    Range.length := FResponse.NData.length - Range.location;
  if Range.length <= 0 then
    Exit(0);

  // read data
  FResponse.NData.getBytes(@Buffer, Range);
  Result := Range.length;
end;

{ EMacHttpNoDataException }

constructor EMacHttpNoDataException.Create(NSErrorPtr: Pointer);
begin
  if NSErrorPtr = nil then
    inherited Create('Response data not received')
  else
    inherited Create(NSErrorMessage(TNSError.Wrap(NSErrorPtr)));
end;

{ EMacHttpNoResponseException }

constructor EMacHttpNoResponseException.Create(NSErrorPtr: Pointer);
begin
  if NSErrorPtr = nil then
    inherited Create('Response object not received')
  else
    inherited Create(NSErrorMessage(TNSError.Wrap(NSErrorPtr)));
end;

initialization
  OnTranslateNSErrorMessage := nil;
{$ENDIF}

end.
