Page 1 of 1

PureBasic and REST APIs - An HTTPRequest() Example

Posted: Mon Sep 09, 2024 9:37 am
by TI-994A
I had posted a REST API example earlier, which demonstrated the use of conventional PureBasic networking functions, from OpenNetworkConnection() and NetworkClientEvent(), to SendNetworkString() and ReceiveNetworkData().

At the suggestion of our developer-in-chief, Monsieur Laboureur, I had adapted the same example to utilise the newer HTTPRequest() network function, which is a fully-integrated networking method that handles all the conventional networking operations in a single call, including:
1. opening, configuring, and negotiating HTTP & HTTPS connections
2. formatting and sending queries and headers
3. configuring, receiving, and formatting the responses


I am re-posting it here, in a separate thread, under a more descriptive title, for the benefit of yielding better search results for this function:

Code: Select all

;==========================================================
;   
;   pbHTTP is a modification of pbPost, which implements
;   the PureBasic HTTPRequest() function added in v5.70
;   instead of the older networking functions in consuming
;   online REST APIs - it also supports the HTTPS protocol
;
;   this is the forum thread for the orignal pbPost code:
;   www.purebasic.fr/english/viewtopic.php?p=625568#p625568
;      
;   tested & working on:
;   1. Windows 8.1 w/ PureBasic 5.73 LTS (x64)
;   2. Windows 10 w/ PureBasic 6.11 LTS (x64)
;   3. macOS High Sierra w/ PureBasic 5.70 LTS (x64)
;   4. macOS Catalina w/ PureBasic 5.73 LTS (x64)
;   5. macOS Sonoma w/ PureBasic 6.11 LTS (arm64)
;   6. Ubuntu Linux 18.04.2 w/ PureBasic 6.11 LTS (x64)
;
;   by TI-994A - free to use, improve, share...
;
;   9th August 2024 - Singapore
;
;==========================================================

EnableExplicit

; the InitNetwork() call is required for
; earlier versions of PureBasic (below v6.0)
CompilerIf #PB_Compiler_Version < 600
  InitNetwork()
CompilerEndIf

; the http protocol transfers data in plain text
; the https protocol transfers data in encrypted format
#http = "http://"
#https = "https://"

; http servers typically respond with status codes
; prior to sending the actual requested datasets.
; more information on these codes can be found here:
; https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
#httpSuccess = 200

; these are the URLs to the test server and API script
#host = "server.syed.sg"
#apiURL = "/pbPost.php"

Enumeration
  #window
  #frame
  #editor
  #buttonPost
  #buttonRetrieve
  #textNewTitle
  #textOldTitle
  #textName
  #textCountry
  #stringName
  #stringCountry
EndEnumeration

Define event, appQuit

; HTTP calls must be preceded by standard headers to
; notify the server of the required transmission protocols.

; more information on the specifications can be found here: 
; https://en.wikipedia.org/wiki/List_of_HTTP_header_fields.

; these are the minimum required headers notifying the server
; of the API script (pbPost.php), the request method (POST),
; the host (server.syed.sg), the client (this app), the type
; of connection (close - meaning close after transmission), and
; the format of the content being transmitted (application/x...).

NewMap headers.s()
headers("Host") = #host
headers("Connection") = "close"
headers("User-Agent") = "PureBasic Network Client"
headers("Content-Type") = "application/x-www-form-urlencoded"

; this is the main function which processes
; the requests and responses to/from the server.
Procedure post(query.s)    
  Shared headers()
  Define.s queryLen, serverData, apiURL, httpRequest.i
  queryLen = Str(StringByteLength(query, #PB_UTF8))
  headers("Content-Length") = queryLen  
  apiUrl = #https + #host + #apiURL
  
  ; HTTPRequest() is an integrated function that performs all the networking
  ; actions, from opening, configuring, and negotiating HTTP/HTTPS connections,
  ; formatting and sending queries and headers, to configuring, receiving, and
  ; formatting the responses which could then be read by the HTTPInfo() function.    
  httpRequest = HTTPRequest(#PB_HTTP_Post, apiURL, query, #Null, headers())
  
  If httpRequest
    If Val(HTTPInfo(httpRequest, #PB_HTTP_StatusCode)) = #httpSuccess
      serverData = HTTPInfo(httpRequest, #PB_HTTP_Response)    
      
      ; an end-of-header delimiter was added by the API to
      ; conveniently display the response without the headers.     
      serverData = StringField(serverData, 2, "end_of_header")
      
      SetGadgetText(#editor, serverData)    
      SetGadgetText(#stringName, "")
      SetGadgetText(#stringCountry, "")      
    EndIf      
  EndIf  
  
  If Trim(serverData) = ""
    SetGadgetText(#editor, "Network connection error!") 
    Debug HTTPInfo(httpRequest, #PB_HTTP_Response)    
  EndIf
  
  FinishHTTP(httpRequest)  
EndProcedure

; this function requests the API to UPDATE (action)
; the given NAME and COUNTRY values into the server
Procedure postEntry()
  Define.s name, country, query
  name = GetGadgetText(#stringName)  
  country = GetGadgetText(#stringCountry)
  query = URLEncoder("action=update&" + "name=" + name + "&country=" + country)
  post(query)
EndProcedure

; this function requests the API to RETRIEVE (action) and
; return the existing server data - the additional parameter
; values are not relevant for this action and will be ignored
Procedure retrieveEntries()
  Define.s query = URLEncoder("action=retrieve&name=&country=")
  post(query)
EndProcedure

OpenWindow(#window, #PB_Ignore, #PB_Ignore, 640, 480, "REST API w/HTTPRequest", 
           #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
FrameGadget(#frame, 20, 40, 480, 110, "")
EditorGadget(#editor, 20, 210, 600, 210)
ButtonGadget(#buttonPost, 510, 40, 110, 110, "POST")
ButtonGadget(#buttonRetrieve, 490, 430, 130, 30, "RETRIEVE")
TextGadget(#textNewTitle, 20, 10, 170, 25, "Participant's Information:")
TextGadget(#textOldTitle, 20, 180, 150, 25, "Previous Participants:")
TextGadget(#textName, 35, 60, 70, 25, "Name:")
TextGadget(#textCountry, 35, 100, 70, 25, "Country:")
StringGadget(#stringName, 110, 60, 350, 25, "")
StringGadget(#stringCountry, 110, 100, 350, 25, "")

Repeat
  event = WaitWindowEvent()
  Select event     
    Case #PB_Event_CloseWindow
      appQuit = #True    
    Case #PB_Event_Gadget
      Select EventGadget()
        Case #buttonPost
          postEntry()          
        Case #buttonRetrieve
          retrieveEntries()
      EndSelect    
  EndSelect
Until appQuit

The PHP script for the API remains mostly unchanged:

Code: Select all

<?php
    // =============================================================
    //
    //    A simple API written in PHP to process 2 request types: 
    //    1. receive and save POSTed data to the server
    //    2. read and return the data from the server
    //
    //    This API will work with any client that complies with
    //    the query format and is able to process the responses.
    //
    //    by TI-994A - free to use, improve, share...
    //
    //    20th July 2024 - Singapore
    //
    // =============================================================

    // PHP stores query values in system-designated variables:
    // POST data is stored in a variable array named $_POST[]
    // GET data is stored in a variable array named $_GET[]
    // similarly, the array variable $_SERVER[] stores system
    // data pertaining to the client, host, protocols, etc.

    // the PureBasic app POSTs [action=Action&name=nameValue&country=countryValue]
    // here the named POST query values are being extracted into local variables 
    $action = trim($_POST['action']);
    $name = trim($_POST['name']);
    $country = trim($_POST['country']);

    // if the requester is the PureBasic application - an end-of-header delimiter
    // will be added before the response data to facilitate the removal of headers.
    if (trim($_SERVER['HTTP_USER_AGENT']) == "PureBasic Network Client")
        echo "end_of_header";

    // the server will identify itself with its HTTP protocol        
    $protocol = (isset($_SERVER['HTTPS'])) ? "https" : "http";
    echo $protocol . "://" . $_SERVER['HTTP_HOST'] . " says: " . PHP_EOL;

    // server data is usually stored in some SQL database for speed & efficiency - 
    // however, for simplicity and clarity, a simple file is being used here.

    // if the action value is [retrieve] - read and return the server data
    if ($action == "retrieve") {

        $index = 0;
        $records = file("datafile.txt");
        if ($records) {
            $records = array_reverse($records);   // list newest entries first
            foreach ($records as $record) {
                $index++;
                $recordSet .= $index . ". " . $record;
                if ($index > 99)   // limit response to 100 newest records only
                    break;
            }
            echo "Here are the records of the previous participants:" . PHP_EOL;
            echo $recordSet;
        } else {
            echo "Server file error! Records not retrieved.";
        }

    // if the action value is [update] - store the data into the server     
    } else if ($action == "update" && $name != "" && $country != "") {

        echo "Hello, " . $name . ". ";
        $writeSuccess = true;
        
        $dataFile = fopen("datafile.txt", "a");
        if ($dataFile) {    

            // formats the record with the date and the requester's IP address
            $newData = $name . " from " . $country . " visited on " .
                date("d M Y") . " (" . $protocol . " > IP: " . 
                $_SERVER['REMOTE_ADDR'] . ")" . PHP_EOL;

            if (fwrite($dataFile, $newData)) {
                echo "Your records have been updated successfully!" . PHP_EOL;
                echo "Thank you for your participation. " . PHP_EOL;
                echo "Goodbye!" . PHP_EOL;
            } else {
                $writeSuccess = false;
            }
            fclose($dataFile);            
        } else {
            $writeSuccess = false;
        }
        if (!$writeSuccess) echo "Server file error! Records not updated.";        

    } else {        

        // a friendly message for stragglers
        echo "You have reached a test site." . PHP_EOL;

    }
?>