Merge pull request #911 from agh1/membership-dash-counts-new
[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_text',
100 'pay_later_receipt',
101 'label', // This is needed for FROM Email Address configuration. dgg
102 'url', // This is needed for navigation items urls
103 'details',
104 'msg_text', // message templates’ text versions
105 'text_message', // (send an) email to contact’s and CiviMail’s text version
106 'data', // data i/p of persistent table
107 'sqlQuery', // CRM-6673
108 'pcp_title',
109 'pcp_intro_text',
110 'new', // The 'new' text in word replacements
111 );
112 }
113 return self::$skipFields;
114 }
115
116 /**
117 * @param string $fldName
118 * @return bool TRUE if encoding should be skipped for this field
119 */
120 public static function isSkippedField($fldName) {
121 $skipFields = self::getSkipFields();
122
123 // Field should be skipped
124 if (in_array($fldName, $skipFields)) {
125 return TRUE;
126 }
127 // Field is multilingual and after cutting off _xx_YY should be skipped (CRM-7230)…
128 if ((preg_match('/_[a-z][a-z]_[A-Z][A-Z]$/', $fldName) && in_array(substr($fldName, 0, -6), $skipFields))) {
129 return TRUE;
130 }
131 // Field can take multiple entries, eg. fieldName[1], fieldName[2], etc.
132 // We remove the index and check again if the fieldName in the list of skipped fields.
133 $matches = array();
134 if (preg_match('/^(.*)\[\d+\]/', $fldName, $matches) && in_array($matches[1], $skipFields)) {
135 return TRUE;
136 }
137
138 return FALSE;
139 }
140
141 /**
142 * This function is going to filter the
143 * submitted values across XSS vulnerability.
144 *
145 * @param array|string $values
146 * @param bool $castToString If TRUE, all scalars will be filtered (and therefore cast to strings)
147 * If FALSE, then non-string values will be preserved
148 */
149 public static function encodeInput(&$values, $castToString = TRUE) {
150 if (is_array($values)) {
151 foreach ($values as &$value) {
152 self::encodeInput($value);
153 }
154 } elseif ($castToString || is_string($values)) {
155 $values = str_replace(array('<', '>'), array('&lt;', '&gt;'), $values);
156 }
157 }
158
159 public static function decodeOutput(&$values, $castToString = TRUE) {
160 if (is_array($values)) {
161 foreach ($values as &$value) {
162 self::decodeOutput($value);
163 }
164 } elseif ($castToString || is_string($values)) {
165 $values = str_replace(array('&lt;', '&gt;'), array('<', '>'), $values);
166 }
167 }
168
169 /**
170 * {@inheritDoc}
171 */
172 public function fromApiInput($apiRequest) {
173 $lowerAction = strtolower($apiRequest['action']);
174 if ($apiRequest['version'] == 3 && in_array($lowerAction, array('get', 'create'))) {
175 // note: 'getsingle', 'replace', 'update', and chaining all build on top of 'get'/'create'
176 foreach ($apiRequest['params'] as $key => $value) {
177 // Don't apply escaping to API control parameters (e.g. 'api.foo' or 'options.foo')
178 // and don't apply to other skippable fields
179 if (!self::isApiControlField($key) && !self::isSkippedField($key)) {
180 self::encodeInput($apiRequest['params'][$key], FALSE);
181 }
182 }
183 } elseif ($apiRequest['version'] == 3 && $lowerAction == 'setvalue') {
184 if (isset($apiRequest['params']['field']) && isset($apiRequest['params']['value'])) {
185 if (!self::isSkippedField($apiRequest['params']['field'])) {
186 self::encodeInput($apiRequest['params']['value'], FALSE);
187 }
188 }
189 }
190 return $apiRequest;
191 }
192
193 /**
194 * {@inheritDoc}
195 */
196 public function toApiOutput($apiRequest, $result) {
197 $lowerAction = strtolower($apiRequest['action']);
198 if ($apiRequest['version'] == 3 && in_array($lowerAction, array('get', 'create', 'setvalue'))) {
199 foreach ($result as $key => $value) {
200 // Don't apply escaping to API control parameters (e.g. 'api.foo' or 'options.foo')
201 // and don't apply to other skippable fields
202 if (!self::isApiControlField($key) && !self::isSkippedField($key)) {
203 self::decodeOutput($result[$key], FALSE);
204 }
205 }
206 }
207 // setvalue?
208 return $result;
209 }
210
211 /**
212 * @return bool
213 */
214 protected function isApiControlField($key) {
215 return (FALSE !== strpos($key, '.'));
216 }
217 }