tvl-depot/configs/shared/emacs/.emacs.d/elpa/request-20170131.1747/request.el

1225 lines
48 KiB
EmacsLisp
Raw Normal View History

;;; request.el --- Compatible layer for URL request in Emacs -*- lexical-binding: t; -*-
;; Copyright (C) 2012 Takafumi Arakaki
;; Copyright (C) 1985-1986, 1992, 1994-1995, 1999-2012
;; Free Software Foundation, Inc.
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
;; Package-Requires: ((emacs "24.4"))
;; Package-Version: 20170131.1747
;; Version: 0.3.0
;; This file is NOT part of GNU Emacs.
;; request.el is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; request.el 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 request.el.
;; If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Request.el is a HTTP request library with multiple backends. It
;; supports url.el which is shipped with Emacs and curl command line
;; program. User can use curl when s/he has it, as curl is more reliable
;; than url.el. Library author can use request.el to avoid imposing
;; external dependencies such as curl to users while giving richer
;; experience for users who have curl.
;; Following functions are adapted from GNU Emacs source code.
;; Free Software Foundation holds the copyright of them.
;; * `request--process-live-p'
;; * `request--url-default-expander'
;;; Code:
(eval-when-compile
(defvar url-http-method)
(defvar url-http-response-status))
(require 'cl-lib)
(require 'url)
(require 'mail-utils)
(defgroup request nil
"Compatible layer for URL request in Emacs."
:group 'comm
:prefix "request-")
(defconst request-version "0.3.0")
;;; Customize variables
(defcustom request-storage-directory
(concat (file-name-as-directory user-emacs-directory) "request")
"Directory to store data related to request.el."
:type 'directory)
(defcustom request-curl "curl"
"Executable for curl command."
:type 'string)
(defcustom request-curl-options nil
"curl command options.
List of strings that will be passed to every curl invocation. You can pass
extra options here, like setting the proxy."
:type '(repeat string))
(defcustom request-backend (if (executable-find request-curl)
'curl
'url-retrieve)
"Backend to be used for HTTP request.
Automatically set to `curl' if curl command is found."
:type '(choice (const :tag "cURL backend" curl)
(const :tag "url-retrieve backend" url-retrieve)))
(defcustom request-timeout nil
"Default request timeout in second.
`nil' means no timeout."
:type '(choice (integer :tag "Request timeout seconds")
(boolean :tag "No timeout" nil)))
(defcustom request-temp-prefix "emacs-request"
"Prefix for temporary files created by Request."
:type 'string
:risky t)
(defcustom request-log-level -1
"Logging level for request.
One of `error'/`warn'/`info'/`verbose'/`debug'.
-1 means no logging."
:type '(choice (integer :tag "No logging" -1)
(const :tag "Level error" error)
(const :tag "Level warn" warn)
(const :tag "Level info" info)
(const :tag "Level Verbose" verbose)
(const :tag "Level DEBUG" debug)))
(defcustom request-message-level 'warn
"Logging level for request.
See `request-log-level'."
:type '(choice (integer :tag "No logging" -1)
(const :tag "Level error" error)
(const :tag "Level warn" warn)
(const :tag "Level info" info)
(const :tag "Level Verbose" verbose)
(const :tag "Level DEBUG" debug)))
;;; Utilities
(defun request--safe-apply (function &rest arguments)
(condition-case err
(apply #'apply function arguments)
((debug error))))
(defun request--safe-call (function &rest arguments)
(request--safe-apply function arguments))
;; (defun request--url-no-cache (url)
;; "Imitate `cache=false' of `jQuery.ajax'.
;; See: http://api.jquery.com/jQuery.ajax/"
;; ;; FIXME: parse URL before adding ?_=TIME.
;; (concat url (format-time-string "?_=%s")))
(defmacro request--document-function (function docstring)
"Document FUNCTION with DOCSTRING. Use this for defstruct accessor etc."
(declare (indent defun)
(doc-string 2))
`(put ',function 'function-documentation ,docstring))
(defun request--process-live-p (process)
"Copied from `process-live-p' for backward compatibility (Emacs < 24).
Adapted from lisp/subr.el.
FSF holds the copyright of this function:
Copyright (C) 1985-1986, 1992, 1994-1995, 1999-2012
Free Software Foundation, Inc."
(memq (process-status process) '(run open listen connect stop)))
;;; Logging
(defconst request--log-level-def
'(;; debugging
(blather . 60) (trace . 50) (debug . 40)
;; information
(verbose . 30) (info . 20)
;; errors
(warn . 10) (error . 0))
"Named logging levels.")
(defun request--log-level-as-int (level)
(if (integerp level)
level
(or (cdr (assq level request--log-level-def))
0)))
(defvar request-log-buffer-name " *request-log*")
(defun request--log-buffer ()
(get-buffer-create request-log-buffer-name))
(defmacro request-log (level fmt &rest args)
(declare (indent 1))
`(let ((level (request--log-level-as-int ,level))
(log-level (request--log-level-as-int request-log-level))
(msg-level (request--log-level-as-int request-message-level)))
(when (<= level (max log-level msg-level))
(let ((msg (format "[%s] %s" ,level
(condition-case err
(format ,fmt ,@args)
(error (format "
!!! Logging error while executing:
%S
!!! Error:
%S"
',args err))))))
(when (<= level log-level)
(with-current-buffer (request--log-buffer)
(setq buffer-read-only t)
(let ((inhibit-read-only t))
(goto-char (point-max))
(insert msg "\n"))))
(when (<= level msg-level)
(message "REQUEST %s" msg))))))
;;; HTTP specific utilities
(defconst request--url-unreserved-chars
'(?a ?b ?c ?d ?e ?f ?g ?h ?i ?j ?k ?l ?m ?n ?o ?p ?q ?r ?s ?t ?u ?v ?w ?x ?y ?z
?A ?B ?C ?D ?E ?F ?G ?H ?I ?J ?K ?L ?M ?N ?O ?P ?Q ?R ?S ?T ?U ?V ?W ?X ?Y ?Z
?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9
?- ?_ ?. ?~)
"`url-unreserved-chars' copied from Emacs 24.3 release candidate.
This is used for making `request--urlencode-alist' RFC 3986 compliant
for older Emacs versions.")
(defun request--urlencode-alist (alist)
;; FIXME: make monkey patching `url-unreserved-chars' optional
(let ((url-unreserved-chars request--url-unreserved-chars))
(cl-loop for sep = "" then "&"
for (k . v) in alist
concat sep
concat (url-hexify-string (format "%s" k))
concat "="
concat (url-hexify-string (format "%s" v)))))
;;; Header parser
(defun request--parse-response-at-point ()
"Parse the first header line such as \"HTTP/1.1 200 OK\"."
(when (re-search-forward "\\=[ \t\n]*HTTP/\\([0-9\\.]+\\) +\\([0-9]+\\)" nil t)
(list :version (match-string 1)
:code (string-to-number (match-string 2)))))
(defun request--goto-next-body ()
(re-search-forward "^\r\n"))
;;; Response object
(cl-defstruct request-response
"A structure holding all relevant information of a request."
status-code history data error-thrown symbol-status url
done-p settings
;; internal variables
-buffer -raw-header -timer -backend -tempfiles)
(defmacro request--document-response (function docstring)
(declare (indent defun)
(doc-string 2))
`(request--document-function ,function ,(concat docstring "
.. This is an accessor for `request-response' object.
\(fn RESPONSE)")))
(request--document-response request-response-status-code
"Integer HTTP response code (e.g., 200).")
(request--document-response request-response-history
"Redirection history (a list of response object).
The first element is the oldest redirection.
You can use restricted portion of functions for the response
objects in the history slot. It also depends on backend. Here
is the table showing what functions you can use for the response
objects in the history slot.
==================================== ============== ==============
Slots Backends
------------------------------------ -----------------------------
\\ curl url-retrieve
==================================== ============== ==============
request-response-url yes yes
request-response-header yes no
other functions no no
==================================== ============== ==============
")
(request--document-response request-response-data
"Response parsed by the given parser.")
(request--document-response request-response-error-thrown
"Error thrown during request.
It takes the form of ``(ERROR-SYMBOL . DATA)``, which can be
re-raised (`signal'ed) by ``(signal ERROR-SYMBOL DATA)``.")
(request--document-response request-response-symbol-status
"A symbol representing the status of request (not HTTP response code).
One of success/error/timeout/abort/parse-error.")
(request--document-response request-response-url
"Final URL location of response.")
(request--document-response request-response-done-p
"Return t when the request is finished or aborted.")
(request--document-response request-response-settings
"Keyword arguments passed to `request' function.
Some arguments such as HEADERS is changed to the one actually
passed to the backend. Also, it has additional keywords such
as URL which is the requested URL.")
(defun request-response-header (response field-name)
"Fetch the values of RESPONSE header field named FIELD-NAME.
It returns comma separated values when the header has multiple
field with the same name, as :RFC:`2616` specifies.
Examples::
(request-response-header response
\"content-type\") ; => \"text/html; charset=utf-8\"
(request-response-header response
\"unknown-field\") ; => nil
"
(let ((raw-header (request-response--raw-header response)))
(when raw-header
(with-temp-buffer
(erase-buffer)
(insert raw-header)
;; ALL=t to fetch all fields with the same name to get comma
;; separated value [#rfc2616-sec4]_.
(mail-fetch-field field-name nil t)))))
;; .. [#rfc2616-sec4] RFC2616 says this is the right thing to do
;; (see http://tools.ietf.org/html/rfc2616.html#section-4.2).
;; Python's requests module does this too.
;;; Backend dispatcher
(defconst request--backend-alist
'((url-retrieve
. ((request . request--url-retrieve)
(request-sync . request--url-retrieve-sync)
(terminate-process . delete-process)
(get-cookies . request--url-retrieve-get-cookies)))
(curl
. ((request . request--curl)
(request-sync . request--curl-sync)
(terminate-process . interrupt-process)
(get-cookies . request--curl-get-cookies))))
"Map backend and method name to actual method (symbol).
It's alist of alist, of the following form::
((BACKEND . ((METHOD . FUNCTION) ...)) ...)
It would be nicer if I can use EIEIO. But as CEDET is included
in Emacs by 23.2, using EIEIO means abandon older Emacs versions.
It is probably necessary if I need to support more backends. But
let's stick to manual dispatch for now.")
;; See: (view-emacs-news "23.2")
(defun request--choose-backend (method)
"Return `fucall'able object for METHOD of current `request-backend'."
(assoc-default
method
(or (assoc-default request-backend request--backend-alist)
(error "%S is not valid `request-backend'." request-backend))))
;;; Cookie
(defun request-cookie-string (host &optional localpart secure)
"Return cookie string (like `document.cookie').
Example::
(request-cookie-string \"127.0.0.1\" \"/\") ; => \"key=value; key2=value2\"
"
(mapconcat (lambda (nv) (concat (car nv) "=" (cdr nv)))
(request-cookie-alist host localpart secure)
"; "))
(defun request-cookie-alist (host &optional localpart secure)
"Return cookies as an alist.
Example::
(request-cookie-alist \"127.0.0.1\" \"/\") ; => ((\"key\" . \"value\") ...)
"
(funcall (request--choose-backend 'get-cookies) host localpart secure))
;;; Main
(cl-defun request-default-error-callback (url &key symbol-status
&allow-other-keys)
(request-log 'error
"Error (%s) while connecting to %s." symbol-status url))
(cl-defun request (url &rest settings
&key
(type "GET")
(params nil)
(data nil)
(files nil)
(parser nil)
(headers nil)
(success nil)
(error nil)
(complete nil)
(timeout request-timeout)
(status-code nil)
(sync nil)
(response (make-request-response))
(unix-socket nil))
"Send request to URL.
Request.el has a single entry point. It is `request'.
==================== ========================================================
Keyword argument Explanation
==================== ========================================================
TYPE (string) type of request to make: POST/GET/PUT/DELETE
PARAMS (alist) set \"?key=val\" part in URL
DATA (string/alist) data to be sent to the server
FILES (alist) files to be sent to the server (see below)
PARSER (symbol) a function that reads current buffer and return data
HEADERS (alist) additional headers to send with the request
SUCCESS (function) called on success
ERROR (function) called on error
COMPLETE (function) called on both success and error
TIMEOUT (number) timeout in second
STATUS-CODE (alist) map status code (int) to callback
SYNC (bool) If `t', wait until request is done. Default is `nil'.
==================== ========================================================
* Callback functions
Callback functions STATUS, ERROR, COMPLETE and `cdr's in element of
the alist STATUS-CODE take same keyword arguments listed below. For
forward compatibility, these functions must ignore unused keyword
arguments (i.e., it's better to use `&allow-other-keys' [#]_).::
(CALLBACK ; SUCCESS/ERROR/COMPLETE/STATUS-CODE
:data data ; whatever PARSER function returns, or nil
:error-thrown error-thrown ; (ERROR-SYMBOL . DATA), or nil
:symbol-status symbol-status ; success/error/timeout/abort/parse-error
:response response ; request-response object
...)
.. [#] `&allow-other-keys' is a special \"markers\" available in macros
in the CL library for function definition such as `cl-defun' and
`cl-function'. Without this marker, you need to specify all arguments
to be passed. This becomes problem when request.el adds new arguments
when calling callback functions. If you use `&allow-other-keys'
(or manually ignore other arguments), your code is free from this
problem. See info node `(cl) Argument Lists' for more information.
Arguments data, error-thrown, symbol-status can be accessed by
`request-response-data', `request-response-error-thrown',
`request-response-symbol-status' accessors, i.e.::
(request-response-data RESPONSE) ; same as data
Response object holds other information which can be accessed by
the following accessors:
`request-response-status-code',
`request-response-url' and
`request-response-settings'
* STATUS-CODE callback
STATUS-CODE is an alist of the following format::
((N-1 . CALLBACK-1)
(N-2 . CALLBACK-2)
...)
Here, N-1, N-2,... are integer status codes such as 200.
* FILES
FILES is an alist of the following format::
((NAME-1 . FILE-1)
(NAME-2 . FILE-2)
...)
where FILE-N is a list of the form::
(FILENAME &key PATH BUFFER STRING MIME-TYPE)
FILE-N can also be a string (path to the file) or a buffer object.
In that case, FILENAME is set to the file name or buffer name.
Example FILES argument::
`((\"passwd\" . \"/etc/passwd\") ; filename = passwd
(\"scratch\" . ,(get-buffer \"*scratch*\")) ; filename = *scratch*
(\"passwd2\" . (\"password.txt\" :file \"/etc/passwd\"))
(\"scratch2\" . (\"scratch.txt\" :buffer ,(get-buffer \"*scratch*\")))
(\"data\" . (\"data.csv\" :data \"1,2,3\\n4,5,6\\n\")))
.. note:: FILES is implemented only for curl backend for now.
As furl.el_ supports multipart POST, it should be possible to
support FILES in pure elisp by making furl.el_ another backend.
Contributions are welcome.
.. _furl.el: http://code.google.com/p/furl-el/
* PARSER function
PARSER function takes no argument and it is executed in the
buffer with HTTP response body. The current position in the HTTP
response buffer is at the beginning of the buffer. As the HTTP
header is stripped off, the cursor is actually at the beginning
of the response body. So, for example, you can pass `json-read'
to parse JSON object in the buffer. To fetch whole response as a
string, pass `buffer-string'.
When using `json-read', it is useful to know that the returned
type can be modified by `json-object-type', `json-array-type',
`json-key-type', `json-false' and `json-null'. See docstring of
each function for what it does. For example, to convert JSON
objects to plist instead of alist, wrap `json-read' by `lambda'
like this.::
(request
\"http://...\"
:parser (lambda ()
(let ((json-object-type 'plist))
(json-read)))
...)
This is analogous to the `dataType' argument of jQuery.ajax_.
Only this function can access to the process buffer, which
is killed immediately after the execution of this function.
* SYNC
Synchronous request is functional, but *please* don't use it
other than testing or debugging. Emacs users have better things
to do rather than waiting for HTTP request. If you want a better
way to write callback chains, use `request-deferred'.
If you can't avoid using it (e.g., you are inside of some hook
which must return some value), make sure to set TIMEOUT to
relatively small value.
Due to limitation of `url-retrieve-synchronously', response slots
`request-response-error-thrown', `request-response-history' and
`request-response-url' are unknown (always `nil') when using
synchronous request with `url-retrieve' backend.
* Note
API of `request' is somewhat mixture of jQuery.ajax_ (Javascript)
and requests.request_ (Python).
.. _jQuery.ajax: http://api.jquery.com/jQuery.ajax/
.. _requests.request: http://docs.python-requests.org
"
(request-log 'debug "REQUEST")
;; FIXME: support CACHE argument (if possible)
;; (unless cache
;; (setq url (request--url-no-cache url)))
(unless error
(setq error (apply-partially #'request-default-error-callback url))
(setq settings (plist-put settings :error error)))
(unless (or (stringp data)
(null data)
(assoc-string "Content-Type" headers t))
(setq data (request--urlencode-alist data))
(setq settings (plist-put settings :data data)))
(when params
(cl-assert (listp params) nil "PARAMS must be an alist. Given: %S" params)
(setq url (concat url (if (string-match-p "\\?" url) "&" "?")
(request--urlencode-alist params))))
(setq settings (plist-put settings :url url))
(setq settings (plist-put settings :response response))
(setf (request-response-settings response) settings)
(setf (request-response-url response) url)
(setf (request-response--backend response) request-backend)
;; Call `request--url-retrieve'(`-sync') or `request--curl'(`-sync').
(apply (if sync
(request--choose-backend 'request-sync)
(request--choose-backend 'request))
url settings)
(when timeout
(request-log 'debug "Start timer: timeout=%s sec" timeout)
(setf (request-response--timer response)
(run-at-time timeout nil
#'request-response--timeout-callback response)))
response)
(defun request--clean-header (response)
"Strip off carriage returns in the header of REQUEST."
(request-log 'debug "-CLEAN-HEADER")
(let ((buffer (request-response--buffer response))
(backend (request-response--backend response))
sep-regexp)
(if (eq backend 'url-retrieve)
;; FIXME: make this workaround optional.
;; But it looks like sometimes `url-http-clean-headers'
;; fails to cleanup. So, let's be bit permissive here...
(setq sep-regexp "^\r?$")
(setq sep-regexp "^\r$"))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(request-log 'trace
"(buffer-string) at %S =\n%s" buffer (buffer-string))
(goto-char (point-min))
(when (and (re-search-forward sep-regexp nil t)
;; Are \r characters stripped off already?:
(not (equal (match-string 0) "")))
(while (re-search-backward "\r$" (point-min) t)
(replace-match "")))))))
(defun request--cut-header (response)
"Cut the first header part in the buffer of RESPONSE and move it to
raw-header slot."
(request-log 'debug "-CUT-HEADER")
(let ((buffer (request-response--buffer response)))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(goto-char (point-min))
(when (re-search-forward "^$" nil t)
(setf (request-response--raw-header response)
(buffer-substring (point-min) (point)))
(delete-region (point-min) (min (1+ (point)) (point-max))))))))
(defun request-untrampify-filename (file)
"Return FILE as the local file name."
(or (file-remote-p file 'localname) file))
(defun request--parse-data (response parser)
"Run PARSER in current buffer if ERROR-THROWN is nil,
then kill the current buffer."
(request-log 'debug "-PARSE-DATA")
(let ((buffer (request-response--buffer response)))
(request-log 'debug "parser = %s" parser)
(when (and (buffer-live-p buffer) parser)
(with-current-buffer buffer
(request-log 'trace
"(buffer-string) at %S =\n%s" buffer (buffer-string))
(when (/= (request-response-status-code response) 204)
(goto-char (point-min))
(setf (request-response-data response) (funcall parser)))))))
(cl-defun request--callback (buffer &key parser success error complete
timeout status-code response
&allow-other-keys)
(request-log 'debug "REQUEST--CALLBACK")
(request-log 'debug "(buffer-string) =\n%s"
(when (buffer-live-p buffer)
(with-current-buffer buffer (buffer-string))))
;; Sometimes BUFFER given as the argument is different from the
;; buffer already set in RESPONSE. That's why it is reset here.
;; FIXME: Refactor how BUFFER is passed around.
(setf (request-response--buffer response) buffer)
(request-response--cancel-timer response)
(cl-symbol-macrolet
((error-thrown (request-response-error-thrown response))
(symbol-status (request-response-symbol-status response))
(data (request-response-data response))
(done-p (request-response-done-p response)))
;; Parse response header
(request--clean-header response)
(request--cut-header response)
;; Note: Try to do this even `error-thrown' is set. For example,
;; timeout error can occur while downloading response body and
;; header is there in that case.
;; Parse response body
(request-log 'debug "error-thrown = %S" error-thrown)
(condition-case err
(request--parse-data response parser)
(error
;; If there was already an error (e.g. server timeout) do not set the
;; status to `parse-error'.
(unless error-thrown
(setq symbol-status 'parse-error)
(setq error-thrown err)
(request-log 'error "Error from parser %S: %S" parser err))))
(kill-buffer buffer)
(request-log 'debug "data = %s" data)
;; Determine `symbol-status'
(unless symbol-status
(setq symbol-status (if error-thrown 'error 'success)))
(request-log 'debug "symbol-status = %s" symbol-status)
;; Call callbacks
(let ((args (list :data data
:symbol-status symbol-status
:error-thrown error-thrown
:response response)))
(let* ((success-p (eq symbol-status 'success))
(cb (if success-p success error))
(name (if success-p "success" "error")))
(when cb
(request-log 'debug "Executing %s callback." name)
(request--safe-apply cb args)))
(let ((cb (cdr (assq (request-response-status-code response)
status-code))))
(when cb
(request-log 'debug "Executing status-code callback.")
(request--safe-apply cb args)))
(when complete
(request-log 'debug "Executing complete callback.")
(request--safe-apply complete args)))
(setq done-p t)
;; Remove temporary files
;; FIXME: Make tempfile cleanup more reliable. It is possible
;; callback is never called.
(request--safe-delete-files (request-response--tempfiles response))))
(cl-defun request-response--timeout-callback (response)
(request-log 'debug "-TIMEOUT-CALLBACK")
(setf (request-response-symbol-status response) 'timeout)
(setf (request-response-error-thrown response) '(error . ("Timeout")))
(let* ((buffer (request-response--buffer response))
(proc (and (buffer-live-p buffer) (get-buffer-process buffer))))
(when proc
;; This will call `request--callback':
(funcall (request--choose-backend 'terminate-process) proc))
(cl-symbol-macrolet ((done-p (request-response-done-p response)))
(unless done-p
;; This code should never be executed. However, it occurs
;; sometimes with `url-retrieve' backend.
;; FIXME: In Emacs 24.3.50 or later, this is always executed in
;; request-get-timeout test. Find out if it is fine.
(request-log 'error "Callback is not called when stopping process! \
Explicitly calling from timer.")
(when (buffer-live-p buffer)
(cl-destructuring-bind (&key code &allow-other-keys)
(with-current-buffer buffer
(goto-char (point-min))
(request--parse-response-at-point))
(setf (request-response-status-code response) code)))
(apply #'request--callback
buffer
(request-response-settings response))
(setq done-p t)))))
(defun request-response--cancel-timer (response)
(request-log 'debug "REQUEST-RESPONSE--CANCEL-TIMER")
(cl-symbol-macrolet ((timer (request-response--timer response)))
(when timer
(cancel-timer timer)
(setq timer nil))))
(defun request-abort (response)
"Abort request for RESPONSE (the object returned by `request').
Note that this function invoke ERROR and COMPLETE callbacks.
Callbacks may not be called immediately but called later when
associated process is exited."
(cl-symbol-macrolet ((buffer (request-response--buffer response))
(symbol-status (request-response-symbol-status response))
(done-p (request-response-done-p response)))
(let ((process (get-buffer-process buffer)))
(unless symbol-status ; should I use done-p here?
(setq symbol-status 'abort)
(setq done-p t)
(when (and
(processp process) ; process can be nil when buffer is killed
(request--process-live-p process))
(funcall (request--choose-backend 'terminate-process) process))))))
;;; Backend: `url-retrieve'
(cl-defun request--url-retrieve-preprocess-settings
(&rest settings &key type data files headers &allow-other-keys)
(when files
(error "`url-retrieve' backend does not support FILES."))
(when (and (equal type "POST")
data
(not (assoc-string "Content-Type" headers t)))
(push '("Content-Type" . "application/x-www-form-urlencoded") headers)
(setq settings (plist-put settings :headers headers)))
settings)
(cl-defun request--url-retrieve (url &rest settings
&key type data timeout response
&allow-other-keys
&aux headers)
(setq settings (apply #'request--url-retrieve-preprocess-settings settings))
(setq headers (plist-get settings :headers))
(let* ((url-request-extra-headers headers)
(url-request-method type)
(url-request-data data)
(buffer (url-retrieve url #'request--url-retrieve-callback
(nconc (list :response response) settings)))
(proc (get-buffer-process buffer)))
(setf (request-response--buffer response) buffer)
(process-put proc :request-response response)
(request-log 'debug "Start querying: %s" url)
(set-process-query-on-exit-flag proc nil)))
(cl-defun request--url-retrieve-callback (status &rest settings
&key response url
&allow-other-keys)
(declare (special url-http-method
url-http-response-status))
(request-log 'debug "-URL-RETRIEVE-CALLBACK")
(request-log 'debug "status = %S" status)
(request-log 'debug "url-http-method = %s" url-http-method)
(request-log 'debug "url-http-response-status = %s" url-http-response-status)
(setf (request-response-status-code response) url-http-response-status)
(let ((redirect (plist-get status :redirect)))
(when redirect
(setf (request-response-url response) redirect)))
;; Construct history slot
(cl-loop for v in
(cl-loop with first = t
with l = nil
for (k v) on status by 'cddr
when (eq k :redirect)
if first
do (setq first nil)
else
do (push v l)
finally do (cons url l))
do (let ((r (make-request-response :-backend 'url-retrieve)))
(setf (request-response-url r) v)
(push r (request-response-history response))))
(cl-symbol-macrolet ((error-thrown (request-response-error-thrown response))
(status-error (plist-get status :error)))
(when (and error-thrown status-error)
(request-log 'warn
"Error %S thrown already but got another error %S from \
`url-retrieve'. Ignoring it..." error-thrown status-error))
(unless error-thrown
(setq error-thrown status-error)))
(apply #'request--callback (current-buffer) settings))
(cl-defun request--url-retrieve-sync (url &rest settings
&key type data timeout response
&allow-other-keys
&aux headers)
(setq settings (apply #'request--url-retrieve-preprocess-settings settings))
(setq headers (plist-get settings :headers))
(let* ((url-request-extra-headers headers)
(url-request-method type)
(url-request-data data)
(buffer (if timeout
(with-timeout
(timeout
(setf (request-response-symbol-status response)
'timeout)
(setf (request-response-done-p response) t)
nil)
(url-retrieve-synchronously url))
(url-retrieve-synchronously url))))
(setf (request-response--buffer response) buffer)
;; It seems there is no way to get redirects and URL here...
(when buffer
;; Fetch HTTP response code
(with-current-buffer buffer
(goto-char (point-min))
(cl-destructuring-bind (&key version code)
(request--parse-response-at-point)
(setf (request-response-status-code response) code)))
;; Parse response body, etc.
(apply #'request--callback buffer settings)))
response)
(defun request--url-retrieve-get-cookies (host localpart secure)
(mapcar
(lambda (c) (cons (url-cookie-name c) (url-cookie-value c)))
(url-cookie-retrieve host localpart secure)))
;;; Backend: curl
(defvar request--curl-cookie-jar nil
"Override what the function `request--curl-cookie-jar' returns.
Currently it is used only for testing.")
(defun request--curl-cookie-jar ()
"Cookie storage for curl backend."
(or request--curl-cookie-jar
(expand-file-name "curl-cookie-jar" request-storage-directory)))
(defconst request--curl-write-out-template
(if (eq system-type 'windows-nt)
"\\n(:num-redirects %{num_redirects} :url-effective %{url_effective})"
"\\n(:num-redirects %{num_redirects} :url-effective \"%{url_effective}\")"))
(defun request--curl-mkdir-for-cookie-jar ()
(ignore-errors
(make-directory (file-name-directory (request--curl-cookie-jar)) t)))
(cl-defun request--curl-command
(url &key type data headers timeout response files* unix-socket
&allow-other-keys
&aux
(cookie-jar (convert-standard-filename
(expand-file-name (request--curl-cookie-jar)))))
(append
(list request-curl "--silent" "--include"
"--location"
;; FIXME: test automatic decompression
"--compressed"
;; FIMXE: this way of using cookie might be problem when
;; running multiple requests.
"--cookie" cookie-jar "--cookie-jar" cookie-jar
"--write-out" request--curl-write-out-template)
request-curl-options
(when unix-socket (list "--unix-socket" unix-socket))
(cl-loop for (name filename path mime-type) in files*
collect "--form"
collect (format "%s=@%s;filename=%s%s" name
(request-untrampify-filename path) filename
(if mime-type
(format ";type=%s" mime-type)
"")))
(when data
(let ((tempfile (request--make-temp-file)))
(push tempfile (request-response--tempfiles response))
(let ((file-coding-system-alist nil)
(coding-system-for-write 'binary))
(with-temp-file tempfile
(setq buffer-file-coding-system 'binary)
(set-buffer-multibyte nil)
(insert data)))
(list "--data-binary" (concat "@" (request-untrampify-filename tempfile)))))
(when type (list "--request" type))
(cl-loop for (k . v) in headers
collect "--header"
collect (format "%s: %s" k v))
(list url)))
(defun request--curl-normalize-files-1 (files get-temp-file)
(cl-loop for (name . item) in files
collect
(cl-destructuring-bind
(filename &key file buffer data mime-type)
(cond
((stringp item) (list (file-name-nondirectory item) :file item))
((bufferp item) (list (buffer-name item) :buffer item))
(t item))
(unless (= (cl-loop for v in (list file buffer data) if v sum 1) 1)
(error "Only one of :file/:buffer/:data must be given. Got: %S"
(cons name item)))
(cond
(file
(list name filename file mime-type))
(buffer
(let ((tf (funcall get-temp-file)))
(with-current-buffer buffer
(write-region (point-min) (point-max) tf nil 'silent))
(list name filename tf mime-type)))
(data
(let ((tf (funcall get-temp-file)))
(with-temp-buffer
(erase-buffer)
(insert data)
(write-region (point-min) (point-max) tf nil 'silent))
(list name filename tf mime-type)))))))
(defun request--make-temp-file ()
"Create a temporary file."
(if (file-remote-p default-directory)
(let ((tramp-temp-name-prefix request-temp-prefix)
(vec (tramp-dissect-file-name default-directory)))
(tramp-make-tramp-file-name
(tramp-file-name-method vec)
(tramp-file-name-user vec)
(tramp-file-name-host vec)
(tramp-make-tramp-temp-file vec)
(tramp-file-name-hop vec)))
(make-temp-file request-temp-prefix)))
(defun request--curl-normalize-files (files)
"Change FILES into a list of (NAME FILENAME PATH MIME-TYPE).
This is to make `request--curl-command' cleaner by converting
FILES to a homogeneous list. It returns a list (FILES* TEMPFILES)
where FILES* is a converted FILES and TEMPFILES is a list of
temporary file paths."
(let (tempfiles noerror)
(unwind-protect
(let* ((get-temp-file (lambda ()
(let ((tf (request--make-temp-file)))
(push tf tempfiles)
tf)))
(files* (request--curl-normalize-files-1 files get-temp-file)))
(setq noerror t)
(list files* tempfiles))
(unless noerror
;; Remove temporary files only when an error occurs
(request--safe-delete-files tempfiles)))))
(defun request--safe-delete-files (files)
"Remove FILES but do not raise error when failed to do so."
(mapc (lambda (f) (condition-case err
(delete-file f)
(error (request-log 'error
"Failed delete file %s. Got: %S" f err))))
files))
(cl-defun request--curl (url &rest settings
&key type data files headers timeout response
&allow-other-keys)
"cURL-based request backend.
Redirection handling strategy
-----------------------------
curl follows redirection when --location is given. However,
all headers are printed when it is used with --include option.
Number of redirects is printed out sexp-based message using
--write-out option (see `request--curl-write-out-template').
This number is used for removing extra headers and parse
location header from the last redirection header.
Sexp at the end of buffer and extra headers for redirects are
removed from the buffer before it is shown to the parser function.
"
(request--curl-mkdir-for-cookie-jar)
(let* (;; Use pipe instead of pty. Otherwise, curl process hangs.
(process-connection-type nil)
;; Avoid starting program in non-existing directory.
(home-directory (if (file-remote-p default-directory)
(let ((vec (tramp-dissect-file-name default-directory)))
(tramp-make-tramp-file-name
(tramp-file-name-method vec)
(tramp-file-name-user vec)
(tramp-file-name-host vec)
"~/"
(tramp-file-name-hop vec)))
"~/"))
(default-directory (expand-file-name home-directory))
(buffer (generate-new-buffer " *request curl*"))
(command (cl-destructuring-bind
(files* tempfiles)
(request--curl-normalize-files files)
(setf (request-response--tempfiles response) tempfiles)
(apply #'request--curl-command url :files* files*
:response response settings)))
(proc (apply #'start-file-process "request curl" buffer command)))
(request-log 'debug "Run: %s" (mapconcat 'identity command " "))
(setf (request-response--buffer response) buffer)
(process-put proc :request-response response)
(set-process-coding-system proc 'binary 'binary)
(set-process-query-on-exit-flag proc nil)
(set-process-sentinel proc #'request--curl-callback)))
(defun request--curl-read-and-delete-tail-info ()
"Read a sexp at the end of buffer and remove it and preceding character.
This function moves the point at the end of buffer by side effect.
See also `request--curl-write-out-template'."
(let (forward-sexp-function)
(goto-char (point-max))
(forward-sexp -1)
(let ((beg (1- (point))))
(prog1
(read (current-buffer))
(delete-region beg (point-max))))))
(defconst request--cookie-reserved-re
(mapconcat
(lambda (x) (concat "\\(^" x "\\'\\)"))
'("comment" "commenturl" "discard" "domain" "max-age" "path" "port"
"secure" "version" "expires")
"\\|")
"Uninterested keys in cookie.
See \"set-cookie-av\" in http://www.ietf.org/rfc/rfc2965.txt")
(defun request--consume-100-continue ()
"Remove \"HTTP/* 100 Continue\" header at the point."
(cl-destructuring-bind (&key code &allow-other-keys)
(save-excursion (request--parse-response-at-point))
(when (equal code 100)
(delete-region (point) (progn (request--goto-next-body) (point)))
;; FIXME: Does this make sense? Is it possible to have multiple 100?
(request--consume-100-continue))))
(defun request--consume-200-connection-established ()
"Remove \"HTTP/* 200 Connection established\" header at the point."
(when (looking-at-p "HTTP/1\\.[0-1] 200 Connection established")
(delete-region (point) (progn (request--goto-next-body) (point)))))
(defun request--curl-preprocess ()
"Pre-process current buffer before showing it to user."
(let (history)
(cl-destructuring-bind (&key num-redirects url-effective)
(request--curl-read-and-delete-tail-info)
(goto-char (point-min))
(request--consume-100-continue)
(request--consume-200-connection-established)
(when (> num-redirects 0)
(cl-loop with case-fold-search = t
repeat num-redirects
;; Do not store code=100 headers:
do (request--consume-100-continue)
do (let ((response (make-request-response
:-buffer (current-buffer)
:-backend 'curl)))
(request--clean-header response)
(request--cut-header response)
(push response history))))
(goto-char (point-min))
(nconc (list :num-redirects num-redirects :url-effective url-effective
:history (nreverse history))
(request--parse-response-at-point)))))
(defun request--curl-absolutify-redirects (start-url redirects)
"Convert relative paths in REDIRECTS to absolute URLs.
START-URL is the URL requested."
(cl-loop for prev-url = start-url then url
for url in redirects
unless (string-match url-nonrelative-link url)
do (setq url (url-expand-file-name url prev-url))
collect url))
(defun request--curl-absolutify-location-history (start-url history)
"Convert relative paths in HISTORY to absolute URLs.
START-URL is the URL requested."
(when history
(setf (request-response-url (car history)) start-url))
(cl-loop for url in (request--curl-absolutify-redirects
start-url
(mapcar (lambda (response)
(request-response-header response "location"))
history))
for response in (cdr history)
do (setf (request-response-url response) url)))
(defun request--curl-callback (proc event)
(let* ((buffer (process-buffer proc))
(response (process-get proc :request-response))
(symbol-status (request-response-symbol-status response))
(settings (request-response-settings response)))
(request-log 'debug "REQUEST--CURL-CALLBACK event = %s" event)
(request-log 'debug "REQUEST--CURL-CALLBACK proc = %S" proc)
(request-log 'debug "REQUEST--CURL-CALLBACK buffer = %S" buffer)
(request-log 'debug "REQUEST--CURL-CALLBACK symbol-status = %S"
symbol-status)
(cond
((and (memq (process-status proc) '(exit signal))
(/= (process-exit-status proc) 0))
(setf (request-response-error-thrown response) (cons 'error event))
(apply #'request--callback buffer settings))
((equal event "finished\n")
(cl-destructuring-bind (&key version code num-redirects history error
url-effective)
(condition-case err
(with-current-buffer buffer
(request--curl-preprocess))
((debug error)
(list :error err)))
(request--curl-absolutify-location-history (plist-get settings :url)
history)
(setf (request-response-status-code response) code)
(setf (request-response-url response) url-effective)
(setf (request-response-history response) history)
(setf (request-response-error-thrown response)
(or error (when (>= code 400) `(error . (http ,code)))))
(apply #'request--callback buffer settings))))))
(cl-defun request--curl-sync (url &rest settings &key response &allow-other-keys)
;; To make timeout work, use polling approach rather than using
;; `call-process'.
(let (finished)
(prog1 (apply #'request--curl url
:complete (lambda (&rest _) (setq finished t))
settings)
(let ((proc (get-buffer-process (request-response--buffer response))))
(while (and (not finished) (request--process-live-p proc))
(accept-process-output proc))))))
(defun request--curl-get-cookies (host localpart secure)
(request--netscape-get-cookies (request--curl-cookie-jar)
host localpart secure))
;;; Netscape cookie.txt parser
(defun request--netscape-cookie-parse ()
"Parse Netscape/Mozilla cookie format."
(goto-char (point-min))
(let ((tsv-re (concat "^\\(#HttpOnly_\\)?"
(cl-loop repeat 6 concat "\\([^\t\n]+\\)\t")
"\\(.*\\)"))
cookies)
(while (not (eobp))
;; HttpOnly cookie starts with '#' but its line is not comment line(#60)
(cond ((and (looking-at-p "^#") (not (looking-at-p "^#HttpOnly_"))) t)
((looking-at-p "^$") t)
((looking-at tsv-re)
(let ((cookie (cl-loop for i from 1 to 8 collect (match-string i))))
(push cookie cookies))))
(forward-line 1))
(setq cookies (nreverse cookies))
(cl-loop for (http-only domain flag path secure expiration name value) in cookies
collect (list domain
(equal flag "TRUE")
path
(equal secure "TRUE")
(null (not http-only))
(string-to-number expiration)
name
value))))
(defun request--netscape-filter-cookies (cookies host localpart secure)
(cl-loop for (domain flag path secure-1 http-only expiration name value) in cookies
when (and (equal domain host)
(equal path localpart)
(or secure (not secure-1)))
collect (cons name value)))
(defun request--netscape-get-cookies (filename host localpart secure)
(when (file-readable-p filename)
(with-temp-buffer
(erase-buffer)
(insert-file-contents filename)
(request--netscape-filter-cookies (request--netscape-cookie-parse)
host localpart secure))))
(provide 'request)
;;; request.el ends here