/* curb_postfield.c - Field class for POST method * Copyright (c)2006 Ross Bamford. * Licensed under the Ruby License. See LICENSE for details. * * $Id: curb_postfield.c 30 2006-12-09 12:30:24Z roscopeco $ */ #include "curb_postfield.h" #include "curb_errors.h" extern VALUE mCurl; static VALUE idCall; #ifdef RDOC_NEVER_DEFINED mCurl = rb_define_module("Curl"); #endif VALUE cCurlPostField; /* ================= APPEND FORM FUNC ================ */ /* This gets called by the post method on Curl::Easy for each postfield * supplied in the arguments. It's job is to add the supplied field to * the list that's being built for a perform. * * THIS FUNC MODIFIES ITS ARGUMENTS. See curl_formadd(3) for details. */ void append_to_form(VALUE self, struct curl_httppost **first, struct curl_httppost **last) { ruby_curl_postfield *rbcpf; CURLFORMcode result = -1; Data_Get_Struct(self, ruby_curl_postfield, rbcpf); if (rbcpf->name == Qnil) { rb_raise(eCurlErrInvalidPostField, "Cannot post unnamed field"); } else { if ((rbcpf->local_file != Qnil) || (rbcpf->remote_file != Qnil)) { // is a file upload field if (rbcpf->content_proc != Qnil) { // with content proc rbcpf->buffer_str = rb_funcall(rbcpf->content_proc, idCall, 1, self); if (rbcpf->remote_file == Qnil) { rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field with no filename"); } else { if (rbcpf->content_type == Qnil) { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_BUFFER, StringValuePtr(rbcpf->remote_file), CURLFORM_BUFFERPTR, StringValuePtr(rbcpf->buffer_str), CURLFORM_BUFFERLENGTH, RSTRING_LEN(rbcpf->buffer_str), CURLFORM_END); } else { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_BUFFER, StringValuePtr(rbcpf->remote_file), CURLFORM_BUFFERPTR, StringValuePtr(rbcpf->buffer_str), CURLFORM_BUFFERLENGTH, RSTRING_LEN(rbcpf->buffer_str), CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type), CURLFORM_END); } } } else if (rbcpf->content != Qnil) { // with content if (rbcpf->remote_file == Qnil) { rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field with no filename"); } else { if (rbcpf->content_type == Qnil) { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_BUFFER, StringValuePtr(rbcpf->remote_file), CURLFORM_BUFFERPTR, StringValuePtr(rbcpf->content), CURLFORM_BUFFERLENGTH, RSTRING_LEN(rbcpf->content), CURLFORM_END); } else { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_BUFFER, StringValuePtr(rbcpf->remote_file), CURLFORM_BUFFERPTR, StringValuePtr(rbcpf->content), CURLFORM_BUFFERLENGTH, RSTRING_LEN(rbcpf->content), CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type), CURLFORM_END); } } } else if (rbcpf->local_file != Qnil) { // with local filename if (rbcpf->local_file == Qnil) { rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field no filename"); } else { if (rbcpf->remote_file == Qnil) { rbcpf->remote_file = rbcpf->local_file; } if (rbcpf->content_type == Qnil) { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_FILE, StringValuePtr(rbcpf->local_file), CURLFORM_FILENAME, StringValuePtr(rbcpf->remote_file), CURLFORM_END); } else { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_FILE, StringValuePtr(rbcpf->local_file), CURLFORM_FILENAME, StringValuePtr(rbcpf->remote_file), CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type), CURLFORM_END); } } } else { rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field with no data"); } } else { // is a content field if (rbcpf->content_proc != Qnil) { rbcpf->buffer_str = rb_funcall(rbcpf->content_proc, idCall, 1, self); if (rbcpf->content_type == Qnil) { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_PTRCONTENTS, StringValuePtr(rbcpf->buffer_str), CURLFORM_CONTENTSLENGTH, RSTRING_LEN(rbcpf->buffer_str), CURLFORM_END); } else { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_PTRCONTENTS, StringValuePtr(rbcpf->buffer_str), CURLFORM_CONTENTSLENGTH, RSTRING_LEN(rbcpf->buffer_str), CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type), CURLFORM_END); } } else if (rbcpf->content != Qnil) { if (rbcpf->content_type == Qnil) { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_PTRCONTENTS, StringValuePtr(rbcpf->content), CURLFORM_CONTENTSLENGTH, RSTRING_LEN(rbcpf->content), CURLFORM_END); } else { result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name), CURLFORM_PTRCONTENTS, StringValuePtr(rbcpf->content), CURLFORM_CONTENTSLENGTH, RSTRING_LEN(rbcpf->content), CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type), CURLFORM_END); } } else { rb_raise(eCurlErrInvalidPostField, "Cannot post content field with no data"); } } } if (result != 0) { const char *reason = NULL; switch (result) { case CURL_FORMADD_MEMORY: reason = "Memory allocation failed"; break; case CURL_FORMADD_OPTION_TWICE: reason = "Duplicate option"; break; case CURL_FORMADD_NULL: reason = "Unexpected NULL string"; break; case CURL_FORMADD_UNKNOWN_OPTION: reason = "Unknown option"; break; case CURL_FORMADD_INCOMPLETE: reason = "Incomplete form data"; break; case CURL_FORMADD_ILLEGAL_ARRAY: reason = "Illegal array [BINDING BUG]"; break; case CURL_FORMADD_DISABLED: reason = "Installed libcurl cannot support requested feature(s)"; break; default: reason = "Unknown error"; } rb_raise(eCurlErrInvalidPostField, "Failed to add field (%s)", reason); } } /* ================== MARK/FREE FUNC ==================*/ void curl_postfield_mark(ruby_curl_postfield *rbcpf) { rb_gc_mark(rbcpf->name); rb_gc_mark(rbcpf->content); rb_gc_mark(rbcpf->content_type); rb_gc_mark(rbcpf->local_file); rb_gc_mark(rbcpf->remote_file); rb_gc_mark(rbcpf->buffer_str); } void curl_postfield_free(ruby_curl_postfield *rbcpf) { free(rbcpf); } /* ================= ALLOC METHODS ====================*/ /* * call-seq: * Curl::PostField.content(name, content) => #<Curl::PostField...> * Curl::PostField.content(name, content, content_type = nil) => #<Curl::PostField...> * Curl::PostField.content(name, content_type = nil) { |field| ... } => #<Curl::PostField...> * * Create a new Curl::PostField, supplying the field name, content, * and, optionally, Content-type (curl will attempt to determine this if * not specified). * * The block form allows a block to supply the content for this field, called * during the perform. The block should return a ruby string with the field * data. */ static VALUE ruby_curl_postfield_new_content(int argc, VALUE *argv, VALUE klass) { ruby_curl_postfield *rbcpf = ALLOC(ruby_curl_postfield); // wierdness - we actually require two args, unless a block is provided, but // we have to work that out below. rb_scan_args(argc, argv, "12&", &rbcpf->name, &rbcpf->content, &rbcpf->content_type, &rbcpf->content_proc); // special handling if theres a block, second arg is actually content_type if (rbcpf->content_proc != Qnil) { if (rbcpf->content != Qnil) { // we were given a content-type rbcpf->content_type = rbcpf->content; rbcpf->content = Qnil; } else { // default content type rbcpf->content_type = Qnil; } } else { // no block, so make sure content was provided if (rbcpf->content == Qnil) { rb_raise(rb_eArgError, "Incorrect number of arguments (expected 2 or 3)"); } } /* assoc objects */ rbcpf->local_file = Qnil; rbcpf->remote_file = Qnil; rbcpf->buffer_str = Qnil; return Data_Wrap_Struct(cCurlPostField, curl_postfield_mark, curl_postfield_free, rbcpf); } /* * call-seq: * Curl::PostField.file(name, local_file_name) => #<Curl::PostField...> * Curl::PostField.file(name, local_file_name, remote_file_name = local_file_name) => #<Curl::PostField...> * Curl::PostField.file(name, remote_file_name) { |field| ... } => #<Curl::PostField...> * * Create a new Curl::PostField for a file upload field, supplying the local filename * to read from, and optionally the remote filename (defaults to the local name). * * The block form allows a block to supply the content for this field, called * during the perform. The block should return a ruby string with the field * data. */ static VALUE ruby_curl_postfield_new_file(int argc, VALUE *argv, VALUE klass) { // TODO needs to handle content-type too ruby_curl_postfield *rbcpf = ALLOC(ruby_curl_postfield); rb_scan_args(argc, argv, "21&", &rbcpf->name, &rbcpf->local_file, &rbcpf->remote_file, &rbcpf->content_proc); // special handling if theres a block, second arg is actually remote name. if (rbcpf->content_proc != Qnil) { if (rbcpf->local_file != Qnil) { // we were given a local file if (rbcpf->remote_file == Qnil) { // we weren't given a remote, so local is actually remote // (correct block call form) rbcpf->remote_file = rbcpf->local_file; } // Shouldn't get a local file, so can ignore it. rbcpf->local_file = Qnil; } } else { if (rbcpf->remote_file == Qnil) { rbcpf->remote_file = rbcpf->local_file; } } /* assoc objects */ rbcpf->content = Qnil; rbcpf->content_type = Qnil; rbcpf->buffer_str = Qnil; return Data_Wrap_Struct(cCurlPostField, curl_postfield_mark, curl_postfield_free, rbcpf); } /* ================= ATTRIBUTES ====================*/ /* * call-seq: * field.name = "name" => "name" * * Set the POST field name for this PostField. */ static VALUE ruby_curl_postfield_name_set(VALUE self, VALUE name) { CURB_OBJECT_SETTER(ruby_curl_postfield, name); } /* * call-seq: * field.name => "name" * * Obtain the POST field name for this PostField. */ static VALUE ruby_curl_postfield_name_get(VALUE self) { CURB_OBJECT_GETTER(ruby_curl_postfield, name); } /* * call-seq: * field.content = "content" => "content" * * Set the POST field content for this PostField. Ignored when a * content_proc is supplied via either +Curl::PostField.file+ or * +set_content_proc+. */ static VALUE ruby_curl_postfield_content_set(VALUE self, VALUE content) { CURB_OBJECT_SETTER(ruby_curl_postfield, content); } /* * call-seq: * field.content => "content" * * Obtain the POST field content for this PostField. */ static VALUE ruby_curl_postfield_content_get(VALUE self) { CURB_OBJECT_GETTER(ruby_curl_postfield, content); } /* * call-seq: * field.content_type = "content_type" => "content_type" * * Set the POST field Content-type for this PostField. */ static VALUE ruby_curl_postfield_content_type_set(VALUE self, VALUE content_type) { CURB_OBJECT_SETTER(ruby_curl_postfield, content_type); } /* * call-seq: * field.content_type => "content_type" * * Get the POST field Content-type for this PostField. */ static VALUE ruby_curl_postfield_content_type_get(VALUE self) { CURB_OBJECT_GETTER(ruby_curl_postfield, content_type); } /* * call-seq: * field.local_file = "filename" => "filename" * * Set the POST field local filename for this PostField (when performing * a file upload). Ignored when a content_proc is supplied via either * +Curl::PostField.file+ or +set_content_proc+. */ static VALUE ruby_curl_postfield_local_file_set(VALUE self, VALUE local_file) { CURB_OBJECT_SETTER(ruby_curl_postfield, local_file); } /* * call-seq: * field.local_file => "filename" * * Get the POST field local filename for this PostField (when performing * a file upload). */ static VALUE ruby_curl_postfield_local_file_get(VALUE self) { CURB_OBJECT_GETTER(ruby_curl_postfield, local_file); } /* * call-seq: * field.remote_file = "filename" => "filename" * * Set the POST field remote filename for this PostField (when performing * a file upload). If no remote filename is provided, and no content_proc * is supplied, the local filename is used. If no remote filename is * specified when a content_proc is used, an exception will be raised * during the perform. */ static VALUE ruby_curl_postfield_remote_file_set(VALUE self, VALUE remote_file) { CURB_OBJECT_SETTER(ruby_curl_postfield, remote_file); } /* * call-seq: * field.local_file => "filename" * * Get the POST field remote filename for this PostField (when performing * a file upload). */ static VALUE ruby_curl_postfield_remote_file_get(VALUE self) { CURB_OBJECT_GETTER(ruby_curl_postfield, remote_file); } /* * call-seq: * field.set_content_proc { |field| ... } => <old proc> * * Set a content proc for this field. This proc will be called during the * perform to supply the content for this field, overriding any setting * of +content+ or +local_file+. */ static VALUE ruby_curl_postfield_content_proc_set(int argc, VALUE *argv, VALUE self) { CURB_HANDLER_PROC_SETTER(ruby_curl_postfield, content_proc); } /* * call-seq: * field.to_str => "name=value" * field.to_s => "name=value" * * Obtain a String representation of this PostField in url-encoded * format. This is used to construct the post data for non-multipart * POSTs. * * Only content fields may be converted to strings. */ static VALUE ruby_curl_postfield_to_str(VALUE self) { // FIXME This is using the deprecated curl_escape func ruby_curl_postfield *rbcpf; VALUE result = Qnil; VALUE name = Qnil; char *tmpchrs; Data_Get_Struct(self, ruby_curl_postfield, rbcpf); if (rbcpf->name != Qnil) { name = rbcpf->name; if (rb_type(name) == T_STRING) { name = rbcpf->name; } else if (rb_respond_to(name,rb_intern("to_s"))) { name = rb_funcall(name, rb_intern("to_s"), 0); } else { name = Qnil; // we can't handle this object } } if (name == Qnil) { rb_raise(eCurlErrInvalidPostField, "Cannot convert unnamed field to string %s:%d, make sure your field name responds_to :to_s", __FILE__, __LINE__); } tmpchrs = curl_escape(StringValuePtr(name), (int)RSTRING_LEN(name)); if (!tmpchrs) { rb_raise(eCurlErrInvalidPostField, "Failed to url-encode name `%s'", tmpchrs); } else { VALUE tmpcontent = Qnil; VALUE escd_name = rb_str_new2(tmpchrs); curl_free(tmpchrs); if (rbcpf->content_proc != Qnil) { tmpcontent = rb_funcall(rbcpf->content_proc, idCall, 1, self); } else if (rbcpf->content != Qnil) { tmpcontent = rbcpf->content; } else if (rbcpf->local_file != Qnil) { tmpcontent = rbcpf->local_file; } else if (rbcpf->remote_file != Qnil) { tmpcontent = rbcpf->remote_file; } else { tmpcontent = rb_str_new2(""); } if (TYPE(tmpcontent) != T_STRING) { if (rb_respond_to(tmpcontent, rb_intern("to_s"))) { tmpcontent = rb_funcall(tmpcontent, rb_intern("to_s"), 0); } else { rb_raise(rb_eRuntimeError, "postfield(%s) is not a string and does not respond_to to_s", RSTRING_PTR(escd_name) ); } } //fprintf(stderr, "encoding content: %ld - %s\n", RSTRING_LEN(tmpcontent), RSTRING_PTR(tmpcontent) ); tmpchrs = curl_escape(RSTRING_PTR(tmpcontent), (int)RSTRING_LEN(tmpcontent)); if (!tmpchrs) { rb_raise(eCurlErrInvalidPostField, "Failed to url-encode content `%s'", tmpchrs); } else { VALUE escd_content = rb_str_new2(tmpchrs); curl_free(tmpchrs); result = escd_name; rb_str_cat(result, "=", 1); rb_str_concat(result, escd_content); } } return result; } /* =================== INIT LIB =====================*/ void init_curb_postfield() { VALUE sc; idCall = rb_intern("call"); cCurlPostField = rb_define_class_under(mCurl, "PostField", rb_cObject); /* Class methods */ rb_define_singleton_method(cCurlPostField, "content", ruby_curl_postfield_new_content, -1); rb_define_singleton_method(cCurlPostField, "file", ruby_curl_postfield_new_file, -1); sc = rb_singleton_class(cCurlPostField); rb_undef(sc, rb_intern("new")); rb_define_method(cCurlPostField, "name=", ruby_curl_postfield_name_set, 1); rb_define_method(cCurlPostField, "name", ruby_curl_postfield_name_get, 0); rb_define_method(cCurlPostField, "content=", ruby_curl_postfield_content_set, 1); rb_define_method(cCurlPostField, "content", ruby_curl_postfield_content_get, 0); rb_define_method(cCurlPostField, "content_type=", ruby_curl_postfield_content_type_set, 1); rb_define_method(cCurlPostField, "content_type", ruby_curl_postfield_content_type_get, 0); rb_define_method(cCurlPostField, "local_file=", ruby_curl_postfield_local_file_set, 1); rb_define_method(cCurlPostField, "local_file", ruby_curl_postfield_local_file_get, 0); rb_define_method(cCurlPostField, "remote_file=", ruby_curl_postfield_remote_file_set, 1); rb_define_method(cCurlPostField, "remote_file", ruby_curl_postfield_remote_file_get, 0); rb_define_method(cCurlPostField, "set_content_proc", ruby_curl_postfield_content_proc_set, -1); rb_define_method(cCurlPostField, "to_str", ruby_curl_postfield_to_str, 0); rb_define_alias(cCurlPostField, "to_s", "to_str"); }