PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.15
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.15
6.2.15 6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 All 197 releases
← All changes | app/Services/Submission/SubmissionService.php +51 -10 6.2.13 → 6.2.15 View file →
@@ -15,8 +15,9 @@
15 15 use FluentForm\Framework\Support\Collection;
16 16 use FluentForm\App\Services\Form\FormService;
17 17 use FluentForm\App\Modules\Form\FormDataParser;
18 18 use FluentForm\App\Modules\Form\FormFieldsParser;
19 +use FluentForm\App\Services\Manager\FormManagerService;
19 20
20 21 class SubmissionService
21 22 {
22 23 /**
@@ -289,8 +290,20 @@
289 290 $submissionId = intval(Arr::get($attributes, 'entry_id'));
290 291
291 292 $status = sanitize_text_field(Arr::get($attributes, 'status'));
292 293
294 + $submission = $this->model->find($submissionId);
295 +
296 + if (!$submission) {
297 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
298 + throw new Exception(__('Submission not found', 'fluentform'));
299 + }
300 +
301 + if (!isset(Helper::getMutableEntryStatuses($submission->form_id, $submissionId)[$status])) {
302 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
303 + throw new Exception(__('Invalid entry status', 'fluentform'));
304 + }
305 +
293 306 $this->model->amend($submissionId, ['status' => $status]);
294 307
295 308 do_action('fluentform/after_submission_status_update', $submissionId, $status);
296 309
@@ -347,15 +360,26 @@
347 360 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
348 361 throw new Exception(__('Please select entries first', 'fluentform'));
349 362 }
350 363
364 + // Re-verify against the form actually mutated; the policy scopes on a request entry_id.
365 + if (!FormManagerService::hasFormPermission($formId)) {
366 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
367 + throw new Exception(__('You do not have permission to modify this form\'s entries.', 'fluentform'));
368 + }
369 +
351 370 $query = $this->model->where('form_id', $formId)->whereIn('id', $submissionIds);
352 371
353 - $statuses = Helper::getEntryStatuses($formId);
372 + $statuses = Helper::getMutableEntryStatuses($formId);
354 373
355 374 $message = '';
356 375
357 376 if (isset($statuses[$actionType])) {
377 + // Hook only ids this form owns; array_flip keeps the caller's own id values and order.
378 + $ownedIds = array_flip(
379 + $this->model->where('form_id', $formId)->whereIn('id', $submissionIds)->pluck('id')->all()
380 + );
381 +
358 382 $query->update([
359 383 'status' => $actionType,
360 384 'updated_at' => current_time('mysql'),
361 385 ]);
@@ -360,9 +384,11 @@
360 384 'updated_at' => current_time('mysql'),
361 385 ]);
362 386
363 387 foreach ($submissionIds as $submissionId) {
364 - do_action('fluentform/after_submission_status_update', $submissionId, $actionType);
388 + if (isset($ownedIds[$submissionId])) {
389 + do_action('fluentform/after_submission_status_update', $submissionId, $actionType);
390 + }
365 391 }
366 392
367 393 $message = 'Selected entries successfully marked as ' . $statuses[$actionType];
368 394 } elseif ('other.delete_permanently' == $actionType) {
@@ -446,10 +472,18 @@
446 472 if ($shouldDelete) {
447 473 $deletables = $this->getAttachments($submissionIds, $formId);
448 474
449 475 foreach ($deletables as $file) {
450 - $file = wp_upload_dir()['basedir'] . FLUENTFORM_UPLOAD_DIR . '/' . basename($file);
476 + $fileName = basename($file);
451 477
478 + // Plugin uploads are ff-prefixed, so guard files and dot-files can only be crafted.
479 + if ('' === $fileName || '.' === $fileName[0]
480 + || in_array(strtolower($fileName), ['index.php', 'web.config'], true)) {
481 + continue;
482 + }
483 +
484 + $file = wp_upload_dir()['basedir'] . FLUENTFORM_UPLOAD_DIR . '/' . $fileName;
485 +
452 486 if (is_readable($file) && !is_dir($file)) {
453 487 wp_delete_file($file);
454 488 }
455 489 }
@@ -457,10 +491,12 @@
457 491 if (defined('FLUENTFORMPRO')) {
458 492 $tempDir = wp_upload_dir()['basedir'] . FLUENTFORM_UPLOAD_DIR . '/temp/';
459 493 $files = glob($tempDir . '*');
460 494 if(!empty($files)){
495 + // Shared across forms/visitors: only aged files, never an in-flight upload.
496 + $cutoff = time() - apply_filters('fluentform/temp_file_delete_time', 172800);
461 497 foreach ($files as $file) {
462 - if (basename($file) !== 'index.php') {
498 + if (basename($file) !== 'index.php' && is_file($file) && filemtime($file) < $cutoff) {
463 499 wp_delete_file($file);
464 500 }
465 501 }
466 502 }
@@ -669,16 +705,21 @@
669 705 );
670 706
671 707 do_action('fluentform/submission_user_changed', $submission, $user);
672 708
709 + // Email is roster PII; gate it on list_users (permalink self-gates). FF-SEC-45.
710 + $responseUser = [
711 + 'name' => $user->display_name,
712 + 'ID' => $user->ID,
713 + 'permalink' => get_edit_user_link($user->ID),
714 + ];
715 + if (current_user_can('list_users')) {
716 + $responseUser['email'] = $user->user_email;
717 + }
718 +
673 719 return ([
674 720 'message' => __('Selected user has been successfully assigned to this submission', 'fluentform'),
675 - 'user' => [
676 - 'name' => $user->display_name,
677 - 'email' => $user->user_email,
678 - 'ID' => $user->ID,
679 - 'permalink' => get_edit_user_link($user->ID),
680 - ],
721 + 'user' => $responseUser,
681 722 'user_id' => $userId,
682 723 ]);
683 724 }
684 725