Saturday, November 28, 2015

WooCommerce API v3 VB.NET / C# library

Here is a VB.NET port of existing C# library for accessing WordPress WooCommerce API (https://gist.github.com/jakobt/b7400463e90e16928f57).
From my testing experience, the C# library worked very well for API versions v1 and v2. However, in it's original form, it was unable to generate proper OAuth signature for WooCommerce API v3.

Also, I have decided to port it to VB.NET as I'm not aware of currently available VB.NET versions of  WooCommerce API libs (at least , OAuth based ones). This version also includes a tiny modification which will enable easier debugging of possible authentication failures ( try / catch block which is able to fetch original error message from the server).

Possible sources of authentication failures:

-Make sure your WordPress uses mod_rewrite , as you can see WooCommerce's OAuth checking routine expects mod_rewrite format (for example, it expects /wc-api instead of index.php/wc-api).
More details can be found in your WC API authentication file:

includes/api/class-wc-api-authentication.php


-Make sure your storeURL is exactly the same as the site URL in WordPress settings (for example, if you query link http://127.0.0.1/wp/wc-api, and your WP site settings are set to http://localhost, you will get authentication failures.)


Imports System.Security.Cryptography
Imports System.Text
Imports System.Web
Imports System.Net
Imports System.Text.RegularExpressions
Imports System.IO


'VB.NET port  of https://gist.github.com/jakobt/b7400463e90e16928f57 by JakobT. 
'Author of the port: Gogi (http://www.softwarehorizont.com/2015/11/woocommerce-api-v3-vbnet-c-library.html)
'This port includes v3 API support and simple try catch code for easier debugging of invalid auth.requests
'To use it for API versions v1 and v2, just comment the line below USE v3 and uncomment line above (USE v1/v2) in GenerateSignature function



'C# version original header info:
'C# port of the https://github.com/kloon/WooCommerce-REST-API-Client-Library

'Including handling of woocommerce insisting on uppercase UrlEncoded entities
Public Class WoocommerceApiClient
    Private Shared Function HashHMAC(key As Byte(), message As Byte()) As Byte()
        Dim hash = New HMACSHA256(key)
        Return hash.ComputeHash(message)
    End Function

    Private Function Hash(input As String) As String
        Using sha1 As New SHA1Managed()
            Dim hash__1 = sha1.ComputeHash(Encoding.UTF8.GetBytes(input))
            Dim sb = New StringBuilder(hash__1.Length * 2)

            For Each b As Byte In hash__1
                ' can be "x2" if you want lowercase
                sb.Append(b.ToString("X2"))
            Next

            Return sb.ToString()
        End Using
    End Function

    Public Const API_ENDPOINT As String = "wc-api/v3/"
    Public Property ApiUrl() As String
        Get
            Return m_ApiUrl
        End Get
        Set(value As String)
            m_ApiUrl = Value
        End Set
    End Property
    Private m_ApiUrl As String
    Public Property ConsumerSecret() As String
        Get
            Return m_ConsumerSecret
        End Get
        Set(value As String)
            m_ConsumerSecret = Value
        End Set
    End Property
    Private m_ConsumerSecret As String
    Public Property ConsumerKey() As String
        Get
            Return m_ConsumerKey
        End Get
        Set(value As String)
            m_ConsumerKey = Value
        End Set
    End Property
    Private m_ConsumerKey As String
    Public Property IsSsl() As Boolean
        Get
            Return m_IsSsl
        End Get
        Set(value As Boolean)
            m_IsSsl = Value
        End Set
    End Property
    Private m_IsSsl As Boolean

    Public Sub New(consumerKey As String, consumerSecret As String, storeUrl As String, Optional isSsl As Boolean = False)
        If String.IsNullOrEmpty(consumerKey) OrElse String.IsNullOrEmpty(consumerSecret) OrElse String.IsNullOrEmpty(storeUrl) Then
            Throw New ArgumentException("ConsumerKey, consumerSecret and storeUrl are required")
        End If
        Me.ConsumerKey = consumerKey
        Me.ConsumerSecret = consumerSecret
        Me.ApiUrl = Convert.ToString(storeUrl.TrimEnd("/"c) + "/") & API_ENDPOINT
        Me.IsSsl = isSsl
    End Sub

    Public Function GetAllProducts() As String
        Return MakeApiCall("products", New Dictionary(Of String, String)() From { _
            {"filter[limit]", "2000"} _
        })
    End Function
    Public Function GetProducts() As String
        Return MakeApiCall("products")
    End Function

    Private Function MakeApiCall(endpoint As String, Optional parameters As Dictionary(Of String, String) = Nothing, Optional method As String = "GET") As String
        If parameters Is Nothing Then
            parameters = New Dictionary(Of String, String)()
        End If
        parameters("oauth_consumer_key") = Me.ConsumerKey
        parameters("oauth_timestamp") = DateTime.UtcNow.Subtract(New DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds.ToString()
        parameters("oauth_timestamp") = parameters("oauth_timestamp").Substring(0, parameters("oauth_timestamp").IndexOf(","))
        parameters("oauth_nonce") = Hash(parameters("oauth_timestamp"))
        parameters("oauth_signature_method") = "HMAC-SHA256"
        parameters("oauth_signature") = GenerateSignature(parameters, method, endpoint)
        Dim wc As New WebClient()
        Dim sb As New StringBuilder()
        For Each pair As KeyValuePair(Of String, String) In parameters
            sb.AppendFormat("&{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(pair.Value))
        Next
        Dim url = (Me.ApiUrl & endpoint) + "?" + sb.ToString().Substring(1).Replace("%5b", "%5B").Replace("%5d", "%5D")
        Dim result As String = ""



        Try
            result = wc.DownloadString(url)
        Catch wex As WebException
            Dim wexResp As HttpWebResponse
            wexResp = wex.Response

            Dim sr As New StreamReader(wexResp.GetResponseStream())

            Dim exceptionDetails As String

            'put a breakpoint here to check for exception details (response body)
            exceptionDetails = sr.ReadToEnd




        End Try


        Return result


    End Function

    Private Function GenerateSignature(parameters As Dictionary(Of String, String), method As String, endpoint As String) As String
        Dim baserequesturi = Regex.Replace(HttpUtility.UrlEncode(Me.ApiUrl & endpoint), "(%[0-9a-f][0-9a-f])", Function(c) c.Value.ToUpper())
        Dim normalized = NormalizeParameters(parameters)

        Dim signingstring = String.Format("{0}&{1}&{2}", method, baserequesturi, String.Join("%26", normalized.OrderBy(Function(x) x.Key).ToList().ConvertAll(Function(x) x.Key + "%3D" + x.Value)))

        'this is valid for v1 and v2:
        'USE v1/v2:
        'Dim signature = Convert.ToBase64String(HashHMAC(Encoding.UTF8.GetBytes(Me.ConsumerSecret + "&"), Encoding.UTF8.GetBytes(signingstring)))

        'this is valid for v3 (extra & added to consumer secret):
        'USE v3:
        Dim signature = Convert.ToBase64String(HashHMAC(Encoding.UTF8.GetBytes(Me.ConsumerSecret + "&"), Encoding.UTF8.GetBytes(signingstring)))



        Console.WriteLine(signature)
        Return signature
    End Function

    Private Function NormalizeParameters(parameters As Dictionary(Of String, String)) As Dictionary(Of String, String)
        Dim result = New Dictionary(Of String, String)()
        For Each pair As KeyValuePair(Of String, String) In parameters
            Dim key = HttpUtility.UrlEncode(HttpUtility.UrlDecode(pair.Key))
            key = Regex.Replace(key, "(%[0-9a-f][0-9a-f])", Function(c) c.Value.ToUpper()).Replace("%", "%25")
            Dim value = HttpUtility.UrlEncode(HttpUtility.UrlDecode(pair.Value))
            value = Regex.Replace(value, "(%[0-9a-f][0-9a-f])", Function(c) c.Value.ToUpper()).Replace("%", "%25")
            result.Add(key, value)
        Next
        Return result
    End Function
End Class

9 comments:

  1. Hi:
    I have this error in the code:
    Excepci├│n no controlada del tipo 'System.ArgumentOutOfRangeException' en mscorlib.dll

    Additional information: Length can not be less than zero. Line 71

    Why?
    With the version in c# of jakobt. I have https

    ReplyDelete
    Replies
    1. Hi,

      You should check the parameters array. Also, if you go with SSL/HTTPS, you don't have to use OAuth (you can just use Basic Auth, as you can see in WooCommerce API docs).

      Delete
  2. I call to class so.
    Is correct?
    This web is http, not ssl
    string consumerKey = "ck_xxxxxxxxxxx";
    string consumerSecret = "cs_cxxxxxxxxx";
    string storeUrl = "http://miweb/";
    bool isSsl = true;

    WoocommerceApiClient client = new WoocommerceApiClient(consumerKey,consumerSecret,storeUrl,isSsl);
    string orders = client.GetProducts();

    ReplyDelete
  3. Your call code looks ok , just set isSsl to false, because you said that your site is using http and not https.
    The error you mentioned, (Length cannot be less than 0), can be possibly caused at line 65 in original C# code.
    Can you try to replace dot with comma in that line, so your line 65 looks like this:

    parameters["oauth_timestamp"] = parameters["oauth_timestamp"].Substring(0,
    parameters["oauth_timestamp"].IndexOf(","));

    ReplyDelete
    Replies
    1. I have two web one with Http ando other with Https.
      I need rutine for Http.
      I changed "," by "." and now I dont see error, but only I see Form1, no sample data, ¿is normal?

      Delete
  4. Sorry.
    Now I see, I create textbox.
    Thank you very much for the help

    ReplyDelete
    Replies
    1. Great to hear that you've got it working! Greetings! :)

      Delete
  5. This comment has been removed by a blog administrator.

    ReplyDelete
  6. When I use it to create a new product, how do I pass the product json data?

    ReplyDelete

PSR-1 and PSR-2 coding standards for PHP

Visual aspects of code play a significant role in raising or drowning developer's productivity. In case that there's too much clutte...