Merge pull request #333 from colemanw/attachments
[civicrm-core.git] / CRM / Core / HTMLInputCoder.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.3 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * This class captures the encoding practices of CRM-5667 in a reusable
30 * fashion. In this design, all submitted values are partially HTML-encoded
31 * before saving to the database. If a DB reader needs to output in
32 * non-HTML medium, then it should undo the partial HTML encoding.
33 *
34 * This class should be short-lived -- 4.3 should introduce an alternative
35 * escaping scheme and consequently remove HTMLInputCoder.
36 *
37 * @package CRM
38 * @copyright CiviCRM LLC (c) 2004-2013
39 * $Id$
40 *
41 */
42
43 require_once 'api/Wrapper.php';
44 class CRM_Core_HTMLInputCoder implements API_Wrapper {
45 private static $skipFields = NULL;
46
47 /**
48 * @var CRM_Core_HTMLInputCoder
49 */
50 private static $_singleton = NULL;
51
52 /**
53 * @return CRM_Core_HTMLInputCoder
54 */
55 public static function singleton() {
56 if (self::$_singleton === NULL) {
57 self::$_singleton = new CRM_Core_HTMLInputCoder();
58 }
59 return self::$_singleton;
60 }
61
62 /**
63 * @return array<string> list of field names
64 */
65 public static function getSkipFields() {
66 if (self::$skipFields === NULL) {
67 self::$skipFields = array(
68 'widget_code',
69 'html_message',
70 'body_html',
71 'msg_html',
72 'description',
73 'intro',
74 'thankyou_text',
75 'tf_thankyou_text',
76 'intro_text',
77 'page_text',
78 'body_text',
79 'footer_text',
80 'thankyou_footer',
81 'thankyou_footer_text',
82 'new_text',
83 'renewal_text',
84 'help_pre',
85 'help_post',
86 'confirm_title',
87 'confirm_text',
88 'confirm_footer_text',
89 'confirm_email_text',
90 'event_full_text',
91 'waitlist_text',
92 'approval_req_text',
93 'report_header',
94 'report_footer',
95 'cc_id',
96 'bcc_id',
97 'premiums_intro_text',
98 'honor_block_text',
99 'pay_later_receipt',
100 'label', // This is needed for FROM Email Address configuration. dgg
101 'url', // This is needed for navigation items urls
102 'details',
103 'msg_text', // message templates’ text versions
104 'text_message', // (send an) email to contact’s and CiviMail’s text version
105 'data', // data i/p of persistent table
106 'sqlQuery', // CRM-6673
107 'pcp_title',
108 'pcp_intro_text',
109 'new', // The 'new' text in word replacements
110 );
111 }
112 return self::$skipFields;
113 }
114
115 /**
116 * @param string $fldName
117 * @return bool TRUE if encoding should be skipped for this field
118 */
119 public static function isSkippedField($fldName) {
120 $skipFields = self::getSkipFields();
121
122 // Field should be skipped
123 if (in_array($fldName, $skipFields)) {
124 return TRUE;
125 }
126 // Field is multilingual and after cutting off _xx_YY should be skipped (CRM-7230)…
127 if ((preg_match('/_[a-z][a-z]_[A-Z][A-Z]$/', $fldName) && in_array(substr($fldName, 0, -6), $skipFields))) {
128 return TRUE;
129 }
130 // Field can take multiple entries, eg. fieldName[1], fieldName[2], etc.
131 // We remove the index and check again if the fieldName in the list of skipped fields.
132 $matches = array();
133 if (preg_match('/^(.*)\[\d+\]/', $fldName, $matches) && in_array($matches[1], $skipFields)) {
134 return TRUE;
135 }
136
137 return FALSE;
138 }
139
140 /**
141 * This function is going to filter the
142 * submitted values across XSS vulnerability.
143 *
144 * @param array|string $values
145 * @param bool $castToString If TRUE, all scalars will be filtered (and therefore cast to strings)
146 * If FALSE, then non-string values will be preserved
147 */
148 public static function encodeInput(&$values, $castToString = TRUE) {
149 if (is_array($values)) {
150 foreach ($values as &$value) {
151 self::encodeInput($value);
152 }
153 } elseif ($castToString || is_string($values)) {
154 $values = str_replace(array('<', '>'), array('&lt;', '&gt;'), $values);
155 }
156 }
157
158 public static function decodeOutput(&$values, $castToString = TRUE) {
159 if (is_array($values)) {
160 foreach ($values as &$value) {
161 self::decodeOutput($value);
162 }
163 } elseif ($castToString || is_string($values)) {
164 $values = str_replace(array('&lt;', '&gt;'), array('<', '>'), $values);
165 }
166 }
167
168 /**
169 * {@inheritDoc}
170 */
171 public function fromApiInput($apiRequest) {
172 $lowerAction = strtolower($apiRequest['action']);
173 if ($apiRequest['version'] == 3 && in_array($lowerAction, array('get', 'create'))) {
174 // note: 'getsingle', 'replace', 'update', and chaining all build on top of 'get'/'create'
175 foreach ($apiRequest['params'] as $key => $value) {
176 // Don't apply escaping to API control parameters (e.g. 'api.foo' or 'options.foo')
177 // and don't apply to other skippable fields
178 if (!self::isApiControlField($key) && !self::isSkippedField($key)) {
179 self::encodeInput($apiRequest['params'][$key], FALSE);
180 }
181 }
182 } elseif ($apiRequest['version'] == 3 && $lowerAction == 'setvalue') {
183 if (isset($apiRequest['params']['field']) && isset($apiRequest['params']['value'])) {
184 if (!self::isSkippedField($apiRequest['params']['field'])) {
185 self::encodeInput($apiRequest['params']['value'], FALSE);
186 }
187 }
188 }
189 return $apiRequest;
190 }
191
192 /**
193 * {@inheritDoc}
194 */
195 public function toApiOutput($apiRequest, $result) {
196 $lowerAction = strtolower($apiRequest['action']);
197 if ($apiRequest['version'] == 3 && in_array($lowerAction, array('get', 'create', 'setvalue'))) {
198 foreach ($result as $key => $value) {
199 // Don't apply escaping to API control parameters (e.g. 'api.foo' or 'options.foo')
200 // and don't apply to other skippable fields
201 if (!self::isApiControlField($key) && !self::isSkippedField($key)) {
202 self::decodeOutput($result[$key], FALSE);
203 }
204 }
205 }
206 // setvalue?
207 return $result;
208 }
209
210 /**
211 * @return bool
212 */
213 protected function isApiControlField($key) {
214 return (FALSE !== strpos($key, '.'));
215 }
216 }