Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a method to perform POST Form Upload #69

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ RestClient::Response r = RestClient::put("http://url.com/put", "application/json
RestClient::Response r = RestClient::patch("http://url.com/patch", "application/json", "{\"foo\": \"bla\"}")
RestClient::Response r = RestClient::del("http://url.com/delete")
RestClient::Response r = RestClient::head("http://url.com")

// Post Form Upload
/* Filling information about the form in a RestClient::PostFormInfo object */
RestClient::PostFormInfo uploadInfo;
/* "submitted" is the name of the "file" input and "TestPostForm.txt"
is the location of the file to submit.
<input type="file" name="submitted">
*/
uploadInfo.addFormFile("submitted", "TestPostForm.txt");
/* In some rare cases, some fields related to the form can be filled with
addFormContent(), the 1st argument is the name of the input element and
the 2nd argument is the value assigned to it.
<input type="text" name="filename" value=""/>
<input type="submit" name="submit" value="send">
*/
uploadInfo.addFormContent("filename", "myfile.cpp");
uploadInfo.addFormContent("submit", "send");
/* Performing a post form upload with the information provided above */
RestClient::Response res = RestClient::postForm("http://posttestserver.com/post.php?dir=restclientcpptests", uploadInfo);
RestClient::Response r = RestClient::options("http://url.com")
```

Expand Down
2 changes: 2 additions & 0 deletions include/restclient-cpp/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ class Connection {
RestClient::Response get(const std::string& uri);
RestClient::Response post(const std::string& uri,
const std::string& data);
RestClient::Response postForm(const std::string& uri,
const PostFormInfo& data);
RestClient::Response put(const std::string& uri,
const std::string& data);
RestClient::Response patch(const std::string& uri,
Expand Down
2 changes: 2 additions & 0 deletions include/restclient-cpp/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#ifndef INCLUDE_RESTCLIENT_CPP_HELPERS_H_
#define INCLUDE_RESTCLIENT_CPP_HELPERS_H_

#include <curl/curl.h>

#include <string>
#include <cctype>
#include <algorithm>
Expand Down
23 changes: 23 additions & 0 deletions include/restclient-cpp/restclient.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <map>
#include <cstdlib>

#include "restclient-cpp/helpers.h"
#include "restclient-cpp/version.h"

/**
Expand Down Expand Up @@ -40,6 +41,26 @@ typedef struct {
HeaderFields headers;
} Response;

/** @class PostFormInfo
* @brief This class represents the form information to send on
* POST Form requests
*/
class PostFormInfo {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name doesn't accurately reflect the notion that a form can be submitted with any HTTP method. Why not just name this FormData instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

struct curl_httppost* formPtr;
struct curl_httppost* lastFormPtr;
public:
PostFormInfo();
~PostFormInfo();
/* Fill in the file upload field */
void addFormFile(const std::string& fieldName,
const std::string& fieldValue);
/* Fill in the filename or the submit field */
void addFormContent(const std::string& fieldName,
const std::string& fieldValue);
/* Get Form pointer */
struct curl_httppost* GetFormPtr() const { return formPtr; }
};

// init and disable functions
int init();
void disable();
Expand All @@ -53,6 +74,8 @@ Response get(const std::string& url);
Response post(const std::string& url,
const std::string& content_type,
const std::string& data);
Response postForm(const std::string& url,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use an overload of post() instead of this newly named function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

const PostFormInfo& data);
Response put(const std::string& url,
edwinpjacques marked this conversation as resolved.
Show resolved Hide resolved
const std::string& content_type,
const std::string& data);
Expand Down
20 changes: 20 additions & 0 deletions source/connection.cc
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,26 @@ RestClient::Connection::post(const std::string& url,

return this->performCurlRequest(url);
}
/**
* @brief HTTP POST Form method
*
* @param url to query
* @param data form info
*
* @return response struct
*/
RestClient::Response
RestClient::Connection::postForm(const std::string& url,
const PostFormInfo& data) {
/** Now specify we want to POST data */
curl_easy_setopt(this->curlHandle, CURLOPT_POST, 1L);
/* stating that Expect: 100-continue is not wanted */
AppendHeader("Expect", "");
/** set post form */
curl_easy_setopt(this->curlHandle, CURLOPT_HTTPPOST, data.GetFormPtr());

return this->performCurlRequest(url);
}
/**
* @brief HTTP PUT method
*
Expand Down
1 change: 1 addition & 0 deletions source/helpers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ size_t RestClient::Helpers::write_callback(void *data, size_t size,
* @param size of data
* @param nmemb memblock
* @param userdata pointer to user data object to save headr data
*
* @return size * nmemb;
*/
size_t RestClient::Helpers::header_callback(void *data, size_t size,
Expand Down
65 changes: 65 additions & 0 deletions source/restclient.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ RestClient::Response RestClient::post(const std::string& url,
#endif
}

/**
* @brief HTTP POST Form method
*
* @param url to query
* @param data post form information
*
* @return response struct
*/
RestClient::Response RestClient::postForm(const std::string& url,
const PostFormInfo& data) {
RestClient::Response ret;
RestClient::Connection *conn = new RestClient::Connection("");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There shouldn't be any need to use new; you only need the connection during the lifetime of the function. Just use a stack allocation instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true. That will allow the remove of a lot of extraneous code and improve performance of RestClient interface. It should be done for all RestClient methods.

ret = conn->postForm(url, data);
delete conn;
return ret;
}

/**
* @brief HTTP PUT method
*
Expand Down Expand Up @@ -187,3 +204,51 @@ RestClient::Response RestClient::options(const std::string& url) {
delete conn;
return ret;
}

/**
* @brief PostFormInfo constructor
*/
RestClient::PostFormInfo::PostFormInfo()
: formPtr(NULL), lastFormPtr(NULL) {
}

/**
* @brief PostFormInfo destructor
*/
RestClient::PostFormInfo::~PostFormInfo() {
// cleanup the formpost chain
if (this->formPtr) {
curl_formfree(this->formPtr);
this->formPtr = NULL;
this->lastFormPtr = NULL;
}
}

/**
* @brief set the name and the value of the HTML "file" form's input
*
* @param fieldName name of the "file" input
* @param fieldValue path to the file to upload
*/
void RestClient::PostFormInfo::addFormFile(
const std::string& fieldName, const std::string& fieldValue) {
curl_formadd(&this->formPtr, &this->lastFormPtr,
CURLFORM_COPYNAME, fieldName.c_str(),
CURLFORM_FILE, fieldValue.c_str(),
CURLFORM_END);
}

/**
* @brief set the name and the value of an HTML form's input
* (other than "file" like "text", "hidden" or "submit")
*
* @param fieldName name of the input element
* @param fieldValue value to be assigned to the input element
*/
void RestClient::PostFormInfo::addFormContent(
const std::string& fieldName, const std::string& fieldValue) {
curl_formadd(&this->formPtr, &this->lastFormPtr,
CURLFORM_COPYNAME, fieldName.c_str(),
CURLFORM_COPYCONTENTS, fieldValue.c_str(),
CURLFORM_END);
}
34 changes: 34 additions & 0 deletions test/test_restclient.cc
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#include "restclient-cpp/restclient.h"
#include <gtest/gtest.h>
#include <json/json.h>
#include <cstdio>
#include <fstream>
#include <string>
#include <sstream>

class RestClientTest : public ::testing::Test
{
Expand Down Expand Up @@ -118,6 +121,37 @@ TEST_F(RestClientTest, TestRestClientPOSTBody)
EXPECT_EQ("restclient-cpp/" RESTCLIENT_VERSION, root["headers"].get("User-Agent", "no url set").asString());
}

TEST_F(RestClientTest, TestRestClientPostForm)
{
// generating a file name with a timestamp
std::ostringstream fileName;
time_t rawtime;
tm * timeinfo;
time(&rawtime);
timeinfo = localtime( &rawtime );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system time is not a reliable way to ensure you have a unique file name. You should just use mkstemp() to get a unique filepath instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but it's reliable enough. If it really bugs you, it can be changed. Mostly this needs to run on dev systems on and a clean container that's spun up in CI so no worries there.


fileName << "TestPostForm_" << (timeinfo->tm_year)+1900 << "_" << timeinfo->tm_mon+1
<< "_" << timeinfo->tm_mday << "-" << timeinfo->tm_hour
<< "_"<< timeinfo->tm_min << "_" << timeinfo->tm_sec << ".txt";

// creating a dummy file to upload via a post form request
std::ofstream ofDummyFile(fileName.str().c_str());
ASSERT_TRUE(static_cast<bool>(ofDummyFile));
ofDummyFile << "Dummy file for the unit test 'TestRestClientPostForm' of the restclient-cpp Project.";
ASSERT_TRUE(static_cast<bool>(ofDummyFile));
ofDummyFile.close();

// uploading the dummy file
RestClient::PostFormInfo UploadInfo;
UploadInfo.addFormFile("submitted", fileName.str());
UploadInfo.addFormContent("filename", fileName.str());
RestClient::Response res = RestClient::postForm("http://posttestserver.com/post.php?dir=restclientcpptests", UploadInfo);
EXPECT_EQ(200, res.code);

// remove dummy file
remove(fileName.str().c_str());
}

// check for failure
TEST_F(RestClientTest, TestRestClientPOSTFailureCode)
{
Expand Down