Initial commit
This commit is contained in:
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Compatibility6\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MailUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Formmail class
|
||||
* used to submit data, and hooks into TSFE
|
||||
*/
|
||||
class FormDataSubmissionController
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $reserved_names = 'recipient,recipient_copy,auto_respond_msg,auto_respond_checksum,redirect,subject,attachment,from_email,from_name,replyto_email,replyto_name,organisation,priority,html_enabled,quoted_printable,submit_x,submit_y';
|
||||
|
||||
/**
|
||||
* Collection of suspicious header data, used for logging
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dirtyHeaders = array();
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $characterSet;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $subject;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $fromName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $replyToName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $organisation;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $fromAddress;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $replyToAddress;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $priority;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $autoRespondMessage;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $encoding = 'quoted-printable';
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Core\Mail\MailMessage
|
||||
*/
|
||||
protected $mailMessage;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $recipient;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $plainContent = '';
|
||||
|
||||
/**
|
||||
* @var array Files to clean up at the end (attachments)
|
||||
*/
|
||||
protected $temporaryFiles = array();
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
|
||||
*/
|
||||
protected $frontendController = null;
|
||||
|
||||
/**
|
||||
* hook to be executed by TypoScriptFrontendController
|
||||
*
|
||||
* @param \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController $frontendController
|
||||
*/
|
||||
public function checkDataSubmission($frontendController)
|
||||
{
|
||||
$this->frontendController = $frontendController;
|
||||
|
||||
// Checks if any email-submissions
|
||||
$formtype_mail = isset($_POST['formtype_mail']) || isset($_POST['formtype_mail_x']);
|
||||
if ($formtype_mail) {
|
||||
$refInfo = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER'));
|
||||
if (GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY') == $refInfo['host'] || $this->frontendController->TYPO3_CONF_VARS['SYS']['doNotCheckReferer']) {
|
||||
if ($this->locDataCheck($_POST['locationData'])) {
|
||||
if ($formtype_mail) {
|
||||
$this->prepareAndSend();
|
||||
$GLOBALS['TT']->setTSlogMessage('"Check Data Submission": Return value: email', 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$GLOBALS['TT']->setTSlogMessage('"Check Data Submission": HTTP_HOST and REFERER HOST did not match when processing submitted formdata!', 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a formmail submission can be sent as email
|
||||
*
|
||||
* @param string $locationData The input from $_POST['locationData']
|
||||
* @return void|int
|
||||
*/
|
||||
protected function locDataCheck($locationData)
|
||||
{
|
||||
$locData = explode(':', $locationData);
|
||||
if (!$locData[1] || $this->frontendController->sys_page->checkRecord($locData[1], $locData[2], 1)) {
|
||||
// $locData[1] -check means that a record is checked only if the locationData has a value for a record else than the page.
|
||||
if (!empty($this->frontendController->sys_page->getPage($locData[0]))) {
|
||||
return 1;
|
||||
}
|
||||
$GLOBALS['TT']->setTSlogMessage('LocationData Error: The page pointed to by location data (' . $locationData . ') was not accessible.', 2);
|
||||
} else {
|
||||
$GLOBALS['TT']->setTSlogMessage('LocationData Error: Location data (' . $locationData . ') record pointed to was not accessible.', 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the emails from the formmail content object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function prepareAndSend()
|
||||
{
|
||||
$EMAIL_VARS = GeneralUtility::_POST();
|
||||
$locationData = $EMAIL_VARS['locationData'];
|
||||
unset($EMAIL_VARS['locationData']);
|
||||
unset($EMAIL_VARS['formtype_mail'], $EMAIL_VARS['formtype_mail_x'], $EMAIL_VARS['formtype_mail_y']);
|
||||
$integrityCheck = $this->frontendController->TYPO3_CONF_VARS['FE']['strictFormmail'];
|
||||
if (!$this->frontendController->TYPO3_CONF_VARS['FE']['secureFormmail']) {
|
||||
// Check recipient field:
|
||||
// These two fields are the ones which contain recipient addresses that can be misused to send mail from foreign servers.
|
||||
$encodedFields = explode(',', 'recipient, recipient_copy');
|
||||
foreach ($encodedFields as $fieldKey) {
|
||||
if ((string)$EMAIL_VARS[$fieldKey] !== '') {
|
||||
// Decode...
|
||||
if ($res = \TYPO3\CMS\Compatibility6\Utility\FormUtility::codeString($EMAIL_VARS[$fieldKey], true)) {
|
||||
$EMAIL_VARS[$fieldKey] = $res;
|
||||
} elseif ($integrityCheck) {
|
||||
// Otherwise abort:
|
||||
$GLOBALS['TT']->setTSlogMessage('"Formmail" discovered a field (' . $fieldKey . ') which could not be decoded to a valid string. Sending formmail aborted due to security reasons!', 3);
|
||||
return;
|
||||
} else {
|
||||
$GLOBALS['TT']->setTSlogMessage('"Formmail" discovered a field (' . $fieldKey . ') which could not be decoded to a valid string. The security level accepts this, but you should consider a correct coding though!', 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$locData = explode(':', $locationData);
|
||||
$record = $this->frontendController->sys_page->checkRecord($locData[1], $locData[2], 1);
|
||||
$EMAIL_VARS['recipient'] = $record['subheader'];
|
||||
$EMAIL_VARS['recipient_copy'] = $this->extractRecipientCopy($record['bodytext']);
|
||||
}
|
||||
// Hook for preprocessing of the content for formmails:
|
||||
if (is_array($this->frontendController->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['sendFormmail-PreProcClass'])) {
|
||||
foreach ($this->frontendController->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['sendFormmail-PreProcClass'] as $_classRef) {
|
||||
$_procObj = GeneralUtility::getUserObj($_classRef);
|
||||
$EMAIL_VARS = $_procObj->sendFormmail_preProcessVariables($EMAIL_VARS, $this);
|
||||
}
|
||||
}
|
||||
$this->start($EMAIL_VARS);
|
||||
$r = $this->sendtheMail();
|
||||
$GLOBALS['TT']->setTSlogMessage('"Formmail" invoked, sending mail to ' . $EMAIL_VARS['recipient'], 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the value of recipient copy field from a formmail CE bodytext
|
||||
*
|
||||
* @param string $bodytext The content of the related bodytext field
|
||||
* @return string The value of the recipient_copy field, or an empty string
|
||||
*/
|
||||
protected function extractRecipientCopy($bodytext)
|
||||
{
|
||||
$fdef = array();
|
||||
//|recipient_copy=hidden|karsten@localhost.localdomain
|
||||
preg_match('/^[\\s]*\\|[\\s]*recipient_copy[\\s]*=[\\s]*hidden[\\s]*\\|(.*)$/m', $bodytext, $fdef);
|
||||
return $fdef[1] ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Start function
|
||||
* This class is able to generate a mail in formmail-style from the data in $V
|
||||
* Fields:
|
||||
*
|
||||
* [recipient]: email-adress of the one to receive the mail. If array, then all values are expected to be recipients
|
||||
* [attachment]: ....
|
||||
*
|
||||
* [subject]: The subject of the mail
|
||||
* [from_email]: Sender email. If not set, [email] is used
|
||||
* [from_name]: Sender name. If not set, [name] is used
|
||||
* [replyto_email]: Reply-to email. If not set [from_email] is used
|
||||
* [replyto_name]: Reply-to name. If not set [from_name] is used
|
||||
* [organisation]: Organization (header)
|
||||
* [priority]: Priority, 1-5, default 3
|
||||
* [html_enabled]: If mail is sent as html
|
||||
* [use_base64]: If set, base64 encoding will be used instead of quoted-printable
|
||||
*
|
||||
* @param array $valueList Contains values for the field names listed above (with slashes removed if from POST input)
|
||||
* @param bool $base64 Whether to base64 encode the mail content
|
||||
* @return void
|
||||
*/
|
||||
public function start($valueList, $base64 = false)
|
||||
{
|
||||
$this->mailMessage = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
|
||||
if ($GLOBALS['TSFE']->config['config']['formMailCharset']) {
|
||||
// Respect formMailCharset if it was set
|
||||
$this->characterSet = $GLOBALS['TSFE']->csConvObj->parse_charset($GLOBALS['TSFE']->config['config']['formMailCharset']);
|
||||
} elseif ($GLOBALS['TSFE']->metaCharset != $GLOBALS['TSFE']->renderCharset) {
|
||||
// Use metaCharset for mail if different from renderCharset
|
||||
$this->characterSet = $GLOBALS['TSFE']->metaCharset;
|
||||
} else {
|
||||
// Otherwise use renderCharset as default
|
||||
$this->characterSet = $GLOBALS['TSFE']->renderCharset;
|
||||
}
|
||||
if ($base64 || $valueList['use_base64']) {
|
||||
$this->encoding = 'base64';
|
||||
}
|
||||
if (isset($valueList['recipient'])) {
|
||||
// Convert form data from renderCharset to mail charset
|
||||
$this->subject = $valueList['subject'] ? $valueList['subject'] : 'Formmail on ' . GeneralUtility::getIndpEnv('HTTP_HOST');
|
||||
$this->subject = $this->sanitizeHeaderString($this->subject);
|
||||
$this->fromName = $valueList['from_name'] ? $valueList['from_name'] : ($valueList['name'] ? $valueList['name'] : '');
|
||||
$this->fromName = $this->sanitizeHeaderString($this->fromName);
|
||||
$this->replyToName = $valueList['replyto_name'] ? $valueList['replyto_name'] : $this->fromName;
|
||||
$this->replyToName = $this->sanitizeHeaderString($this->replyToName);
|
||||
$this->organisation = $valueList['organisation'] ? $valueList['organisation'] : '';
|
||||
$this->organisation = $this->sanitizeHeaderString($this->organisation);
|
||||
$this->fromAddress = $valueList['from_email'] ? $valueList['from_email'] : ($valueList['email'] ? $valueList['email'] : '');
|
||||
if (!GeneralUtility::validEmail($this->fromAddress)) {
|
||||
$this->fromAddress = MailUtility::getSystemFromAddress();
|
||||
$this->fromName = MailUtility::getSystemFromName();
|
||||
}
|
||||
$this->replyToAddress = $valueList['replyto_email'] ? $valueList['replyto_email'] : $this->fromAddress;
|
||||
$this->priority = $valueList['priority'] ? MathUtility::forceIntegerInRange($valueList['priority'], 1, 5) : 3;
|
||||
// Auto responder
|
||||
$this->autoRespondMessage = trim($valueList['auto_respond_msg']) && $this->fromAddress ? trim($valueList['auto_respond_msg']) : '';
|
||||
if ($this->autoRespondMessage !== '') {
|
||||
// Check if the value of the auto responder message has been modified with evil intentions
|
||||
$autoRespondChecksum = $valueList['auto_respond_checksum'];
|
||||
$correctHmacChecksum = GeneralUtility::hmac($this->autoRespondMessage, 'content_form');
|
||||
if ($autoRespondChecksum !== $correctHmacChecksum) {
|
||||
GeneralUtility::sysLog('Possible misuse of DataSubmissionController auto respond method. Subject: ' . $valueList['subject'], 'core', GeneralUtility::SYSLOG_SEVERITY_ERROR);
|
||||
return;
|
||||
} else {
|
||||
$this->autoRespondMessage = $this->sanitizeHeaderString($this->autoRespondMessage);
|
||||
}
|
||||
}
|
||||
$plainTextContent = '';
|
||||
$htmlContent = '<table border="0" cellpadding="2" cellspacing="2">';
|
||||
// Runs through $V and generates the mail
|
||||
if (is_array($valueList)) {
|
||||
foreach ($valueList as $key => $val) {
|
||||
if (!GeneralUtility::inList($this->reserved_names, $key)) {
|
||||
$space = strlen($val) > 60 ? LF : '';
|
||||
$val = is_array($val) ? implode($val, LF) : $val;
|
||||
// Convert form data from renderCharset to mail charset (HTML may use entities)
|
||||
$plainTextValue = $val;
|
||||
$HtmlValue = htmlspecialchars($val);
|
||||
$plainTextContent .= strtoupper($key) . ': ' . $space . $plainTextValue . LF . $space;
|
||||
$htmlContent .= '<tr><td bgcolor="#eeeeee"><font face="Verdana" size="1"><strong>' . strtoupper($key) . '</strong></font></td><td bgcolor="#eeeeee"><font face="Verdana" size="1">' . nl2br($HtmlValue) . ' </font></td></tr>';
|
||||
}
|
||||
}
|
||||
}
|
||||
$htmlContent .= '</table>';
|
||||
$this->plainContent = $plainTextContent;
|
||||
if ($valueList['html_enabled']) {
|
||||
$this->mailMessage->setBody($htmlContent, 'text/html', $this->characterSet);
|
||||
$this->mailMessage->addPart($plainTextContent, 'text/plain', $this->characterSet);
|
||||
} else {
|
||||
$this->mailMessage->setBody($plainTextContent, 'text/plain', $this->characterSet);
|
||||
}
|
||||
for ($a = 0; $a < 10; $a++) {
|
||||
$variableName = 'attachment' . ($a ?: '');
|
||||
if (!isset($_FILES[$variableName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($_FILES[$variableName]['error'] !== UPLOAD_ERR_OK) {
|
||||
GeneralUtility::sysLog(
|
||||
'Error in uploaded file in DataSubmissionController: temporary file "' .
|
||||
$_FILES[$variableName]['tmp_name'] . '" ("' . $_FILES[$variableName]['name'] . '") Error code: ' .
|
||||
$_FILES[$variableName]['error'],
|
||||
'core',
|
||||
GeneralUtility::SYSLOG_SEVERITY_ERROR
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_uploaded_file($_FILES[$variableName]['tmp_name'])) {
|
||||
GeneralUtility::sysLog(
|
||||
'Possible abuse of DataSubmissionController: temporary file "' . $_FILES[$variableName]['tmp_name'] .
|
||||
'" ("' . $_FILES[$variableName]['name'] . '") was not an uploaded file.',
|
||||
'core',
|
||||
GeneralUtility::SYSLOG_SEVERITY_ERROR
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$theFile = GeneralUtility::upload_to_tempfile($_FILES[$variableName]['tmp_name']);
|
||||
$theName = $_FILES[$variableName]['name'];
|
||||
if ($theFile && file_exists($theFile)) {
|
||||
if (filesize($theFile) < $GLOBALS['TYPO3_CONF_VARS']['FE']['formmailMaxAttachmentSize']) {
|
||||
$this->mailMessage->attach(\Swift_Attachment::fromPath($theFile)->setFilename($theName));
|
||||
}
|
||||
}
|
||||
$this->temporaryFiles[] = $theFile;
|
||||
}
|
||||
$from = $this->fromName ? array($this->fromAddress => $this->fromName) : array($this->fromAddress);
|
||||
$this->recipient = $this->parseAddresses($valueList['recipient']);
|
||||
$this->mailMessage->setSubject($this->subject)->setFrom($from)->setTo($this->recipient)->setPriority($this->priority);
|
||||
$replyTo = $this->replyToName ? array($this->replyToAddress => $this->replyToName) : array($this->replyToAddress);
|
||||
$this->mailMessage->setReplyTo($replyTo);
|
||||
$this->mailMessage->getHeaders()->addTextHeader('Organization', $this->organisation);
|
||||
if ($valueList['recipient_copy']) {
|
||||
$this->mailMessage->setCc($this->parseAddresses($valueList['recipient_copy']));
|
||||
}
|
||||
$this->mailMessage->setCharset($this->characterSet);
|
||||
// Ignore target encoding. This is handled automatically by Swift Mailer and overriding the defaults
|
||||
// is not worth the trouble
|
||||
// Log dirty header lines
|
||||
if ($this->dirtyHeaders) {
|
||||
GeneralUtility::sysLog('Possible misuse of DataSubmissionController: see TYPO3 devLog', 'core', GeneralUtility::SYSLOG_SEVERITY_ERROR);
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['enable_DLOG']) {
|
||||
GeneralUtility::devLog('DataSubmissionController: ' . GeneralUtility::arrayToLogString($this->dirtyHeaders, '', 200), 'Core', 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks string for suspicious characters
|
||||
*
|
||||
* @param string $string String to check
|
||||
* @return string Valid or empty string
|
||||
*/
|
||||
protected function sanitizeHeaderString($string)
|
||||
{
|
||||
$pattern = '/[\\r\\n\\f\\e]/';
|
||||
if (preg_match($pattern, $string) > 0) {
|
||||
$this->dirtyHeaders[] = $string;
|
||||
$string = '';
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses mailbox headers and turns them into an array.
|
||||
*
|
||||
* Mailbox headers are a comma separated list of 'name <email@example.org' combinations or plain email addresses (or a mix
|
||||
* of these).
|
||||
* The resulting array has key-value pairs where the key is either a number (no display name in the mailbox header) and the
|
||||
* value is the email address, or the key is the email address and the value is the display name.
|
||||
*
|
||||
* @param string $rawAddresses Comma separated list of email addresses (optionally with display name)
|
||||
* @return array Parsed list of addresses.
|
||||
*/
|
||||
protected function parseAddresses($rawAddresses = '')
|
||||
{
|
||||
/** @var $addressParser \TYPO3\CMS\Core\Mail\Rfc822AddressesParser */
|
||||
$addressParser = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\Rfc822AddressesParser::class, $rawAddresses);
|
||||
$addresses = $addressParser->parseAddressList();
|
||||
$addressList = array();
|
||||
foreach ($addresses as $address) {
|
||||
if ($address->personal) {
|
||||
// Item with name found ( name <email@example.org> )
|
||||
$addressList[$address->mailbox . '@' . $address->host] = $address->personal;
|
||||
} else {
|
||||
// Item without name found ( email@example.org )
|
||||
$addressList[] = $address->mailbox . '@' . $address->host;
|
||||
}
|
||||
}
|
||||
return $addressList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the actual mail and handles autorespond message
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function sendTheMail()
|
||||
{
|
||||
// Sending the mail requires the recipient and message to be set.
|
||||
if (!$this->mailMessage->getTo() || !trim($this->mailMessage->getBody())) {
|
||||
return false;
|
||||
}
|
||||
$this->mailMessage->send();
|
||||
// Auto response
|
||||
if ($this->autoRespondMessage) {
|
||||
$theParts = explode('/', $this->autoRespondMessage, 2);
|
||||
$theParts[0] = str_replace('###SUBJECT###', $this->subject, $theParts[0]);
|
||||
$theParts[1] = str_replace(
|
||||
array('/', '###MESSAGE###'),
|
||||
array(LF, $this->plainContent),
|
||||
$theParts[1]
|
||||
);
|
||||
/** @var $autoRespondMail \TYPO3\CMS\Core\Mail\MailMessage */
|
||||
$autoRespondMail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
|
||||
$autoRespondMail->setTo($this->fromAddress)->setSubject($theParts[0])->setFrom($this->recipient)->setBody($theParts[1]);
|
||||
$autoRespondMail->send();
|
||||
}
|
||||
return $this->mailMessage->isSent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do some cleanup at the end (deleting attachment files)
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
foreach ($this->temporaryFiles as $file) {
|
||||
if (GeneralUtility::isAllowedAbsPath($file) && GeneralUtility::isFirstPartOfStr($file, PATH_site . 'typo3temp/upload_temp_')) {
|
||||
GeneralUtility::unlink_tempfile($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Compatibility6\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Module\BaseScriptClass;
|
||||
|
||||
/**
|
||||
* The Wizard function in the Web>Info module
|
||||
* Creates a framework for adding wizard sub-sub-modules under the Wizard function in Web>Info
|
||||
*/
|
||||
class WebFunctionWizardsBaseController extends \TYPO3\CMS\Backend\Module\AbstractFunctionModule
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $function_key = 'wiz';
|
||||
|
||||
/**
|
||||
* Initialize.
|
||||
* Calls parent init function and then the handleExternalFunctionValue() function from the parent class
|
||||
*
|
||||
* @param BaseScriptClass $pObj A reference to the parent (calling) object (which is probably an instance of an extension class to \TYPO3\CMS\Backend\Module\BaseScriptClass)
|
||||
* @param array $conf The configuration set for this module - from global array TBE_MODULES_EXT
|
||||
* @return void
|
||||
*/
|
||||
public function init(&$pObj, $conf)
|
||||
{
|
||||
// OK, handles ordinary init. This includes setting up the menu array with ->modMenu
|
||||
parent::init($pObj, $conf);
|
||||
$this->handleExternalFunctionValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies parent objects internal MOD_MENU array, adding items this module needs.
|
||||
*
|
||||
* @return array Items merged with the parent objects.
|
||||
*/
|
||||
public function modMenu()
|
||||
{
|
||||
$GLOBALS['LANG']->includeLLFile('EXT:compatibility6/Resources/Private/Language/wizards.xlf');
|
||||
$modMenuAdd = array(
|
||||
$this->function_key => array()
|
||||
);
|
||||
$modMenuAdd[$this->function_key] = $this->pObj->mergeExternalItems($this->pObj->MCONF['name'], $this->function_key, $modMenuAdd[$this->function_key]);
|
||||
$modMenuAdd[$this->function_key] = \TYPO3\CMS\Backend\Utility\BackendUtility::unsetMenuItems(
|
||||
$this->pObj->modTSconfig['properties'],
|
||||
$modMenuAdd[$this->function_key],
|
||||
'menu.' . $this->function_key
|
||||
);
|
||||
return $modMenuAdd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creation of the main content. Calling extObjContent() to trigger content generation from the sub-sub modules
|
||||
*
|
||||
* @return string The content
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
$menu = \TYPO3\CMS\Backend\Utility\BackendUtility::getFuncMenu(
|
||||
$this->pObj->id,
|
||||
'SET[wiz]',
|
||||
$this->pObj->MOD_SETTINGS['wiz'],
|
||||
$this->pObj->MOD_MENU['wiz']
|
||||
);
|
||||
|
||||
$content = '';
|
||||
if (!empty($menu)) {
|
||||
$menu = $GLOBALS['LANG']->getLL('wiz_lWizards', true) . ': ' . $menu;
|
||||
$content = '<div>';
|
||||
$content .= '<span class="text-nowrap">' . $menu . '</span>';
|
||||
$content .= '</div>';
|
||||
$content .= '<div style="padding-top: 20px;"></div>';
|
||||
}
|
||||
|
||||
$content .= $this->extObjContent();
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,898 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Compatibility6\Controller\Wizard;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* API comments:
|
||||
*
|
||||
* The form wizard can help you to create forms - it allows you to create almost any kind of HTML form elements and in any order and amount.
|
||||
*
|
||||
* The format for the resulting configuration code can be either a line-based configuration. That can look like this:
|
||||
*
|
||||
* Your name: | *name=input | (input your name here!)
|
||||
* Your Email: | *email=input
|
||||
* Your address: | address=textarea,40,10
|
||||
* Your Haircolor: | hair=radio |
|
||||
* upload | attachment=file
|
||||
* | quoted_printable=hidden | 0
|
||||
* | formtype_mail=submit | Send form
|
||||
* | html_enabled=hidden
|
||||
* | subject=hidden | This is the subject
|
||||
*
|
||||
*
|
||||
* Alternatively it can be XML. The same configuration from above looks like this in XML:
|
||||
*
|
||||
* <T3FormWizard>
|
||||
* <n2>
|
||||
* <type>input</type>
|
||||
* <label>Your name:</label>
|
||||
* <required>1</required>
|
||||
* <fieldname>name</fieldname>
|
||||
* <size></size>
|
||||
* <max></max>
|
||||
* <default>(input your name here!)</default>
|
||||
* </n2>
|
||||
* <n4>
|
||||
* <type>input</type>
|
||||
* <label>Your Email:</label>
|
||||
* <required>1</required>
|
||||
* <fieldname>email</fieldname>
|
||||
* <size></size>
|
||||
* <max></max>
|
||||
* <default></default>
|
||||
* </n4>
|
||||
* <n6>
|
||||
* <type>textarea</type>
|
||||
* <label>Your address:</label>
|
||||
* <fieldname>address</fieldname>
|
||||
* <cols>40</cols>
|
||||
* <rows>10</rows>
|
||||
* <default></default>
|
||||
* </n6>
|
||||
* <n8>
|
||||
* <type>radio</type>
|
||||
* <label>Your Haircolor:</label>
|
||||
* <fieldname>hair</fieldname>
|
||||
* <options></options>
|
||||
* </n8>
|
||||
* <n10>
|
||||
* <type>file</type>
|
||||
* <label>upload</label>
|
||||
* <fieldname>attachment</fieldname>
|
||||
* <size></size>
|
||||
* </n10>
|
||||
* <n12>
|
||||
* <type>hidden</type>
|
||||
* <label></label>
|
||||
* <fieldname>quoted_printable</fieldname>
|
||||
* <default>0</default>
|
||||
* </n12>
|
||||
* <n2000>
|
||||
* <fieldname>formtype_mail</fieldname>
|
||||
* <type>submit</type>
|
||||
* <default>Send form</default>
|
||||
* </n2000>
|
||||
* <n2002>
|
||||
* <fieldname>html_enabled</fieldname>
|
||||
* <type>hidden</type>
|
||||
* </n2002>
|
||||
* <n2004>
|
||||
* <fieldname>subject</fieldname>
|
||||
* <type>hidden</type>
|
||||
* <default>This is the subject</default>
|
||||
* </n2004>
|
||||
* <n20>
|
||||
* <content></content>
|
||||
* </n20>
|
||||
* </T3FormWizard>
|
||||
*
|
||||
*
|
||||
* The XML/phpArray structure is the internal format of the wizard.
|
||||
*/
|
||||
class FormsController extends \TYPO3\CMS\Backend\Controller\Wizard\AbstractWizardController
|
||||
{
|
||||
/**
|
||||
* document template object
|
||||
*
|
||||
* @var \TYPO3\CMS\Backend\Template\DocumentTemplate
|
||||
*/
|
||||
public $doc;
|
||||
|
||||
/**
|
||||
* Content accumulation for the module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $content;
|
||||
|
||||
/**
|
||||
* Used to numerate attachments automatically.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $attachmentCounter = 0;
|
||||
|
||||
/**
|
||||
* If set, the string version of the content is interpreted/written as XML instead of
|
||||
* the original linebased kind. This variable still needs binding to the wizard parameters
|
||||
* - but support is ready!
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $xmlStorage = 0;
|
||||
|
||||
/**
|
||||
* Wizard parameters, coming from TCEforms linking to the wizard.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $P;
|
||||
|
||||
/**
|
||||
* The array which is constantly submitted by the multidimensional form of this wizard.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $FORMCFG;
|
||||
|
||||
/**
|
||||
* Indicates if the form is of a dedicated type, like "formtype_mail" (for tt_content element "Form")
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $special;
|
||||
|
||||
/**
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$this->getLanguageService()->includeLLFile('EXT:compatibility6/Resources/Private/Language/locallang_wizards.xlf');
|
||||
$GLOBALS['SOBE'] = $this;
|
||||
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization the class
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
// GPvars:
|
||||
$this->P = GeneralUtility::_GP('P');
|
||||
$this->special = GeneralUtility::_GP('special');
|
||||
$this->FORMCFG = GeneralUtility::_GP('FORMCFG');
|
||||
// Setting options:
|
||||
$this->xmlStorage = $this->P['params']['xmlOutput'];
|
||||
// Document template object:
|
||||
$this->doc = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Template\DocumentTemplate::class);
|
||||
$this->doc->setModuleTemplate('EXT:compatibility6/Resources/Private/Templates/Wizard/Forms.html');
|
||||
// Setting form tag:
|
||||
list($rUri) = explode('#', GeneralUtility::getIndpEnv('REQUEST_URI'));
|
||||
$this->doc->form = '<form action="' . htmlspecialchars($rUri) . '" method="post" name="wizardForm">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @param ResponseInterface $response
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request, ResponseInterface $response)
|
||||
{
|
||||
$this->main();
|
||||
|
||||
$response->getBody()->write($this->content);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function for rendering the form wizard HTML
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
if ($this->P['table'] && $this->P['field'] && $this->P['uid']) {
|
||||
$this->content .= '<h2>' . $this->getLanguageService()->getLL('forms_title', true) . '</h2><div>' . $this->formsWizard() . '</div>';
|
||||
} else {
|
||||
$this->content .= '<h2>' . $this->getLanguageService()->getLL('forms_title', true) . '<div><span class="text-danger">' . $this->getLanguageService()->getLL('table_noData', true) . '</span></div>';
|
||||
}
|
||||
// Setting up the buttons and markers for docheader
|
||||
$docHeaderButtons = $this->getButtons();
|
||||
$markers['CSH'] = $docHeaderButtons['csh'];
|
||||
$markers['CONTENT'] = $this->content;
|
||||
// Build the <body> for the module
|
||||
$this->content = $this->doc->startPage('Form Wizard');
|
||||
$this->content .= $this->doc->moduleBody($this->pageinfo, $docHeaderButtons, $markers);
|
||||
$this->content .= $this->doc->endPage();
|
||||
$this->content = $this->doc->insertStylesAndJS($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputting the accumulated content to screen
|
||||
*
|
||||
* @return void
|
||||
* @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8, use mainAction() instead
|
||||
*/
|
||||
public function printContent()
|
||||
{
|
||||
GeneralUtility::logDeprecatedFunction();
|
||||
echo $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the panel of buttons for submitting the form or otherwise perform operations.
|
||||
*
|
||||
* @return array All available buttons as an assoc. array
|
||||
*/
|
||||
protected function getButtons()
|
||||
{
|
||||
$buttons = array(
|
||||
'csh' => '',
|
||||
'csh_buttons' => '',
|
||||
'close' => '',
|
||||
'save' => '',
|
||||
'save_close' => '',
|
||||
'reload' => ''
|
||||
);
|
||||
if ($this->P['table'] && $this->P['field'] && $this->P['uid']) {
|
||||
// CSH
|
||||
$buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'wizard_forms_wiz');
|
||||
// CSH Buttons
|
||||
$buttons['csh_buttons'] = BackendUtility::cshItem('xMOD_csh_corebe', 'wizard_forms_wiz_buttons');
|
||||
// Close
|
||||
$buttons['close'] = '<button class="c-inputButton" name="closedok" value="1" title=' . $this->getLanguageService()->sL('LLL:EXT:compatibility6/Resources/Private/Language/locallang.xlf:closeDoc', true) . '>' . $this->iconFactory->getIcon('actions-document-close', Icon::SIZE_SMALL)->render() . '</button>';
|
||||
// Save
|
||||
$buttons['save'] = '<button class="c-inputButton" name="savedok" value="1" title=' . $this->getLanguageService()->sL('LLL:EXT:compatibility6/Resources/Private/Language/locallang.xlf:saveDoc', true) . '>' . $this->iconFactory->getIcon('actions-document-save', Icon::SIZE_SMALL)->render() . '</button>';
|
||||
// Save & Close
|
||||
$buttons['save_close'] = '<button class="c-inputButton" name="saveandclosedok" value="1" title=' . $this->getLanguageService()->sL('LLL:EXT:compatibility6/Resources/Private/Language/locallang.xlf:saveCloseDoc', true) . '>' . $this->iconFactory->getIcon('actions-document-save-close', Icon::SIZE_SMALL)->render() . '</button>';
|
||||
// Reload
|
||||
$buttons['reload'] = '<button class="c-inputButton" name="_refresh" value="1" title="' . $this->getLanguageService()->getLL('forms_refresh', true) . '">' . $this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL)->render() . '</button>';
|
||||
}
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the form wizard content
|
||||
*
|
||||
* @return string HTML content for the form.
|
||||
*/
|
||||
public function formsWizard()
|
||||
{
|
||||
if (!$this->checkEditAccess($this->P['table'], $this->P['uid'])) {
|
||||
throw new \RuntimeException('Wizard Error: No access', 1385807526);
|
||||
}
|
||||
// First, check the references by selecting the record:
|
||||
$row = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
|
||||
if (!is_array($row)) {
|
||||
throw new \RuntimeException('Wizard Error: No reference to record', 1294587124);
|
||||
}
|
||||
// This will get the content of the form configuration code field to us - possibly
|
||||
// cleaned up, saved to database etc. if the form has been submitted in the meantime.
|
||||
$formCfgArray = $this->getConfigCode($row);
|
||||
// Generation of the Form Wizards HTML code:
|
||||
$content = $this->getFormHTML($formCfgArray, $row);
|
||||
// Return content:
|
||||
return $content;
|
||||
}
|
||||
|
||||
/****************************
|
||||
*
|
||||
* Helper functions
|
||||
*
|
||||
***************************/
|
||||
/**
|
||||
* Will get and return the configuration code string
|
||||
* Will also save (and possibly redirect/exit) the content if a save button has been pressed
|
||||
*
|
||||
* @param array $row Current parent record row (passed by value!)
|
||||
* @return array Configuration Array
|
||||
* @access private
|
||||
*/
|
||||
public function getConfigCode(&$row)
|
||||
{
|
||||
// If some data has been submitted, then construct
|
||||
if (isset($this->FORMCFG['c'])) {
|
||||
// Process incoming:
|
||||
$this->changeFunc();
|
||||
// Convert to string (either line based or XML):
|
||||
if ($this->xmlStorage) {
|
||||
// Convert the input array to XML:
|
||||
$bodyText = GeneralUtility::array2xml_cs($this->FORMCFG['c'], 'T3FormWizard');
|
||||
// Setting cfgArr directly from the input:
|
||||
$cfgArr = $this->FORMCFG['c'];
|
||||
} else {
|
||||
// Convert the input array to a string of configuration code:
|
||||
$bodyText = $this->cfgArray2CfgString($this->FORMCFG['c']);
|
||||
// Create cfgArr from the string based configuration - that way it is cleaned
|
||||
// up and any incompatibilities will be removed!
|
||||
$cfgArr = $this->cfgString2CfgArray($bodyText);
|
||||
}
|
||||
// If a save button has been pressed, then save the new field content:
|
||||
if (isset($_POST['savedok']) || isset($_POST['saveandclosedok'])) {
|
||||
// Make TCEmain object:
|
||||
$tce = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
|
||||
$tce->stripslashes_values = 0;
|
||||
// Put content into the data array:
|
||||
$data = array();
|
||||
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = $bodyText;
|
||||
if ($this->special == 'formtype_mail') {
|
||||
$data[$this->P['table']][$this->P['uid']]['subheader'] = $this->FORMCFG['recipient'];
|
||||
}
|
||||
// Perform the update:
|
||||
$tce->start($data, array());
|
||||
$tce->process_datamap();
|
||||
// Re-load the record content:
|
||||
$row = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
|
||||
}
|
||||
// If the save/close or close button was pressed, then redirect the screen:
|
||||
if (isset($_POST['saveandclosedok']) || isset($_POST['closedok'])) {
|
||||
\TYPO3\CMS\Core\Utility\HttpUtility::redirect(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']));
|
||||
}
|
||||
} else {
|
||||
// If nothing has been submitted, load the $bodyText variable from the selected database row:
|
||||
if ($this->xmlStorage) {
|
||||
$cfgArr = GeneralUtility::xml2array($row[$this->P['field']]);
|
||||
} else {
|
||||
// Regular linebased form configuration:
|
||||
$cfgArr = $this->cfgString2CfgArray($row[$this->P['field']]);
|
||||
}
|
||||
$cfgArr = is_array($cfgArr) ? $cfgArr : array();
|
||||
}
|
||||
// Return configuration code:
|
||||
return $cfgArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the HTML for the Form Wizard:
|
||||
*
|
||||
* @param string $formCfgArray Form config array
|
||||
* @param array $row Current parent record array
|
||||
* @return string HTML for the form wizard
|
||||
* @access private
|
||||
*/
|
||||
public function getFormHTML($formCfgArray, $row)
|
||||
{
|
||||
// Initialize variables:
|
||||
$specParts = array();
|
||||
$hiddenFields = array();
|
||||
$tRows = array();
|
||||
// Set header row:
|
||||
$cells = array(
|
||||
$this->getLanguageService()->getLL('forms_preview', true) . ':',
|
||||
$this->getLanguageService()->getLL('forms_element', true) . ':',
|
||||
$this->getLanguageService()->getLL('forms_config', true) . ':'
|
||||
);
|
||||
$tRows[] = '
|
||||
<tr id="typo3-formWizardHeader">
|
||||
<th> </th>
|
||||
<th><strong>' . implode('</strong></th>
|
||||
<th><strong>', $cells) . '</strong></th>
|
||||
</tr>';
|
||||
// Traverse the number of form elements:
|
||||
$k = 0;
|
||||
foreach ($formCfgArray as $confData) {
|
||||
// Initialize:
|
||||
$cells = array();
|
||||
// If there is a configuration line which is active, then render it:
|
||||
if (!isset($confData['comment'])) {
|
||||
// Special parts:
|
||||
if ($this->special == 'formtype_mail' && GeneralUtility::inList('formtype_mail,subject,html_enabled', $confData['fieldname'])) {
|
||||
$specParts[$confData['fieldname']] = $confData['default'];
|
||||
} else {
|
||||
// Render title/field preview COLUMN
|
||||
$cells[] = $confData['type'] != 'hidden' ? '<strong>' . htmlspecialchars($confData['label']) . '</strong>' : '';
|
||||
// Render general type/title COLUMN:
|
||||
$temp_cells = array();
|
||||
// Field type selector:
|
||||
$opt = array();
|
||||
$opt[] = '<option value=""></option>';
|
||||
$types = explode(',', 'input,textarea,select,check,radio,password,file,hidden,submit,property,label');
|
||||
foreach ($types as $t) {
|
||||
$opt[] = '
|
||||
<option value="' . $t . '"' . ($confData['type'] == $t ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL(('forms_type_' . $t), true) . '</option>';
|
||||
}
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_type')] = '
|
||||
<select name="FORMCFG[c][' . ($k + 1) * 2 . '][type]">
|
||||
' . implode('
|
||||
', $opt) . '
|
||||
</select>';
|
||||
// Title field:
|
||||
if (!GeneralUtility::inList('hidden,submit', $confData['type'])) {
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_label')] = '<input type="text"' . $this->doc->formWidth(15) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][label]" value="' . htmlspecialchars($confData['label']) . '" />';
|
||||
}
|
||||
// Required checkbox:
|
||||
if (!GeneralUtility::inList('check,hidden,submit,label', $confData['type'])) {
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_required')] = '<input type="checkbox" name="FORMCFG[c][' . ($k + 1) * 2 . '][required]" value="1"' . ($confData['required'] ? ' checked="checked"' : '') . ' title="' . $this->getLanguageService()->getLL('forms_required', true) . '" />';
|
||||
}
|
||||
// Put sub-items together into table cell:
|
||||
$cells[] = $this->formatCells($temp_cells);
|
||||
// Render specific field configuration COLUMN:
|
||||
$temp_cells = array();
|
||||
// Fieldname
|
||||
if ($this->special == 'formtype_mail' && $confData['type'] == 'file') {
|
||||
$confData['fieldname'] = 'attachment' . ++$this->attachmentCounter;
|
||||
}
|
||||
if (!GeneralUtility::inList('label', $confData['type'])) {
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_fieldName')] = '<input type="text"' . $this->doc->formWidth(10) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][fieldname]" value="' . htmlspecialchars($confData['fieldname']) . '" title="' . $this->getLanguageService()->getLL('forms_fieldName', true) . '" />';
|
||||
}
|
||||
// Field configuration depending on the fields type:
|
||||
switch ((string)$confData['type']) {
|
||||
case 'textarea':
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_cols')] = '<input type="text"' . $this->doc->formWidth(5) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][cols]" value="' . htmlspecialchars($confData['cols']) . '" title="' . $this->getLanguageService()->getLL('forms_cols', true) . '" />';
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_rows')] = '<input type="text"' . $this->doc->formWidth(5) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][rows]" value="' . htmlspecialchars($confData['rows']) . '" title="' . $this->getLanguageService()->getLL('forms_rows', true) . '" />';
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_extra')] = '<input type="checkbox" name="FORMCFG[c][' . ($k + 1) * 2 . '][extra]" value="OFF"' . ($confData['extra'] == 'OFF' ? ' checked="checked"' : '') . ' title="' . $this->getLanguageService()->getLL('forms_extra', true) . '" />';
|
||||
break;
|
||||
case 'input':
|
||||
|
||||
case 'password':
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_size')] = '<input type="text"' . $this->doc->formWidth(5) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][size]" value="' . htmlspecialchars($confData['size']) . '" title="' . $this->getLanguageService()->getLL('forms_size', true) . '" />';
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_max')] = '<input type="text"' . $this->doc->formWidth(5) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][max]" value="' . htmlspecialchars($confData['max']) . '" title="' . $this->getLanguageService()->getLL('forms_max', true) . '" />';
|
||||
break;
|
||||
case 'file':
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_size')] = '<input type="text"' . $this->doc->formWidth(5) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][size]" value="' . htmlspecialchars($confData['size']) . '" title="' . $this->getLanguageService()->getLL('forms_size', true) . '" />';
|
||||
break;
|
||||
case 'select':
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_size')] = '<input type="text"' . $this->doc->formWidth(5) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][size]" value="' . htmlspecialchars($confData['size']) . '" title="' . $this->getLanguageService()->getLL('forms_size', true) . '" />';
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_autosize')] = '<input type="checkbox" name="FORMCFG[c][' . ($k + 1) * 2 . '][autosize]" value="1"' . ($confData['autosize'] ? ' checked="checked"' : '') . ' title="' . $this->getLanguageService()->getLL('forms_autosize', true) . '" />';
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_multiple')] = '<input type="checkbox" name="FORMCFG[c][' . ($k + 1) * 2 . '][multiple]" value="1"' . ($confData['multiple'] ? ' checked="checked"' : '') . ' title="' . $this->getLanguageService()->getLL('forms_multiple', true) . '" />';
|
||||
break;
|
||||
}
|
||||
// Field configuration depending on the fields type:
|
||||
switch ((string)$confData['type']) {
|
||||
case 'textarea':
|
||||
|
||||
case 'input':
|
||||
|
||||
case 'password':
|
||||
if (trim($confData['specialEval']) !== '') {
|
||||
$hiddenFields[] = '<input type="hidden" name="FORMCFG[c][' . ($k + 1) * 2 . '][specialEval]" value="' . htmlspecialchars($confData['specialEval']) . '" />';
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Default data
|
||||
if ($confData['type'] == 'select' || $confData['type'] == 'radio') {
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_options')] = '<textarea ' . $this->doc->formWidth(15) . ' rows="4" name="FORMCFG[c][' . ($k + 1) * 2 . '][options]" title="' . $this->getLanguageService()->getLL('forms_options', true) . '">' . htmlspecialchars($confData['default']) . '</textarea>';
|
||||
} elseif ($confData['type'] == 'check') {
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_checked')] = '<input type="checkbox" name="FORMCFG[c][' . ($k + 1) * 2 . '][default]" value="1"' . (trim($confData['default']) ? ' checked="checked"' : '') . ' title="' . $this->getLanguageService()->getLL('forms_checked', true) . '" />';
|
||||
} elseif ($confData['type'] && $confData['type'] != 'file') {
|
||||
$temp_cells[$this->getLanguageService()->getLL('forms_default')] = '<input type="text"' . $this->doc->formWidth(15) . ' name="FORMCFG[c][' . ($k + 1) * 2 . '][default]" value="' . htmlspecialchars($confData['default']) . '" title="' . $this->getLanguageService()->getLL('forms_default', true) . '" />';
|
||||
}
|
||||
$cells[] = $confData['type'] ? $this->formatCells($temp_cells) : '';
|
||||
// CTRL panel for an item (move up/down/around):
|
||||
$ctrl = '';
|
||||
$onClick = 'document.wizardForm.action+=\'#ANC_' . (($k + 1) * 2 - 2) . '\';';
|
||||
$onClick = ' onclick="' . htmlspecialchars($onClick) . '"';
|
||||
// @todo $inputStyle undefined
|
||||
$brTag = $inputStyle ? '' : '<br />';
|
||||
if ($k != 1) {
|
||||
$ctrl .= '<button name="FORMCFG[row_top][' . ($k + 1) * 2 . ']"' . $onClick . ' title="' . $this->getLanguageService()->getLL('table_top', true) . '">' . $this->iconFactory->getIcon('actions-move-to-top', Icon::SIZE_SMALL)->render() . '</button>' . $brTag;
|
||||
$ctrl .= '<button name="FORMCFG[row_up][' . ($k + 1) * 2 . ']"' . $onClick . ' title="' . $this->getLanguageService()->getLL('table_up', true) . '">' . $this->iconFactory->getIcon('actions-move-up', Icon::SIZE_SMALL)->render() . '</button>' . $brTag;
|
||||
}
|
||||
$ctrl .= '<button name="FORMCFG[row_remove][' . ($k + 1) * 2 . ']" ' . $onClick . ' title = "' . $this->getLanguageService()->getLL('table_removeRow', true) . '">' . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</button>' . $brTag;
|
||||
|
||||
if ($k != (count($formCfgArray)/2)) {
|
||||
$ctrl .= '<button name="FORMCFG[row_down][' . ($k + 1) * 2 . ']"' . $onClick . ' title="' . $this->getLanguageService()->getLL('table_down', true) . '">' . $this->iconFactory->getIcon('actions-move-down', Icon::SIZE_SMALL)->render() . '</button>' . $brTag;
|
||||
$ctrl .= '<button name="FORMCFG[row_bottom][' . ($k + 1) * 2 . ']"' . $onClick . ' title="' . $this->getLanguageService()->getLL('table_bottom', true) . '">' . $this->iconFactory->getIcon('actions-move-to-bottom', Icon::SIZE_SMALL)->render() . '</button>' . $brTag;
|
||||
}
|
||||
|
||||
$ctrl .= '<button name="FORMCFG[row_add][' . ($k + 1) * 2 . ']"' . $onClick . ' title="' . $this->getLanguageService()->getLL('table_addRow', true) . '">' . $this->iconFactory->getIcon('actions-template-new', Icon::SIZE_SMALL)->render() . '</button>' . $brTag;
|
||||
$ctrl = '<span class="c-wizButtonsV">' . $ctrl . '</span>';
|
||||
// Finally, put together the full row from the generated content above:
|
||||
$tRows[] = '
|
||||
<tr>
|
||||
<td><a name="ANC_' . ($k + 1) * 2 . '"></a>' . $ctrl . '</td>
|
||||
<td>' . implode('</td>
|
||||
<td valign="top">', $cells) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
} else {
|
||||
$hiddenFields[] = '<input type="hidden" name="FORMCFG[c][' . ($k + 1) * 2 . '][comment]" value="' . htmlspecialchars($confData['comment']) . '" />';
|
||||
}
|
||||
// Increment counter:
|
||||
$k++;
|
||||
}
|
||||
// If the form is of the special type "formtype_mail" (used for tt_content elements):
|
||||
if ($this->special == 'formtype_mail') {
|
||||
// Blank spacer:
|
||||
$tRows[] = '
|
||||
<tr>
|
||||
<td colspan="4"> </td>
|
||||
</tr>';
|
||||
// Header:
|
||||
$tRows[] = '
|
||||
<tr>
|
||||
<th colspan="4"><h4>' . $this->getLanguageService()->getLL('forms_special_eform', true) . ': ' . BackendUtility::cshItem('xMOD_csh_corebe', 'wizard_forms_wiz_formmail_info') . '</h4></th>
|
||||
</tr>';
|
||||
// "FORM type":
|
||||
$tRows[] = '
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
<td>' . $this->getLanguageService()->getLL('forms_eform_formtype_mail', true) . ':</td>
|
||||
<td>
|
||||
<input type="hidden" name="FORMCFG[c][' . 1000 * 2 . '][fieldname]" value="formtype_mail" />
|
||||
<input type="hidden" name="FORMCFG[c][' . 1000 * 2 . '][type]" value="submit" />
|
||||
<input type="text"' . $this->doc->formWidth(15) . ' name="FORMCFG[c][' . 1000 * 2 . '][default]" value="' . htmlspecialchars($specParts['formtype_mail']) . '" />
|
||||
</td>
|
||||
</tr>';
|
||||
// "Send HTML mail":
|
||||
$tRows[] = '
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
<td>' . $this->getLanguageService()->getLL('forms_eform_html_enabled', true) . ':</td>
|
||||
<td>
|
||||
<input type="hidden" name="FORMCFG[c][' . 1001 * 2 . '][fieldname]" value="html_enabled" />
|
||||
<input type="hidden" name="FORMCFG[c][' . 1001 * 2 . '][type]" value="hidden" />
|
||||
<input type="checkbox" name="FORMCFG[c][' . 1001 * 2 . '][default]" value="1"' . ($specParts['html_enabled'] ? ' checked="checked"' : '') . ' />
|
||||
</td>
|
||||
</tr>';
|
||||
// "Subject":
|
||||
$tRows[] = '
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
<td>' . $this->getLanguageService()->getLL('forms_eform_subject', true) . ':</td>
|
||||
<td>
|
||||
<input type="hidden" name="FORMCFG[c][' . 1002 * 2 . '][fieldname]" value="subject" />
|
||||
<input type="hidden" name="FORMCFG[c][' . 1002 * 2 . '][type]" value="hidden" />
|
||||
<input type="text"' . $this->doc->formWidth(15) . ' name="FORMCFG[c][' . 1002 * 2 . '][default]" value="' . htmlspecialchars($specParts['subject']) . '" />
|
||||
</td>
|
||||
</tr>';
|
||||
// Recipient:
|
||||
$tRows[] = '
|
||||
<tr>
|
||||
<td colspan="2"> </td>
|
||||
<td>' . $this->getLanguageService()->getLL('forms_eform_recipient', true) . ':</td>
|
||||
<td>
|
||||
<input type="text"' . $this->doc->formWidth(15) . ' name="FORMCFG[recipient]" value="' . htmlspecialchars($row['subheader']) . '" />
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
$content = '';
|
||||
// Implode all table rows into a string, wrapped in table tags.
|
||||
$content .= '
|
||||
|
||||
<!--
|
||||
Form wizard
|
||||
-->
|
||||
<table class="table table-bordered table-condensed" id="typo3-formwizard">
|
||||
' . implode('', $tRows) . '
|
||||
</table>';
|
||||
// Add hidden fields:
|
||||
$content .= implode('', $hiddenFields);
|
||||
// Return content:
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a control button (up/down/around/delete) has been pressed for an item and accordingly it will manipulate the internal FORMCFG array
|
||||
*
|
||||
* @return void
|
||||
* @access private
|
||||
*/
|
||||
public function changeFunc()
|
||||
{
|
||||
if ($this->FORMCFG['row_remove']) {
|
||||
$kk = key($this->FORMCFG['row_remove']);
|
||||
$cmd = 'row_remove';
|
||||
} elseif ($this->FORMCFG['row_add']) {
|
||||
$kk = key($this->FORMCFG['row_add']);
|
||||
$cmd = 'row_add';
|
||||
} elseif ($this->FORMCFG['row_top']) {
|
||||
$kk = key($this->FORMCFG['row_top']);
|
||||
$cmd = 'row_top';
|
||||
} elseif ($this->FORMCFG['row_bottom']) {
|
||||
$kk = key($this->FORMCFG['row_bottom']);
|
||||
$cmd = 'row_bottom';
|
||||
} elseif ($this->FORMCFG['row_up']) {
|
||||
$kk = key($this->FORMCFG['row_up']);
|
||||
$cmd = 'row_up';
|
||||
} elseif ($this->FORMCFG['row_down']) {
|
||||
$kk = key($this->FORMCFG['row_down']);
|
||||
$cmd = 'row_down';
|
||||
}
|
||||
if ($cmd && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($kk)) {
|
||||
if (substr($cmd, 0, 4) == 'row_') {
|
||||
switch ($cmd) {
|
||||
case 'row_remove':
|
||||
unset($this->FORMCFG['c'][$kk]);
|
||||
break;
|
||||
case 'row_add':
|
||||
$this->FORMCFG['c'][$kk + 1] = array();
|
||||
break;
|
||||
case 'row_top':
|
||||
$this->FORMCFG['c'][1] = $this->FORMCFG['c'][$kk];
|
||||
unset($this->FORMCFG['c'][$kk]);
|
||||
break;
|
||||
case 'row_bottom':
|
||||
$this->FORMCFG['c'][1000000] = $this->FORMCFG['c'][$kk];
|
||||
unset($this->FORMCFG['c'][$kk]);
|
||||
break;
|
||||
case 'row_up':
|
||||
$this->FORMCFG['c'][$kk - 3] = $this->FORMCFG['c'][$kk];
|
||||
unset($this->FORMCFG['c'][$kk]);
|
||||
break;
|
||||
case 'row_down':
|
||||
$this->FORMCFG['c'][$kk + 3] = $this->FORMCFG['c'][$kk];
|
||||
unset($this->FORMCFG['c'][$kk]);
|
||||
break;
|
||||
}
|
||||
ksort($this->FORMCFG['c']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the input array to a configuration code string
|
||||
*
|
||||
* @param array $cfgArr Array of form configuration (follows the input structure from the form wizard POST form)
|
||||
* @return string The array converted into a string with line-based configuration.
|
||||
* @see cfgString2CfgArray()
|
||||
*/
|
||||
public function cfgArray2CfgString($cfgArr)
|
||||
{
|
||||
// Initialize:
|
||||
$inLines = array();
|
||||
// Traverse the elements of the form wizard and transform the settings into configuration code.
|
||||
foreach ($cfgArr as $vv) {
|
||||
// If "content" is found, then just pass it over.
|
||||
if ($vv['comment']) {
|
||||
$inLines[] = trim($vv['comment']);
|
||||
} else {
|
||||
// Begin to put together the single-line configuration code of this field:
|
||||
// Reset:
|
||||
$thisLine = array();
|
||||
// Set Label:
|
||||
$thisLine[0] = str_replace('|', '', $vv['label']);
|
||||
// Set Type:
|
||||
if ($vv['type']) {
|
||||
$thisLine[1] = ($vv['required'] ? '*' : '') . str_replace(',', '', (($vv['fieldname'] ? $vv['fieldname'] . '=' : '') . $vv['type']));
|
||||
// Default:
|
||||
$tArr = array('', '', '', '', '', '');
|
||||
switch ((string)$vv['type']) {
|
||||
case 'textarea':
|
||||
if ((int)$vv['cols']) {
|
||||
$tArr[0] = (int)$vv['cols'];
|
||||
}
|
||||
if ((int)$vv['rows']) {
|
||||
$tArr[1] = (int)$vv['rows'];
|
||||
}
|
||||
if (trim($vv['extra'])) {
|
||||
$tArr[2] = trim($vv['extra']);
|
||||
}
|
||||
if ($vv['specialEval'] !== '') {
|
||||
// Preset blank default value so position 3 can get a value...
|
||||
$thisLine[2] = '';
|
||||
$thisLine[3] = $vv['specialEval'];
|
||||
}
|
||||
break;
|
||||
case 'input':
|
||||
case 'password':
|
||||
if ((int)$vv['size']) {
|
||||
$tArr[0] = (int)$vv['size'];
|
||||
}
|
||||
if ((int)$vv['max']) {
|
||||
$tArr[1] = (int)$vv['max'];
|
||||
}
|
||||
if ($vv['specialEval'] !== '') {
|
||||
// Preset blank default value so position 3 can get a value...
|
||||
$thisLine[2] = '';
|
||||
$thisLine[3] = $vv['specialEval'];
|
||||
}
|
||||
break;
|
||||
case 'file':
|
||||
if ((int)$vv['size']) {
|
||||
$tArr[0] = (int)$vv['size'];
|
||||
}
|
||||
break;
|
||||
case 'select':
|
||||
if ((int)$vv['size']) {
|
||||
$tArr[0] = (int)$vv['size'];
|
||||
}
|
||||
if ($vv['autosize']) {
|
||||
$tArr[0] = 'auto';
|
||||
}
|
||||
if ($vv['multiple']) {
|
||||
$tArr[1] = 'm';
|
||||
}
|
||||
break;
|
||||
}
|
||||
$tArr = $this->cleanT($tArr);
|
||||
if (!empty($tArr)) {
|
||||
$thisLine[1] .= ',' . implode(',', $tArr);
|
||||
}
|
||||
$thisLine[1] = str_replace('|', '', $thisLine[1]);
|
||||
// Default:
|
||||
if ($vv['type'] == 'select' || $vv['type'] == 'radio') {
|
||||
$options = str_replace(',', '', $vv['options']);
|
||||
$options = str_replace(
|
||||
array(CRLF, CR, LF),
|
||||
', ',
|
||||
$options);
|
||||
$thisLine[2] = $options;
|
||||
} elseif ($vv['type'] == 'check') {
|
||||
if ($vv['default']) {
|
||||
$thisLine[2] = 1;
|
||||
}
|
||||
} elseif (trim($vv['default']) !== '') {
|
||||
$thisLine[2] = $vv['default'];
|
||||
}
|
||||
if (isset($thisLine[2])) {
|
||||
$thisLine[2] = str_replace('|', '', $thisLine[2]);
|
||||
}
|
||||
}
|
||||
// Compile the final line:
|
||||
$inLines[] = preg_replace('/[
|
||||
|
||||
]*/', '', implode(' | ', $thisLine));
|
||||
}
|
||||
}
|
||||
// Finally, implode the lines into a string, and return it:
|
||||
return implode(LF, $inLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the input configuration code string into an array
|
||||
*
|
||||
* @param string $cfgStr Configuration code
|
||||
* @return array Configuration array
|
||||
* @see cfgArray2CfgString()
|
||||
*/
|
||||
public function cfgString2CfgArray($cfgStr)
|
||||
{
|
||||
// Traverse the number of form elements:
|
||||
$tLines = explode(LF, $cfgStr);
|
||||
$attachmentCounter = 0;
|
||||
foreach ($tLines as $k => $v) {
|
||||
// Initialize:
|
||||
$confData = array();
|
||||
$val = trim($v);
|
||||
// Accept a line as configuration if a) it is blank(! - because blank lines indicates new,
|
||||
// unconfigured fields) or b) it is NOT a comment.
|
||||
if (!$val || strcspn($val, '#/')) {
|
||||
// Split:
|
||||
$parts = GeneralUtility::trimExplode('|', $val);
|
||||
// Label:
|
||||
$confData['label'] = trim($parts[0]);
|
||||
// Field:
|
||||
$fParts = GeneralUtility::trimExplode(',', $parts[1]);
|
||||
$fParts[0] = trim($fParts[0]);
|
||||
if ($fParts[0][0] === '*') {
|
||||
$confData['required'] = 1;
|
||||
$fParts[0] = substr($fParts[0], 1);
|
||||
}
|
||||
$typeParts = GeneralUtility::trimExplode('=', $fParts[0]);
|
||||
$confData['type'] = trim(strtolower(end($typeParts)));
|
||||
if ($confData['type']) {
|
||||
if (count($typeParts) === 1) {
|
||||
$confData['fieldname'] = substr(preg_replace('/[^a-zA-Z0-9_]/', '', str_replace(' ', '_', trim($parts[0]))), 0, 30);
|
||||
// Attachment names...
|
||||
if ($confData['type'] == 'file') {
|
||||
$confData['fieldname'] = 'attachment' . $attachmentCounter;
|
||||
$attachmentCounter = (int)$attachmentCounter + 1;
|
||||
}
|
||||
} else {
|
||||
$confData['fieldname'] = str_replace(' ', '_', trim($typeParts[0]));
|
||||
}
|
||||
switch ((string)$confData['type']) {
|
||||
case 'select':
|
||||
case 'radio':
|
||||
$confData['default'] = implode(LF, GeneralUtility::trimExplode(',', $parts[2]));
|
||||
break;
|
||||
default:
|
||||
$confData['default'] = trim($parts[2]);
|
||||
}
|
||||
// Field configuration depending on the fields type:
|
||||
switch ((string)$confData['type']) {
|
||||
case 'textarea':
|
||||
$confData['cols'] = $fParts[1];
|
||||
$confData['rows'] = $fParts[2];
|
||||
$confData['extra'] = strtoupper($fParts[3]) == 'OFF' ? 'OFF' : '';
|
||||
$confData['specialEval'] = trim($parts[3]);
|
||||
break;
|
||||
case 'input':
|
||||
case 'password':
|
||||
$confData['size'] = $fParts[1];
|
||||
$confData['max'] = $fParts[2];
|
||||
$confData['specialEval'] = trim($parts[3]);
|
||||
break;
|
||||
case 'file':
|
||||
$confData['size'] = $fParts[1];
|
||||
break;
|
||||
case 'select':
|
||||
$confData['size'] = (int)$fParts[1] ? $fParts[1] : '';
|
||||
$confData['autosize'] = strtolower(trim($fParts[1])) === 'auto' ? 1 : 0;
|
||||
$confData['multiple'] = strtolower(trim($fParts[2])) === 'm' ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No configuration, only a comment:
|
||||
$confData = array(
|
||||
'comment' => $val
|
||||
);
|
||||
}
|
||||
// Adding config array:
|
||||
$cfgArr[] = $confData;
|
||||
}
|
||||
// Return cfgArr
|
||||
return $cfgArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any "trailing elements" in the array which consists of whitespace (little like trim() does for strings, so this does for arrays)
|
||||
*
|
||||
* @param array $tArr Single dim array
|
||||
* @return array Processed array
|
||||
* @access private
|
||||
*/
|
||||
public function cleanT($tArr)
|
||||
{
|
||||
for ($a = count($tArr); $a > 0; $a--) {
|
||||
if ((string)$tArr[$a - 1] !== '') {
|
||||
break;
|
||||
} else {
|
||||
unset($tArr[$a - 1]);
|
||||
}
|
||||
}
|
||||
return $tArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps items in $fArr in table cells/rows, displaying them vertically.
|
||||
*
|
||||
* @param array $fArr Array of label/HTML pairs.
|
||||
* @return string HTML table
|
||||
* @access private
|
||||
*/
|
||||
public function formatCells($fArr)
|
||||
{
|
||||
// Traverse the elements in $fArr and wrap them in table cells:
|
||||
$lines = array();
|
||||
foreach ($fArr as $l => $c) {
|
||||
$lines[] = '
|
||||
<tr>
|
||||
<td nowrap="nowrap">' . htmlspecialchars(($l . ':')) . ' </td>
|
||||
<td>' . $c . '</td>
|
||||
</tr>';
|
||||
}
|
||||
$lines[] = '
|
||||
<tr>
|
||||
<td colspan="2"></td>
|
||||
</tr>';
|
||||
// Wrap in table and return:
|
||||
return '
|
||||
<table>
|
||||
' . implode('', $lines) . '
|
||||
</table>';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user