diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 69eb2a8..f15b43b 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,9 @@
+1.3.47 (2026-06-26)
+ - fix: remove double-escaping in evalform.php (add_slashes + mpform_escape_string)
+ - fix: remove add_slashes() before htmlspecialchars() in save_field.php
+ - fix: case 'date'; -> case 'date': in modify.php (deprecated PHP 8.5)
+ - fix: email field value obfuscated by output filter on form re-display (@ and . encoded as HTML entities in session)
+
mpForm wbCMS Content Module
###########################
diff --git a/evalform.php b/evalform.php
index 78b96fe..145a5c3 100644
--- a/evalform.php
+++ b/evalform.php
@@ -538,13 +538,22 @@ function eval_form($section_id) {
);
} else {
// make sure user does see what he entered:
+ $session_val = htmlspecialchars(
+ stripslashes($post_field) ?? '', ENT_QUOTES);
+ // protect @ and . from output filter obfuscation in email fields
+ if($field['type'] == 'email') {
+ $session_val = str_replace(
+ array('@', '.'),
+ array('@', '.'),
+ $session_val
+ );
+ }
$_SESSION['mpf']['field'.$field_id]
= str_replace(
array("[[", "]]"),
array("[[", "]]"),
- htmlspecialchars(
- stripslashes($post_field) ?? '', ENT_QUOTES)
- );
+ $session_val
+ );
}
// no injections, please
@@ -763,7 +772,7 @@ function eval_form($section_id) {
array("[[", "]]"),
array("[[", "]]"),
htmlspecialchars(
- $admin->add_slashes($v) ?? '', ENT_QUOTES
+ $v ?? '', ENT_QUOTES
)
);
$curr_field .= mpform_escape_string($field_value) . ", ";
diff --git a/export.php b/export.php
index 0f5a91e..72c49b6 100644
--- a/export.php
+++ b/export.php
@@ -16,7 +16,13 @@
*
**/
/* This file exports the whole section (excluding the submissions) to an xml file.
- The code was taken form the export_section module and integrated into mpform now */
+ The code was taken form the export_section module and integrated into mpform now.
+
+ 2026-07: rewritten to use information_schema for table/column discovery
+ instead of SHOW TABLES / SHOW COLUMNS ... LIKE, and to use parameterized
+ queries throughout. This is more reliable under the PDO-based Database
+ class and avoids relying on driver-specific numeric-index quirks of
+ fetchRow(). */
unset($_GET['page_id']);
unset($_GET['section_id']);
@@ -24,9 +30,6 @@
// manually include the config.php file (defines the required constants)
require('../../config.php');
-// Include WB admin wrapper script
-//require(WB_PATH.'/modules/admin.php');
-
// include core functions of WB 2.7 to edit the optional module CSS files (frontend.css, backend.css)
@include_once(WB_PATH .'/framework/module.functions.php');
@@ -35,11 +38,9 @@
// obtain module directory
$mod_dir = basename(dirname(__FILE__));
-
// include the module language file depending on the backend language of the current user
if (!@include(get_module_language_file($mod_dir))) return;
-
// include WB admin wrapper script to check permissions
$admin_header = false;
require(WB_PATH . '/modules/admin.php');
@@ -53,14 +54,16 @@
exit();
}
-// protect from cross site scripting
+// load the section row once; it's needed both for the id check below
+// and for the export itself, so there is no need to query it twice
$query_content = $database->query(
- "SELECT *"
- . " FROM ".TABLE_PREFIX."sections"
- . " WHERE section_id = '$section_id'");
+ "SELECT * FROM ".TABLE_PREFIX."sections WHERE section_id = ?",
+ [$section_id]
+);
+$section_row = $query_content ? $query_content->fetchRow(MYSQLI_ASSOC) : null;
-$res = $query_content->fetchRow();
-if (($res['page_id'] != $page_id)
+// protect from cross site scripting
+if ((!$section_row || $section_row['page_id'] != $page_id)
&& (!(defined('MPFORM_SKIP_ID_CHECK')&&(MPFORM_SKIP_ID_CHECK)))) {
$sUrlToGo = ADMIN_URL."/pages/index.php";
if(headers_sent())
@@ -72,124 +75,134 @@
exit(0);
}
-// obtain module directory
-$curr_dir = dirname(__FILE__);
-
-/* code originally in config.inc.php, not needed anymore here:
-
-// groups of known modules:
-$smooth_modules = array('wysiwyg', 'guestbook');
-$warn_modules = array(
- 'code' => 'Make sure to check whether you need to change '
- . 'variable names used in the code of this section!',
- 'bakery' => 'Make sure to move and rename all image files used '
- . 'for the articles of this section!',
- 'form' => 'Submissions have been omitted from export!',
- 'formx' => 'Submissions have been omitted from export!
'
- . 'It is highly recommended to use the module '
- . '"Migrate formx" to migrate the page to mpform!',
- 'mpform' => 'Submissions and results have been omitted from export!'
-);
-$blocked_modules = array('section_picker', 'foldergallery');
-
-// extract path separator and detect this module name
-$path_sep = strtoupper(substr(PHP_OS, 0, 3) == 'WIN') ? '\\' : '/';
-$module_folder
- = str_replace(
- WB_PATH
- . $path_sep
- . 'modules'
- . $path_sep,
- '',
- dirname(__FILE__)
- );
-$url_admintools = ADMIN_URL . '/admintools/tool.php?tool=' . $module_folder;
-
-*/
+/**
+ * Escapes a value for safe placement inside a CDATA section.
+ * CDATA blocks may contain anything except the literal sequence "]]>",
+ * so that sequence has to be broken up across two adjacent CDATA blocks.
+ */
+function mpform_export_cdata(?string $value): string
+{
+ $value = $value ?? '';
+ return str_replace(']]>', ']]]]>', $value);
+}
$lines = array();
$lines[] = '';
-$sql = "SELECT * FROM ". TABLE_PREFIX ."sections where section_id = '$section_id'";
-$results = $database->query($sql);
-if ($results && $row = $results->fetchRow()) {
- // if ( in_array($row['module'], $blocked_modules))
- // after integrating into mpform we restrict this to mpform sections instead
- // we should not arrive here anyway...
- if ($row['module'] != 'mpform'){
- $admin->print_header();
- $admin->print_error("Export of sections of type ".$row['module']." is not possible",
- ADMIN_URL.'/pages/modify.php?page_id='.(int)$page_id);
- $admin->print_footer();
- exit;
- } else {
- $lines[] = "";
- $lines[] = "\t";
- $lines[] = "\t\t".$row['module']."";
- $sql = "SELECT * FROM ".TABLE_PREFIX ."addons where directory = '".$row['module']."'";
- $results = $database->query($sql);
- if ($results && $row2 = $results->fetchRow()) {
- $lines[] = "\t\t".$row2['version']."";
- }
- /* we don't include the warning anymore
- // look for known issues and warn:
- if (array_key_exists($row['module'], $warn_modules)) {
- $lines[] = "\t\t";
+if (!$section_row) {
+ // section no longer exists - nothing to export
+ $admin->print_header();
+ $admin->print_error("Section $section_id not found",
+ ADMIN_URL.'/pages/modify.php?page_id='.(int)$page_id);
+ $admin->print_footer();
+ exit;
+}
+
+if ($section_row['module'] != 'mpform') {
+ $admin->print_header();
+ $admin->print_error("Export of sections of type ".$section_row['module']." is not possible",
+ ADMIN_URL.'/pages/modify.php?page_id='.(int)$page_id);
+ $admin->print_footer();
+ exit;
+}
+
+$lines[] = "";
+$lines[] = "\t";
+$lines[] = "\t\t".$section_row['module']."";
+
+$results = $database->query(
+ "SELECT * FROM ".TABLE_PREFIX."addons WHERE directory = ?",
+ [$section_row['module']]
+);
+if ($results && $addon_row = $results->fetchRow(MYSQLI_ASSOC)) {
+ $lines[] = "\t\t".$addon_row['version']."";
+}
+$lines[] = "\t";
+
+// ── Discover all module tables that carry a section_id column ──────────────
+// Instead of SHOW TABLES + SHOW COLUMNS ... LIKE (which turned out to behave
+// unreliably through the PDO wrapper), ask information_schema directly for
+// every table in the current database that starts with "mod_" and
+// has a `section_id` column. This is a single, portable, MySQL/MariaDB-
+// standard query.
+$prefix_pattern = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], TABLE_PREFIX.'mod_') . '%';
+$tables_result = $database->query(
+ "SELECT DISTINCT c.TABLE_NAME"
+ . " FROM information_schema.COLUMNS c"
+ . " WHERE c.TABLE_SCHEMA = DATABASE()"
+ . " AND c.COLUMN_NAME = 'section_id'"
+ . " AND c.TABLE_NAME LIKE ? ESCAPE '\\\\'"
+ . " ORDER BY c.TABLE_NAME",
+ [$prefix_pattern]
+);
+
+$export_tables = [];
+if ($tables_result) {
+ while ($t = $tables_result->fetchRow(MYSQLI_ASSOC)) {
+ $tablename = $t['TABLE_NAME'];
+ // skip submissions from form / formx / mpform modules
+ if (strpos($tablename, TABLE_PREFIX.'mod_form_submissions') === 0) continue;
+ if (strpos($tablename, TABLE_PREFIX.'mod_formx_submissions') === 0) continue;
+ if (strpos($tablename, TP_MPFORM.'submissions') === 0) continue;
+ $export_tables[] = $tablename;
+ }
+}
+
+foreach ($export_tables as $tablename) {
+ // find the auto_increment column of this table (if any), so it can be
+ // excluded from the export - a new value is assigned again on import.
+ // IMPORTANT: this must only exclude AUTO_INCREMENT columns, not every
+ // primary key column - some tables (e.g. mod_mpform_settings) use
+ // section_id itself as a non-autoincrement primary key, and that value
+ // is required on import (import.php re-injects the current section_id
+ // only for fields that are actually present in the export).
+ // This mirrors import.php's own detection query exactly:
+ // SHOW COLUMNS FROM `$tn` WHERE extra LIKE 'auto_increment'
+ $pk_result = $database->query(
+ "SELECT COLUMN_NAME FROM information_schema.COLUMNS"
+ . " WHERE TABLE_SCHEMA = DATABASE()"
+ . " AND TABLE_NAME = ?"
+ . " AND EXTRA LIKE '%auto_increment%'"
+ . " LIMIT 1",
+ [$tablename]
+ );
+ $pk_row = $pk_result ? $pk_result->fetchRow(MYSQLI_ASSOC) : null;
+ $pk_column = $pk_row['COLUMN_NAME'] ?? null;
+
+ // table name comes from information_schema (trusted), not user input,
+ // so it's safe to interpolate into the identifier position here
+ $results2 = $database->query(
+ "SELECT * FROM `" . $tablename . "` WHERE section_id = ?",
+ [$section_id]
+ );
+
+ $inside_tab = false;
+ while ($results2 && $row2 = $results2->fetchRow(MYSQLI_ASSOC)) {
+ if (!$inside_tab) {
+ $tn = substr($tablename, strlen(TABLE_PREFIX));
+ $lines[] = "\t";
+ $lines[] = "\t\t$tn";
+ $inside_tab = true;
}
- */
- $lines[] = "\t";
-
- $sql = "SHOW TABLES";
- $result = $database->query($sql);
- while ($row = $result->fetchRow()) {
- // skip non-module tables:
- if (strpos($row[0], TABLE_PREFIX.'mod_') !== 0) continue;
- // skip submissions from form module:
- if (strpos($row[0], TABLE_PREFIX.'mod_form_submissions') === 0) continue;
- // skip submissions from formx module:
- if (strpos($row[0], TABLE_PREFIX.'mod_formx_submissions') === 0) continue;
- // skip submissions from mpform module:
- if (strpos($row[0], TP_MPFORM.'submissions') === 0) continue;
- $sql = "SHOW COLUMNS FROM `" . $row[0] . "` LIKE 'section_id'";
- $results = $database->query($sql);
- if ($results && $exists = $results->fetchRow()) {
- $sql2 = "SELECT * FROM `" . $row[0] . "` WHERE section_id = '$section_id'";
- $results2 = $database->query($sql2);
- $inside_tab = false;
- while ($results2 && $row2 = $results2->fetchRow()) {
- if (!$inside_tab) {
- $tn = substr($row[0], strlen(TABLE_PREFIX));
- $lines[] = "\t";
- $lines[] = "\t\t$tn";
- $inside_tab = true;
- }
- $lines[] = "\t\t";
- $i = 0;
- foreach ($row2 as $k => $v) {
- $i++;
- if ($i > 1) {
- if ($i % 2 == 0) {
- $cv = addslashes($v);
- $lines[]
- = "\t\t\t"
- . "$k"
- . ""
- . "";
- }
- }
- }
- $lines[] = "\t\t";
- }
- if ($inside_tab) {
- $lines[] = "\t";
- $inside_tab = false;
- }
- }
+ $lines[] = "\t\t";
+ foreach ($row2 as $k => $v) {
+ if ($pk_column !== null && $k === $pk_column) continue;
+ $cv = mpform_export_cdata($v);
+ $lines[]
+ = "\t\t\t"
+ . "$k"
+ . ""
+ . "";
}
-
- $lines[] = "";
+ $lines[] = "\t\t";
+ }
+ if ($inside_tab) {
+ $lines[] = "\t";
}
}
+
+$lines[] = "";
+
header("Content-Type: text/plain");
header("Content-Disposition: attachment; filename=section_$section_id.xml");
foreach ($lines as $l) echo "$l\r\n";
diff --git a/info.php b/info.php
index 7203651..62ef68e 100644
--- a/info.php
+++ b/info.php
@@ -20,7 +20,7 @@
$module_directory = 'mpform';
$module_name = 'mpForm';
$module_function = 'page';
-$module_version = '1.3.44';
+$module_version = '1.3.47';
$module_platform = '2.8.x';
$module_status = 'stable';
$module_author = 'Frank Heyne, NorHei(heimsath.org), Christian M. Stefan (Stefek), Martin Hecht (mrbaseman) and others';
diff --git a/modify.php b/modify.php
index 6cf06ef..c486227 100644
--- a/modify.php
+++ b/modify.php
@@ -147,7 +147,7 @@
case 'filename':
$rt = $TEXT['UPLOAD_FILES'];
break;
- case 'date';
+ case 'date':
$rt = $TEXT['DATE'];
break;
case 'email':
diff --git a/save_field.php b/save_field.php
index 9dca2a4..ea8210d 100644
--- a/save_field.php
+++ b/save_field.php
@@ -213,8 +213,8 @@ function int_not0($s) {
$values[] = preg_replace("/&(#?[a-zA-Z0-9]+);/","&\\1;",
str_replace(array(",", "[[", "]]"),
array(",", '', ''),
- htmlspecialchars($admin->add_slashes(
- $admin->get_post('value'.$i)),
+ htmlspecialchars(
+ $admin->get_post('value'.$i),
ENT_QUOTES)
)
) . $defcode;