org-capture: Prevent recursive evaluation of %(...) placeholders

* lisp/org-capture.el (org-capture-templates): Improve docstring.
(org-capture-fill-template): Prevent recursive evaluation of %(...)
placeholders.  Fix escaping of % character in templates.
* testing/lisp/test-org-capture.el: New file.

Reported-by: Thomas Preindl <thomas.preindl@gmail.com>
<http://permalink.gmane.org/gmane.emacs.orgmode/103018>
This commit is contained in:
Nicolas Goaziou 2015-11-29 16:53:35 +01:00
parent bd79085be0
commit bd3a2cbf2f
3 changed files with 312 additions and 182 deletions

View File

@ -104,6 +104,15 @@ Org files.
The new algorithm doesn't remove TAB characters not used for The new algorithm doesn't remove TAB characters not used for
indentation. indentation.
*** Secure placeholders in capture templates
Placeholders in capture templates are no longer expanded recursively.
However, ~%(...)~ constructs are expanded very late, so you can still
fill the contents of the S-exp with the replacement text of other
placeholders.
Only ~%(...)~ placeholders initially present are expanded. This
prevents evaluating potentially malicious code when another placehold,
e.g., ~%i~ expands to a S-exp.
*** ~org-babel-check-confirm-evaluate~ is now a function instead of a macro *** ~org-babel-check-confirm-evaluate~ is now a function instead of a macro
The calling convention has changed. The calling convention has changed.

View File

@ -219,15 +219,20 @@ properties are:
is finalized. is finalized.
The template defines the text to be inserted. Often this is an The template defines the text to be inserted. Often this is an
org-mode entry (so the first line should start with a star) that Org mode entry (so the first line should start with a star) that
will be filed as a child of the target headline. It can also be will be filed as a child of the target headline. It can also be
freely formatted text. Furthermore, the following %-escapes will freely formatted text. Furthermore, the following %-escapes will
be replaced with content and expanded in this order: be replaced with content and expanded:
%[pathname] Insert the contents of the file given by `pathname'. %[pathname] Insert the contents of the file given by
`pathname'. These placeholders are expanded at the very
beginning of the process so they can be used to extend the
current template.
%(sexp) Evaluate elisp `(sexp)' and replace it with the results. %(sexp) Evaluate elisp `(sexp)' and replace it with the results.
For convenience, %:keyword (see below) placeholders within Only placeholders pre-existing within the template, or
the expression will be expanded prior to this. introduced with %[pathname] are expanded this way. Since this
happens very late in the process, other %-escapes can be used to
fill the expression.
%<...> The result of format-time-string on the ... format specification. %<...> The result of format-time-string on the ... format specification.
%t Time stamp, date only. %t Time stamp, date only.
%T Time stamp with date and time. %T Time stamp with date and time.
@ -256,8 +261,9 @@ be replaced with content and expanded in this order:
A default value and a completion table ca be specified like this: A default value and a completion table ca be specified like this:
%^{prompt|default|completion2|completion3|...}. %^{prompt|default|completion2|completion3|...}.
%? After completing the template, position cursor here. %? After completing the template, position cursor here.
%\\n Insert the text entered at the nth %^{prompt}, where `n' is %number Insert the text entered at the nth %^{prompt}, where `number' is a
a number, starting from 1. number, starting from 1. These placeholders are expanded as the
last step of the process.
Apart from these general escapes, you can access information specific to Apart from these general escapes, you can access information specific to
the link type that is created. For example, calling `org-capture' in emails the link type that is created. For example, calling `org-capture' in emails
@ -277,7 +283,10 @@ gnus | %:from %:fromname %:fromaddress
gnus | %:group, for messages also all email fields gnus | %:group, for messages also all email fields
w3, w3m | %:type %:url w3, w3m | %:type %:url
info | %:type %:file %:node info | %:type %:file %:node
calendar | %:type %:date" calendar | %:type %:date
When you need to insert a literal percent sign in the template,
you can escape ambiguous cases with a backward slash, e.g., \\%i."
:group 'org-capture :group 'org-capture
:version "24.1" :version "24.1"
:type :type
@ -1536,7 +1545,7 @@ The template may still contain \"%?\" for cursor positioning."
(if (< (nth 2 dct) org-extend-today-until) (if (< (nth 2 dct) org-extend-today-until)
(encode-time 0 59 23 (1- (nth 3 dct)) (nth 4 dct) (nth 5 dct)) (encode-time 0 59 23 (1- (nth 3 dct)) (nth 4 dct) (nth 5 dct))
ct)) ct))
(plist-p (if org-store-link-plist t nil)) (plist-p org-store-link-plist)
(v-c (and (> (length kill-ring) 0) (current-kill 0))) (v-c (and (> (length kill-ring) 0) (current-kill 0)))
(v-x (or (org-get-x-clipboard 'PRIMARY) (v-x (or (org-get-x-clipboard 'PRIMARY)
(org-get-x-clipboard 'CLIPBOARD) (org-get-x-clipboard 'CLIPBOARD)
@ -1545,8 +1554,8 @@ The template may still contain \"%?\" for cursor positioning."
(v-T (format-time-string (cdr org-time-stamp-formats) ct1)) (v-T (format-time-string (cdr org-time-stamp-formats) ct1))
(v-u (concat "[" (substring v-t 1 -1) "]")) (v-u (concat "[" (substring v-t 1 -1) "]"))
(v-U (concat "[" (substring v-T 1 -1) "]")) (v-U (concat "[" (substring v-T 1 -1) "]"))
;; `initial' and `annotation' might habe been passed. ;; `initial' and `annotation' might have been passed. But if
;; But if the property list has them, we prefer those values ;; the property list has them, we prefer those values.
(v-i (or (plist-get org-store-link-plist :initial) (v-i (or (plist-get org-store-link-plist :initial)
initial initial
(org-capture-get :initial) (org-capture-get :initial)
@ -1557,7 +1566,7 @@ The template may still contain \"%?\" for cursor positioning."
"")) ""))
;; Is the link empty? Then we do not want it... ;; Is the link empty? Then we do not want it...
(v-a (if (equal v-a "[[]]") "" v-a)) (v-a (if (equal v-a "[[]]") "" v-a))
(clipboards (remove nil (list v-i (clipboards (remq nil (list v-i
(org-get-x-clipboard 'PRIMARY) (org-get-x-clipboard 'PRIMARY)
(org-get-x-clipboard 'CLIPBOARD) (org-get-x-clipboard 'CLIPBOARD)
(org-get-x-clipboard 'SECONDARY) (org-get-x-clipboard 'SECONDARY)
@ -1570,7 +1579,7 @@ The template may still contain \"%?\" for cursor positioning."
(replace-match "\\1" nil nil v-a) (replace-match "\\1" nil nil v-a)
v-a)) v-a))
(v-n user-full-name) (v-n user-full-name)
(v-k (if (marker-buffer org-clock-marker) (v-k (and (marker-buffer org-clock-marker)
(org-no-properties org-clock-heading))) (org-no-properties org-clock-heading)))
(v-K (if (marker-buffer org-clock-marker) (v-K (if (marker-buffer org-clock-marker)
(org-make-link-string (org-make-link-string
@ -1578,30 +1587,32 @@ The template may still contain \"%?\" for cursor positioning."
org-clock-heading))) org-clock-heading)))
(v-f (or (org-capture-get :original-file-nondirectory) "")) (v-f (or (org-capture-get :original-file-nondirectory) ""))
(v-F (or (org-capture-get :original-file) "")) (v-F (or (org-capture-get :original-file) ""))
v-I
(org-startup-folded nil) (org-startup-folded nil)
(org-inhibit-startup t) (org-inhibit-startup t)
org-time-was-given org-end-time-was-given x strings)
prompt completions char time pos default histvar strings)
(setq org-store-link-plist (setq org-store-link-plist (plist-put org-store-link-plist :annotation v-a))
(plist-put org-store-link-plist :annotation v-a) (setq org-store-link-plist (plist-put org-store-link-plist :initial v-i))
org-store-link-plist
(plist-put org-store-link-plist :initial v-i))
(setq initial v-i) (setq initial v-i)
(unless template (setq template "") (message "No template") (ding) (unless template
(setq template "")
(message "no template") (ding)
(sit-for 1)) (sit-for 1))
(save-window-excursion (save-window-excursion
(org-switch-to-buffer-other-window (get-buffer-create "*Capture*")) (org-switch-to-buffer-other-window (get-buffer-create "*Capture*"))
(erase-buffer) (erase-buffer)
;; Turn on org-mode in temp buffer, set local variables. This
;; is to support completion in interactive prompts
(let ((org-inhibit-startup t)) (org-mode))
(insert template) (insert template)
(goto-char (point-min)) (goto-char (point-min))
(org-capture-steal-local-variables buffer) (org-capture-steal-local-variables buffer)
(setq buffer-file-name nil mark-active nil) (setq buffer-file-name nil)
(setq mark-active nil)
;; %[] Insert contents of a file. ;; %[] insert contents of a file.
(goto-char (point-min)) (save-excursion
(while (re-search-forward "%\\[\\(.+\\)\\]" nil t) (while (re-search-forward "%\\[\\(.+\\)\\]" nil t)
(unless (org-capture-escaped-%) (unless (org-capture-escaped-%)
(let ((start (match-beginning 0)) (let ((start (match-beginning 0))
@ -1611,162 +1622,190 @@ The template may still contain \"%?\" for cursor positioning."
(delete-region start end) (delete-region start end)
(condition-case error (condition-case error
(insert-file-contents filename) (insert-file-contents filename)
(error (insert (format "%%![Couldn not insert %s: %s]" (error (insert (format "%%![couldn not insert %s: %s]"
filename error))))))) filename
error))))))))
;; The current time ;; Mark %() embedded elisp for later evaluation.
(goto-char (point-min)) (org-capture--expand-embedded-elisp 'mark)
(while (re-search-forward "%<\\([^>\n]+\\)>" nil t)
(replace-match (format-time-string (match-string 1)) t t))
;; Simple %-escapes (let ((regexp "%\\(:[-a-za-z]+\\|<\\([^>\n]+\\)>\\|[aAcfFikKlntTuUx]\\|\
(goto-char (point-min)) ^\\(?:{\\([^}]*\\)}\\)?\\([CgGLptTuU]\\)?\\)")
(while (re-search-forward "%\\([tTuUaliAcxkKInfF]\\)" nil t) (strings))
(while (re-search-forward regexp nil t)
(let ((pos (copy-marker (match-beginning 0)))
(end (copy-marker (match-end 0)))
(value (match-string 1))
(time-string (match-string 2))
(prompt (match-string-no-properties 3))
(key (match-string 4)))
(unless (org-capture-escaped-%) (unless (org-capture-escaped-%)
(when (and initial (equal (match-string 0) "%i")) (goto-char pos)
(save-match-data (delete-region pos end)
(let* ((lead (buffer-substring (pcase (string-to-char value)
(point-at-bol) (match-beginning 0)))) (?<
(setq v-i (mapconcat 'identity ;; The current time.
(org-split-string initial "\n") (insert (format-time-string time-string)))
(concat "\n" lead)))))) (?:
(replace-match (or (eval (intern (concat "v-" (match-string 1)))) "") ;; From the property list.
t t))) (insert (or (plist-get org-store-link-plist (intern value))
"")))
;; From the property list (?i (let ((lead (buffer-substring-no-properties
(when plist-p (line-beginning-position) pos)))
(goto-char (point-min)) (insert (mapconcat #'identity
(while (re-search-forward "%\\(:[-a-zA-Z]+\\)" nil t) (split-string initial "\n")
(unless (org-capture-escaped-%) (concat "\n" lead)))))
(and (setq x (or (plist-get org-store-link-plist (?^
(intern (match-string 1))) "")) ;; Interactive template entries.
(replace-match x t t))))) (let ((completions nil)
(default nil)
;; %() embedded elisp (histvar nil))
(goto-char (point-min))
(org-capture-expand-embedded-elisp)
;; Turn on org-mode in temp buffer, set local variables
;; This is to support completion in interactive prompts
(let ((org-inhibit-startup t)) (org-mode))
;; Interactive template entries
(goto-char (point-min))
(while (re-search-forward "%^\\({\\([^}]*\\)}\\)?\\([gGtTuUCLp]\\)?" nil t)
(unless (org-capture-escaped-%)
(setq char (if (match-end 3) (match-string-no-properties 3))
prompt (if (match-end 2) (match-string-no-properties 2)))
(goto-char (match-beginning 0))
(replace-match "")
(setq completions nil default nil)
(when prompt (when prompt
(setq completions (org-split-string prompt "|") (setq completions (org-split-string prompt "|"))
prompt (pop completions) (setq prompt (pop completions))
default (car completions) (setq default (car completions))
histvar (intern (concat (setq histvar
(intern (concat
"org-capture-template-prompt-history::" "org-capture-template-prompt-history::"
(or prompt ""))) (or prompt ""))))
completions (mapcar 'list completions))) (setq completions (mapcar #'list completions)))
(unless (boundp histvar) (set histvar nil)) (pcase (string-to-char key)
(cond ((or ?G ?g)
((member char '("G" "g"))
(let* ((org-last-tags-completion-table (let* ((org-last-tags-completion-table
(org-global-tags-completion-table (org-global-tags-completion-table
(if (equal char "G") (cond ((equal key "G") (org-agenda-files))
(org-agenda-files) (file (list file))
(and file (list file))))) (t nil))))
(org-add-colon-after-tag-completion t) (org-add-colon-after-tag-completion t)
(ins (org-icompleting-read (ins (mapconcat
#'identity
(org-split-string
(completing-read
(if prompt (concat prompt ": ") "Tags: ") (if prompt (concat prompt ": ") "Tags: ")
'org-tags-completion-function nil nil nil 'org-tags-completion-function nil nil nil
'org-tags-history))) 'org-tags-history)
(setq ins (mapconcat 'identity "[^[:alnum:]_@#%]+")
(org-split-string ":")))
ins (org-re "[^[:alnum:]_@#%]+")) (when (org-string-nw-p ins)
":")) (unless (eq (char-before) ?:) (insert ":"))
(when (string-match "\\S-" ins)
(or (equal (char-before) ?:) (insert ":"))
(insert ins) (insert ins)
(or (equal (char-after) ?:) (insert ":")) (unless (eq (char-after) ?:) (insert ":"))
(and (org-at-heading-p) (and (org-at-heading-p)
(let ((org-ignore-region t)) (let ((org-ignore-region t))
(org-set-tags nil 'align)))))) (org-set-tags nil 'align))))))
((equal char "C") (?C
(cond ((= (length clipboards) 1) (insert (car clipboards))) (cond
((= (length clipboards) 1) (insert (car clipboards)))
((> (length clipboards) 1) ((> (length clipboards) 1)
(insert (read-string "Clipboard/kill value: " (insert (read-string "Clipboard/kill value: "
(car clipboards) '(clipboards . 1)
(car clipboards))))))
((equal char "L")
(cond ((= (length clipboards) 1)
(org-insert-link 0 (car clipboards)))
((> (length clipboards) 1)
(org-insert-link 0 (read-string "Clipboard/kill value: "
(car clipboards) (car clipboards)
'(clipboards . 1) '(clipboards . 1)
(car clipboards)))))) (car clipboards))))))
((equal char "p") (?L
(org-set-property (org-no-properties prompt) nil)) (cond ((= (length clipboards) 1)
(char (org-insert-link 0 (car clipboards)))
;; These are the date/time related ones ((> (length clipboards) 1)
(setq org-time-was-given (equal (upcase char) char)) (org-insert-link
(setq time (org-read-date (equal (upcase char) char) t nil 0
prompt)) (read-string "Clipboard/kill value: "
(if (equal (upcase char) char) (setq org-time-was-given t)) (car clipboards)
(org-insert-time-stamp time org-time-was-given '(clipboards . 1)
(member char '("u" "U")) (car clipboards))))))
nil nil (list org-end-time-was-given))) (?p (org-set-property prompt nil))
((guard key)
;; These are the date/time related ones.
(let* ((upcase? (equal (upcase key) key))
(org-time-was-given upcase?)
(org-end-time-was-given)
(time (org-read-date upcase? t nil prompt)))
(org-insert-time-stamp
time org-time-was-given
(member key '("u" "U"))
nil nil (list org-end-time-was-given))))
(t (t
(let (org-completion-use-ido) (push (completing-read
(push (org-completing-read-no-i (concat (or prompt "Enter string")
(concat (if prompt prompt "Enter string") (and default (format " [%s]" default))
(if default (concat " [" default "]"))
": ") ": ")
completions nil nil nil histvar default) completions nil nil nil histvar default)
strings) strings)
(insert (car strings))))))) (insert (car strings))))))
;; Replace %n escapes with nth %^{...} string (?a (insert v-a))
(?A (insert v-A))
(?c (insert v-c))
(?f (insert v-f))
(?F (insert v-F))
(?k (insert v-k))
(?K (insert v-K))
(?l (insert v-l))
(?t (insert v-t))
(?T (insert v-T))
(?u (insert v-u))
(?U (insert v-U))
(?x (insert v-x)))
(set-marker pos nil)
(set-marker end nil)))))
;; Replace %n escapes with nth %^{...} string.
(setq strings (nreverse strings)) (setq strings (nreverse strings))
(goto-char (point-min)) (save-excursion
(while (re-search-forward "%\\\\\\([1-9][0-9]*\\)" nil t) (while (re-search-forward "%\\\\\\([1-9][0-9]*\\)" nil t)
(unless (org-capture-escaped-%) (unless (org-capture-escaped-%)
(replace-match (replace-match
(nth (1- (string-to-number (match-string 1))) strings) (nth (1- (string-to-number (match-string 1))) strings)
nil t))) nil t))))
;; Expand %() embedded Elisp. Limit to Sexp originally marked.
(org-capture--expand-embedded-elisp)
;; Make sure there are no empty lines before the text, and that ;; Make sure there are no empty lines before the text, and that
;; it ends with a newline character ;; it ends with a newline character.
(goto-char (point-min)) (goto-char (point-min))
(while (looking-at "[ \t]*\n") (replace-match "")) (while (looking-at "[ \t]*\n") (replace-match ""))
(if (re-search-forward "[ \t\n]*\\'" nil t) (replace-match "\n")) (when (re-search-forward "[ \t\n]*\\'" nil t) (replace-match "\n"))
;; Return the expanded template and kill the temporary buffer ;; Return the expanded template and kill the temporary buffer.
(untabify (point-min) (point-max)) (untabify (point-min) (point-max))
(set-buffer-modified-p nil) (set-buffer-modified-p nil)
(prog1 (buffer-string) (kill-buffer (current-buffer)))))) (prog1 (buffer-substring-no-properties (point-min) (point-max))
(kill-buffer (current-buffer))))))
(defun org-capture-escaped-% () (defun org-capture-escaped-% ()
"Check if % was escaped - if yes, unescape it now." "Non-nil if % was escaped.
(if (equal (char-before (match-beginning 0)) ?\\) If yes, unescape it now. Assume match-data contains the
(progn placeholder to check."
(delete-region (1- (match-beginning 0)) (match-beginning 0)) (save-excursion
t) (goto-char (match-beginning 0))
nil)) (let ((n (abs (skip-chars-backward "\\\\"))))
(delete-char (/ (1+ n) 2))
(= (% n 2) 1))))
(defun org-capture-expand-embedded-elisp () (defun org-capture--expand-embedded-elisp (&optional mark)
"Evaluate embedded elisp %(sexp) and replace with the result." "Evaluate embedded elisp %(sexp) and replace with the result.
When optional MARK argument is non-nil, mark Sexp with a text
property (`org-embedded-elisp') for later evaluation. Only
marked Sexp are evaluated when this argument is nil."
(save-excursion
(goto-char (point-min)) (goto-char (point-min))
(while (re-search-forward "%(" nil t) (while (re-search-forward "%(" nil t)
(unless (org-capture-escaped-%) (unless (org-capture-escaped-%)
(if mark
(put-text-property
(match-beginning 0) (match-end 0) 'org-embedded-elisp t)
(when (get-text-property (match-beginning 0) 'org-embedded-elisp)
(goto-char (match-beginning 0)) (goto-char (match-beginning 0))
(let ((template-start (point))) (let ((template-start (point)))
(forward-char 1) (forward-char 1)
(let* ((sexp (read (current-buffer))) (let* ((sexp (read (current-buffer)))
(result (org-eval (result (org-eval
(org-capture--expand-keyword-in-embedded-elisp sexp)))) (org-capture--expand-keyword-in-embedded-elisp
sexp))))
(delete-region template-start (point)) (delete-region template-start (point))
(when result (when result
(if (stringp result) (if (stringp result)
(insert result) (insert result)
(error "Capture template sexp `%s' must evaluate to string or nil" (error
sexp)))))))) "Capture template sexp `%s' must evaluate to string or nil"
sexp)))))))))))
(defun org-capture--expand-keyword-in-embedded-elisp (attr) (defun org-capture--expand-keyword-in-embedded-elisp (attr)
"Recursively replace capture link keywords in ATTR sexp. "Recursively replace capture link keywords in ATTR sexp.

View File

@ -0,0 +1,82 @@
;;; test-org-capture.el --- Tests for org-capture.el -*- lexical-binding: t; -*-
;; Copyright (C) 2015 Nicolas Goaziou
;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
;; This program 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.
;; 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, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Unit tests for Org Capture library.
;;; Code:
(require 'org-capture)
(ert-deftest test-org-capture/fill-template ()
"Test `org-capture-fill-template' specifications."
;; %(sexp) placeholder.
(should
(equal "success!\n"
(org-capture-fill-template "%(concat \"success\" \"!\")")))
;; %<...> placeholder.
(should
(equal (concat (format-time-string "%Y") "\n")
(org-capture-fill-template "%<%Y>")))
;; %t and %T placeholders.
(should
(equal (concat (format-time-string (car org-time-stamp-formats)) "\n")
(org-capture-fill-template "%t")))
(should
(equal (concat (format-time-string (cdr org-time-stamp-formats)) "\n")
(org-capture-fill-template "%T")))
;; %u and %U placeholders.
(should
(string-match-p
(format-time-string (substring (car org-time-stamp-formats) 1 -1))
(org-capture-fill-template "%u")))
(should
(string-match-p
(format-time-string (substring (cdr org-time-stamp-formats) 1 -1))
(org-capture-fill-template "%U")))
;; %i placeholder. Make sure sexp placeholders are not expanded
;; when they are inserted through this one.
(should
(equal "success!\n"
(let ((org-store-link-plist nil))
(org-capture-fill-template "%i" "success!"))))
(should-not
(equal "failure!\n"
(let ((org-store-link-plist nil))
(org-capture-fill-template "%i" "%(concat \"failure\" \"!\")"))))
;; Test %-escaping with / character.
(should
(equal "%i\n"
(let ((org-store-link-plist nil))
(org-capture-fill-template "\\%i" "success!"))))
(should
(equal "\\success!\n"
(let ((org-store-link-plist nil))
(org-capture-fill-template "\\\\%i" "success!"))))
(should
(equal "\\%i\n"
(let ((org-store-link-plist nil))
(org-capture-fill-template "\\\\\\%i" "success!")))))
(provide 'test-org-capture)
;;; test-org-capture.el ends here