PluginProbe
The GDPR Framework By Data443 / trunk
The GDPR Framework By Data443 vtrunk
2.5.0 2.4.0 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.3 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.39 1.0.4 1.0.40 1.0.41 1.0.42 1.0.43 1.0.44 1.0.45 1.0.46 All 41 releases
gdpr-framework / src / Components / Consent / AdminTabConsent.php

AdminTabConsent.php in The GDPR Framework By Data443 trunk, at src/Components/Consent/AdminTabConsent.php

272 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Codelight\GDPR\Components\Consent;
4
5 if ( ! defined( 'ABSPATH' ) ) exit;
6
7 use Codelight\GDPR\Admin\AdminTab;
8
9 /**
10 * Handle rendering and saving the Consent tab on GDPR Options page
11 *
12 * Class AdminTabConsent
13 * @package Codelight\GDPR\Components\Consent
14 */
15 class AdminTabConsent extends AdminTab
16 {
17 /* @var string */
18 protected $slug = 'consent';
19
20 /* @var ConsentManager */
21 protected $consentManager;
22
23 /**
24 * AdminTabConsent constructor.
25 *
26 * @param ConsentManager $consentManager
27 */
28 public function __construct(ConsentManager $consentManager)
29 {
30 $this->consentManager = $consentManager;
31
32 $this->title = _x('Consent', '(Admin)', 'gdpr-framework');
33
34 // If we don't register the settings, WP will not allow this page to be submitted
35 $this->registerSetting('consent_types');
36 $this->registerSetting('consent_info');
37 $this->registerSetting('gdpr_consent_until_display');
38
39 $this->renderErrors();
40
41 // Register handler for this action
42 add_action('gdpr/admin/action/update_consent_data', [$this, 'updateConsentData']);
43 }
44
45 /**
46 * Initialize tab contents and register hooks
47 */
48 public function init()
49 {
50 $this->registerSettingSection(
51 'gdpr_section_consent',
52 _x('Consent', '(Admin)', 'gdpr-framework'),
53 [$this, 'renderConsentForm']
54 );
55 $this->registerSettingSection(
56 'gdpr_section_consent_until',
57 _x('Additional Settings', '(Admin)', 'gdpr-framework'),
58 [$this, 'renderConsentUntil']
59 );
60 $this->registerSettingField(
61 'gdpr_consent_until_display',
62 _x( 'Display Consent Calendar', '(Admin)', 'gdpr-framework' ),
63 array( $this, 'consent_until_display' ),
64 'gdpr_section_consent_until'
65 );
66 }
67
68 /**
69 * Render the contents of the registered section
70 */
71 public function renderConsentForm()
72 {
73 global $gdpr;
74 $consentInfo = $gdpr->Options->get('consent_info');
75
76 if (is_null($consentInfo)) {
77 $consentInfo = $this->getDefaultConsentInfo();
78 } elseif (!$consentInfo) {
79 $consentInfo = '';
80 }
81
82 $nonce = wp_create_nonce("gdpr/admin/action/update_consent_data");
83 $defaultConsentTypes = $this->consentManager->getDefaultConsentTypes();
84 $customConsentTypes = $this->consentManager->getCustomConsentTypes();
85
86 if (defined('ICL_LANGUAGE_CODE')) {
87 $prefix = ICL_LANGUAGE_CODE . '_';
88 } else {
89 $prefix = '';
90 }
91
92 echo gdpr('view')->render('admin/consent', compact('nonce', 'customConsentTypes', 'defaultConsentTypes', 'consentInfo', 'prefix'));
93 }
94
95 /**
96 * Save the submitted consent types
97 */
98 public function updateConsentData()
99 {
100 global $gdpr;
101 // Update additional information
102 if (isset($_POST['gdpr_consent_info'])) {
103 $gdpr->Options->set('consent_info', wp_unslash($_POST['gdpr_consent_info']));
104 }
105
106 // Update consent types
107 if (isset($_POST['gdpr_consent_types']) && is_array($_POST['gdpr_consent_types'])) {
108 $consentTypes = $_POST['gdpr_consent_types'];
109 } else {
110 $consentTypes = [];
111 }
112
113 // Strip slashes which WP adds automatically
114 if (count($consentTypes)) {
115 foreach ($consentTypes as &$type) {
116 foreach ($type as $key => $item) {
117 if (is_array($item)) {
118 $type[$key] = array_map('wp_unslash', $item);
119 } else {
120 $type[$key] = wp_unslash($item);
121 }
122
123 if ('visible' === $key) {
124 $type[$key] = 1;
125 }
126 }
127
128 // Security fix (SECURITY-AUDIT.md Finding 2): title/description
129 // are plain-text fields in views/admin/consent.php (a text
130 // input and a plain textarea, no rich-text editor) but were
131 // previously stored completely unsanitized, then echoed
132 // unescaped on public consent forms and the admin
133 // data-subject search page -- a stored XSS reachable by
134 // anyone who can save a custom consent type. Sanitize on the
135 // way in as defense in depth alongside escaping on the way out.
136 if (isset($type['title'])) {
137 $type['title'] = sanitize_text_field($type['title']);
138 }
139 if (isset($type['description'])) {
140 $type['description'] = sanitize_textarea_field($type['description']);
141 }
142 }
143 }
144
145 // Fix (PR review Finding 2): drop completely empty repeater/template
146 // rows before validating or saving. The hidden "add consent type"
147 // template can otherwise submit an all-blank row (see the JS fix in
148 // assets/gdpr-admin.js), which would fail validation and block the
149 // whole settings save, and would be persisted as junk if it slipped
150 // through.
151 $consentTypes = array_values(array_filter($consentTypes, function ($type) {
152 return '' !== $this->trimField($type, 'slug')
153 || '' !== $this->trimField($type, 'title')
154 || '' !== $this->trimField($type, 'description');
155 }));
156
157 $errors = [];
158
159 if (!empty($consentTypes)) {
160 // Security fix (SECURITY-AUDIT.md Finding 2): this validation was
161 // disabled, allowing e.g. an empty required title/slug to be saved.
162 $errors = $this->validate($consentTypes);
163 }
164
165 if (!count($errors)) {
166 $this->consentManager->saveCustomConsentTypes($consentTypes);
167 } else {
168 $errorQuery = http_build_query($errors);
169 wp_safe_redirect(gdpr('helpers')->getAdminUrl('&gdpr-tab=consent&') . $errorQuery);
170 exit;
171 }
172 }
173
174 protected function validate($consentTypes)
175 {
176 // Fix (PR review Finding 2): the previous version indexed
177 // $consentType['slug'] directly (undefined-index warning on a partial
178 // row) and reused the single key 'errors[]' for every failure, so each
179 // new error overwrote the last instead of accumulating. Read fields
180 // defensively, tolerate a completely empty template row, and collect
181 // all distinct errors.
182 $errors = [];
183
184 foreach ($consentTypes as $consentType) {
185 $slug = $this->trimField($consentType, 'slug');
186 $title = $this->trimField($consentType, 'title');
187 $description = $this->trimField($consentType, 'description');
188
189 // Ignore a completely empty repeater/template row.
190 if ('' === $slug && '' === $title && '' === $description) {
191 continue;
192 }
193
194 if ('' === $slug) {
195 $errors[] = 'slug-empty';
196 } elseif (!preg_match('/^[A-Za-z0-9_-]+$/', $slug)) {
197 $errors[] = 'slug-invalid';
198 }
199
200 if ('' === $title) {
201 $errors[] = 'title-empty';
202 }
203 }
204
205 $errors = array_values(array_unique($errors));
206
207 return empty($errors) ? [] : ['errors' => $errors];
208 }
209
210 /**
211 * Read a consent-type field as a trimmed string, tolerating a missing key
212 * or a non-scalar value (e.g. an array submitted for a text field).
213 *
214 * @param array $consentType
215 * @param string $key
216 * @return string
217 */
218 protected function trimField($consentType, $key)
219 {
220 if (!isset($consentType[$key]) || !is_scalar($consentType[$key])) {
221 return '';
222 }
223
224 return trim((string) $consentType[$key]);
225 }
226
227 public function renderErrors()
228 {
229 if (isset($_GET['errors']) && count($_GET['errors'])) {
230
231 foreach ($_GET['errors'] as $error) {
232 if ('slug-empty' === $error) {
233 $message = _x("Consent slug is a required field!", '(Admin)', 'gdpr-framework');
234 gdpr('admin-error')->add('admin/notices/error', compact('message'));
235 }
236
237 if ('slug-invalid' === $error) {
238 $message = _x("You may only use alphanumeric characters, dash and underscore in the consent slug field.", '(Admin)', 'gdpr-framework');
239 gdpr('admin-error')->add('admin/notices/error', compact('message'));
240 }
241
242 if ('title-empty' === $error) {
243 $message = _x("Consent title is a required field!", '(Admin)', 'gdpr-framework');
244 gdpr('admin-error')->add('admin/notices/error', compact('message'));
245 }
246 }
247 }
248 }
249
250 /**
251 * @return string
252 */
253 public function getDefaultConsentInfo()
254 {
255 // this is merely a one time initializtion, this string can be changed in the Admin Consent tab and is saved into the database with
256 // the key of gdpr_consent_info
257 return __('To use this website, you accepted our Privacy Policy. If you wish to withdraw your acceptance, please use the "Delete my data" button below.', 'gdpr-framework');
258 }
259 public function renderConsentUntil() {
260 echo '<p>' . __('Enable this feature to allow users to submit a time limit on how many months their consent is given for their coments and registration.', 'gdpr-framework') . '</p>';
261 }
262
263 public function consent_until_display(){
264 if ( get_option( 'gdpr_consent_until_display' ) === '1' ) {
265 $checked = get_option( 'gdpr_consent_until_display' );
266 }else{
267 $checked = 0;
268 }
269 echo gdpr( 'view' )->render( 'admin/consent/enable-consent-until' , compact( 'checked' ) );
270 }
271 }
272