summaryrefslogtreecommitdiff
path: root/emacs/http-post-simple.el
blob: cf86cb433e072763f7828026e668447e9862e702 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
;;; http-post-simple.el --- HTTP POST requests using the url library

;; Author: Tom Schutzer-Weissmann
;; Keywords: comm, data, processes, hypermedia

;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as1
;; published by the Free Software Foundation; either version 2 of
;; the License, or (at your option) any later version.

;; This program is distributed in the hope that it will be
;; useful, but WITHOUT ANY WARRANTY; without even the implied
;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
;; PURPOSE.  See the GNU General Public License for more details.

;; You should have received a copy of the GNU General Public
;; License along with this program; if not, write to the Free
;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
;; MA 02111-1307 USA

;;; Commentary:

;; Provides ways to use the url library to perform HTTP POST requests.
;; See the documentation to `http-post-simple' for more information.
;;
;; The `url-http' library does not handle 1xx response codes.

;; However, as RFC 2616 puts it:
;;     a server MAY send a 100 (Continue)
;;     status in response to an HTTP/1.1 PUT or POST request that does
;;     not include an Expect request-header field with the "100-continue"
;;     expectation.
;;
;; -- and some servers do, giving you annoying errors. To avoid these errors,
;; you can either set `url-http-version' to "1.0", in which case any compliant
;; server will not send the 100 (Continue) code, or call
;; `http-post-finesse-code-100'. Note that the latter advises
;; 'url-http-parse-response'
;;
;;; Change Log:

;; 11/06/2008 Set `url-http-version' to "1.0" when posting.
;; 19/07/2008 Don't set special variables like `url-http-version' and
;;	      `url-http-attempt-keepalives'.
;; 03/11/2008 Tell the server what charset we're using & accepting.

;;; Code:
(require 'url)
(require 'url-http)

(defvar url-http-response-status nil) ; url-http

(defun http-post-simple (url fields &optional charset)
  "Send FIELDS to URL as an HTTP POST request, returning the response
and response headers.
FIELDS is an alist, eg ((field-name . \"value\")); all values
need to be strings, and they are encoded using CHARSET,
which defaults to 'utf-8"
  (http-post-simple-internal
   url
   (http-post-encode-fields fields charset)
   charset
   `(("Content-Type"
      .
      ,(http-post-content-type
        "application/x-www-form-urlencoded"
        charset)))))


(defun http-post-simple-multipart (url fields files &optional charset)
  "Send FIELDS and FILES to URL as a multipart HTTP POST, returning the
response and response headers.
FIELDS is an alist, as for `http-post-simple', FILES is an a list of
\(fieldname \"filename\" \"file MIME type\" \"file data\")*"
  (let ((boundary (http-post-multipart-boundary)))
    (http-post-simple-internal
     url
     (http-post-encode-multipart-data fields files charset)
     charset
     `(("Content-Type"
        .
        ,(http-post-content-type
          (format "multipart/form-data; boundary=%S" boundary)
          charset))))))


(defun http-post-content-type (content-type &optional charset)
  (if charset
      (format "%s; charset=%s" content-type (http-post-charset-name charset))
    content-type))


(defun http-post-charset-name (charset)
  (symbol-name charset))


;; based on `http-url-encode' from the from http-get package
;; http://savannah.nongnu.org/projects/http-emacs
(defun http-post-encode-string (str content-type)
  "URL encode STR using CONTENT-TYPE as the coding system."
  (apply 'concat
	 (mapcar (lambda (c)
		   (if (or (and (>= c ?a) (<= c ?z))
			   (and (>= c ?A) (<= c ?Z))
			   (and (>= c ?0) (<= c ?9)))
		       (string c)
                     (format "%%%02x" c)))
		 (encode-coding-string str content-type))))


(defun http-post-encode-fields (fields &optional charset)
  "Encode FIELDS using `http-post-encode-string', where
FIELDS is an alist of \(
	\(field-name-as-symbol . \"field value as string\"\) |
	\(field-name \"value1\" \"value2\" ...\)
	\)*

CHARSET defaults to 'utf-8"
  (let ((charset (or charset 'utf-8)))
    (mapconcat #'identity
	       (mapcar (lambda (field)
                         (concat (symbol-name (car field))
                                 "="
                                 (http-post-encode-string (cdr field) charset)))
		       (cl-mapcan (lambda (field)
                                    (if (atom (cdr field)) (list field)
                                      ;; unpack the list
                                      (mapcar (lambda (value)
                                                `(,(car field) . ,value))
                                              (cdr field))))
                                  fields))
	       "&")))


(defun http-post-simple-internal (url data charset extra-headers)
  (let ((url-request-method        "POST")
	(url-request-data          data)
	(url-request-extra-headers extra-headers)
        (url-mime-charset-string   (http-post-charset-name charset)))
    (let (header
	  data
	  status)
      (with-current-buffer
	  (url-retrieve-synchronously url)
	;; status
	(setq status url-http-response-status)
	;; return the header and the data separately
	(goto-char (point-min))
	(if (search-forward-regexp "^$" nil t)
	    (setq header (buffer-substring (point-min) (point))
		  data   (buffer-substring (1+ (point)) (point-max)))
          ;; unexpected situation, return the whole buffer
          (setq data (buffer-string))))
      (values data header status))))


(defun http-post-multipart-boundary ()
  "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=")


(defun http-post-bound-field (&rest parts)
  (let ((boundary (format "--%s" (http-post-multipart-boundary))))
    (http-post-join-lines  boundary parts)))


(defun http-post-encode-multipart-data (fields files charset)
  "Return FIELDS and FILES encoded for use as the data for a multipart HTTP POST request"
  (http-post-join-lines
   (mapcar (lambda (field)
             (http-post-bound-field
              (format "Content-Disposition: form-data; name=%S" (symbol-name (car field)))
              ""
              (cdr field)))
	   fields)
   (cl-mapcan (lambda (file)
                (destructuring-bind (fieldname filename mime-type data) file
                  (http-post-bound-field
                   (format "Content-Disposition: form-data; name=%S; filename=%S" fieldname filename)
                   (format "Content-type: %s" (http-post-content-type mime-type charset))
                   ""
                   data)))
              files)
   (format "--%s--" (http-post-multipart-boundary))))


(defun http-post-join-lines (&rest bits)
  (let ((sep "\r\n"))
    (mapconcat (lambda (bit)
		 (if (listp bit)
		     (apply 'http-post-join-lines bit)
                   bit))
	       bits sep)))


(defun http-post-finesse-code-100 ()
  "Transforms response code 100 into 200, to avoid errors when the
server sends code 100 in response to a POST request."
  (defadvice url-http-parse-response (after url-http-parse-response-100 activate)
    "Turns any HTTP 100 response code to 200, to avoid getting an error."
    (declare (special url-http-response-status
                      url-request-method))
    (when (and (= 100               url-http-response-status)
               (string-equal "POST" url-request-method)
               (string-equal "1.1"  url-http-version))
      (setf url-http-response-status 200))))

(provide 'http-post-simple)