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;