Commit 00d02c9d authored by Marc Egger's avatar Marc Egger

Refs #9600 Edit report file using frontend editor.

parent 29ac8bc8
Pipeline #3681 failed with stages
in 2 minutes and 22 seconds
......@@ -1461,6 +1461,7 @@ const T3DATA_BODYTEXT_RAW = 'bodytext-raw';
const T3DATA_UID = 'uid';
const T3DATA_PID = 'pid';
const T3DATA_HEADER = 'header';
const T3DATA_REPORT_PATH_FILENAME = 'reportPathFileName';
const REPORT_INLINE_BODYTEXT = 'bodytext';
const REPORT_FILE_EXTENSION = '.qfqr';
......@@ -1545,6 +1546,7 @@ const TOKEN_RECORD_ID = CLIENT_RECORD_ID;
const TOKEN_DEBUG_BODYTEXT = TYPO3_DEBUG_SHOW_BODY_TEXT;
const TOKEN_DB_INDEX = F_DB_INDEX;
const TOKEN_CONTENT = 'content';
const TOKEN_REPORT_FILE = 'file';
const TOKEN_VALID_LIST = 'sql|twig|head|althead|altsql|tail|shead|stail|rbeg|rend|renr|rsep|fbeg|fend|fsep|fskipwrap|rbgd|debug|form|r|debugShowBodyText|dbIndex|sqlLog|sqlLogMode|content|render';
......
......@@ -557,7 +557,7 @@ class HelperFile {
* @param $string
* @return string
*/
public static function sanitizeFileName($string): ?string
public static function sanitizeFileName($string) // : ?string
{
$res = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $string), '-'));
$res = preg_replace('~-+~', '-', $res);
......@@ -573,7 +573,7 @@ class HelperFile {
* @param $path
* @throws \UserFormException
*/
public static function createPathRecursive($path): void
public static function createPathRecursive($path) // : void
{
if (!is_dir($path)) {
$success = mkdir($path, 0777, true);
......@@ -585,5 +585,23 @@ class HelperFile {
}
}
}
/**
* Wrapper for file_put_contents which throws exception on failure.
*
* @param $pathFileName
* @param $content
* @throws \UserFormException
*/
public static function file_put_contents($pathFileName, $content) // : void
{
$success = file_put_contents($pathFileName, $content);
if ($success === false) {
throw new \UserFormException(json_encode([
ERROR_MESSAGE_TO_USER => "Writing file failed.",
ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$pathFileName'"]),
ERROR_IO_WRITE_FILE);
}
}
}
......@@ -146,17 +146,10 @@ class QuickFormQuery {
$t3data[T3DATA_UID] = 0;
}
// Read report file if file keyword exists
// Read report file, if file keyword exists in bodytext
$reportPathFileNameFull = ReportAsFile::parseFileKeyword($t3data[T3DATA_BODYTEXT]);
if ($reportPathFileNameFull !== null) {
$fileContents = file_get_contents($reportPathFileNameFull);
if ($fileContents === false) {
throw new \UserReportException(json_encode([
ERROR_MESSAGE_TO_USER => "File read error.",
ERROR_MESSAGE_TO_DEVELOPER => "Report file not found or no permission to read file: '$reportPathFileNameFull'"]),
ERROR_FORM_NOT_FOUND);
}
$t3data[T3DATA_BODYTEXT] = $fileContents;
$t3data[T3DATA_BODYTEXT] = ReportAsFile::read_report_file($reportPathFileNameFull);
}
$btp = new BodytextParser();
......@@ -203,67 +196,13 @@ class QuickFormQuery {
$this->store->setVar(TOKEN_DB_INDEX, $dbIndex, STORE_TYPO3);
// Create report file if file keyword not found
// TODO: ?CR: $t3data might not contain the following values. Or uid could be 0.
// TODO: ?CR: uid could be 0. When does this happen?
if ($reportPathFileNameFull === null && $t3data[T3DATA_UID] !== 0) {
// read page path of tt-content element (use page alias if exists)
$dbT3 = $this->store->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
$reportPath = '';
$pid = $t3data[T3DATA_PID];
while (intval($pid) !== 0) {
$sql = "SELECT `pid`, `alias`, `title` FROM `$dbT3`.`pages` WHERE `uid` = ?";
$page = $this->dbArray[$this->dbIndexData]->sql($sql, ROW_EXPECT_1,
[$pid], "Typo3 page not found. Uid: $pid");
$supDir = $page['alias'] !== '' ? $page['alias'] : $page['title'];
$reportPath = HelperFile::joinPathFilename(HelperFile::sanitizeFileName($supDir), $reportPath);
$pid = $page['pid'];
if (strlen($reportPath) > 4096) {
// Throw exception in case of infinite loop.
throw new \UserReportException(json_encode([
ERROR_MESSAGE_TO_USER => "Path too long.",
ERROR_MESSAGE_TO_DEVELOPER => "Report path too long: '$reportPath'"]),
ERROR_FORM_NOT_FOUND);
}
}
$reportPathFull = HelperFile::joinPathFilename(ReportAsFile::reportPath(), $reportPath);
// create path in filesystem if not exists
HelperFile::createPathRecursive($reportPathFull);
// add filename
$fileName = HelperFile::sanitizeFileName($t3data[T3DATA_HEADER]);
if ($fileName === null) {
$fileName = strval($t3data[T3DATA_UID]);
}
$reportPathFileNameFull = HelperFile::joinPathFilename($reportPathFull, $fileName) . REPORT_FILE_EXTENSION;
// if file exists, throw exception
if (file_exists($reportPathFileNameFull)) {
throw new \UserReportException(json_encode([
ERROR_MESSAGE_TO_USER => "Writing file failed.",
ERROR_MESSAGE_TO_DEVELOPER => "Can't export report to file. File already exists: '$reportPathFileNameFull'"]),
ERROR_FORM_NOT_FOUND);
}
// create file with content bodytext
$success = file_put_contents($reportPathFileNameFull, $t3data[T3DATA_BODYTEXT_RAW]);
if ($success === false) {
throw new \UserFormException(json_encode([
ERROR_MESSAGE_TO_USER => "Writing file failed.",
ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$reportPathFileNameFull'"]),
ERROR_IO_WRITE_FILE);
}
// replace tt-content bodytext with file=<path>
$newBodytext = "file=" . HelperFile::joinPathFilename($reportPath, $fileName) . REPORT_FILE_EXTENSION;
$sql = "UPDATE `$dbT3`.`tt_content` SET `bodytext` = ?, `tstamp` = UNIX_TIMESTAMP(NOW()) WHERE `uid` = ?";
$this->dbArray[$this->dbIndexData]->sql($sql, ROW_REGULAR, [$newBodytext, $t3data[T3DATA_UID]]);
// Clear cache
// Need to truncate cf_cache_pages because it is used to restore page-specific cache
$sql = "DELETE FROM `$dbT3`.`cf_cache_pages`";
$this->dbArray[$this->dbIndexData]->sql($sql);
$reportPathFileNameFull = ReportAsFile::create_file_from_ttContent($t3data[T3DATA_UID], $this->dbArray[$this->dbIndexData]);
}
// Save pathFileName for use in inline editor
$this->t3data[T3DATA_REPORT_PATH_FILENAME] = $reportPathFileNameFull;
}
/**
......@@ -1792,15 +1731,16 @@ class QuickFormQuery {
return $html;
}
/** Constructs a form to directly edit qfq content elements inline.
/**
* Constructs a form to directly edit qfq content elements inline.
*
* @return string - the html code
* @throws \CodeException
* @throws \UserFormException
*/
private function buildInlineReport() {
// TODO: replace $uid with file path
$uid = $this->t3data[T3DATA_UID];
$reportPathFileNameFull = $this->t3data[T3DATA_REPORT_PATH_FILENAME];
$bodytext = $this->t3data[T3DATA_BODYTEXT_RAW];
$header = $this->t3data[T3DATA_HEADER];
......@@ -1815,7 +1755,7 @@ class QuickFormQuery {
Support::doAttribute('title', 'Save & Reload');
$saveBtnIcon = Support::renderGlyphIcon(GLYPH_ICON_CHECK);
$saveBtn = Support::wrapTag("<button $saveBtnAttributes>", $saveBtnIcon);
$header = "QFQ Page Content '$header'";
$header = "QFQ Page Content '$header'.<br><small>File: '$reportPathFileNameFull'</small>";
$headerBar = Support::wrapTag("<div class='col-md-12 qfq-form-title'>", $header . $saveBtn);
$ttContentCode = Support::htmlEntityEncodeDecode(MODE_ENCODE, $bodytext);
......@@ -1841,28 +1781,17 @@ class QuickFormQuery {
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
public function saveReport() {
// TODO: replace $uid with file path
$uid = $this->store->getVar(T3DATA_UID, STORE_SIP . STORE_ZERO, SANITIZE_ALLOW_DIGIT);
if ($uid == 0) {
// Check if it was called with a SIP (containing a uid)
// If not, this might be an attack => cancel.
return;
}
$bodytext = Support::htmlEntityEncodeDecode(MODE_DECODE, $_POST[REPORT_INLINE_BODYTEXT]);
$dbT3 = $this->store->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
// Update bodytext
$sql = "UPDATE `$dbT3`.`tt_content` SET `bodytext` = ?, `tstamp` = UNIX_TIMESTAMP(NOW()) WHERE `uid` = ?";
$this->dbArray[$this->dbIndexData]->sql($sql, ROW_REGULAR, [$bodytext, $uid]);
// Clear cache
// Need to truncate cf_cache_pages because it is used to restore page-specific cache
$sql = "DELETE FROM `$dbT3`.`cf_cache_pages`";
$this->dbArray[$this->dbIndexData]->sql($sql);
$bodytextNew = Support::htmlEntityEncodeDecode(MODE_DECODE, $_POST[REPORT_INLINE_BODYTEXT]);
ReportAsFile::write_file_uid($uid, $bodytextNew, $this->dbArray[$this->dbIndexData]);
$this->formSpec[F_FORWARD_MODE] = 'auto';
}
......
<?php
// TODO: QuickFormQuery.php: edit saveReport() such that it saves to file instead of DB. Search T3DATA_UID
// pass the file path instead of uid in sip. Escape could be a problem. Example: Search KeyValueStringParser::unparse($t3varsArray
// TODO: Add filename to Report Editor. Search T3DATA_HEADER
// TODO: abstract all these IO functions which throw exceptions
// TODO: replace keyword 'file' with constant
// TODO: show frontend editor even when an exception occured during report rendering
// Solution: in rendering of exception, add the editor to the html.
// TODO: frontend edit komplett auschalten koennen (CR)
// TODO: volle kontrolle ueber code, dass der nicht pauschal ausgeliefert wird. (CR)
// TODO: zentrale konfig, ob es angezeigt wird. (sekreteaerin soll nicht sehen obwohl BE login) (CR)
// TODO: pro user option, ob gezeigt wird. (CR)
// TODO: fix unittests run by gitlab runner.
// TODO: ?CR: QuickFormQuery.php L138: Why fallback $t3data[T3DATA_UID] = 0; and $t3data[T3DATA_BODYTEXT] = ''; ? When does this happen?
// TODO: ?CR: alle ?CR suchen und durchgehen.
namespace IMATHUZH\Qfq\Core\Report;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Store\Store;
class ReportAsFile
{
/**
* Finds the keyword file=<pathFileName> and returns pathFileName of report.
* Finds the keyword file=<pathFileName> and returns pathFileName of report relative to CWD.
*
* @param string $bodyText
* @return string|null ReportPathFileName
* @return string|null pathFileName of report relative to CWD.
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
*/
public static function parseFileKeyword(string $bodyText): ?string
public static function parseFileKeyword(string $bodyText) // : ?string
{
if (preg_match('/^\s*file\s*=\s*([^\s]*)/m', $bodyText, $matches)) {
if (preg_match('/^\s*' . TOKEN_REPORT_FILE . '\s*=\s*([^\s]*)/m', $bodyText, $matches)) {
return HelperFile::joinPathFilename(self::reportPath(), $matches[1]);
} else {
return null;
}
}
/**
* Read report file from given pathFileName. Throw exception on failure.
*
* @param string $reportPathFileNameFull
* @return string|null
* @throws \UserReportException
*/
public static function read_report_file(string $reportPathFileNameFull): string
{
$fileContents = file_get_contents($reportPathFileNameFull);
if ($fileContents === false) {
throw new \UserReportException(json_encode([
ERROR_MESSAGE_TO_USER => "File read error.",
ERROR_MESSAGE_TO_DEVELOPER => "Report file not found or no permission to read file: '$reportPathFileNameFull'"]),
ERROR_FORM_NOT_FOUND);
}
return $fileContents;
}
/**
* Create new file named after the tt_content header or the UID if not defined.
* - Invalid characters are stripped from filename.
* - If file exists, throw exception.
* - The path of the file is derived the typo3 page structure (use page alias if exists, else page name).
* - The path is created if not exists.
*
* @param int $uid
* @param Database $databaseT3
* @return string pathFileName of report relative to CWD
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
public static function create_file_from_ttContent(int $uid, Database $databaseT3): string
{
// Select tt_content
$dbT3 = Store::getInstance()->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
$sql = "SELECT `bodytext`, `header`, `pid` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
$ttContent = $databaseT3->sql($sql, ROW_EXPECT_1,
[$uid], "Typo3 content element not found. Uid: $uid");
// read page path of tt-content element (use page alias if exists)
$reportPath = '';
$pid = $ttContent['pid'];
while (intval($pid) !== 0) {
$sql = "SELECT `pid`, `alias`, `title` FROM `$dbT3`.`pages` WHERE `uid` = ?";
$page = $databaseT3->sql($sql, ROW_EXPECT_1,
[$pid], "Typo3 page not found. Uid: $pid");
$supDir = $page['alias'] !== '' ? $page['alias'] : $page['title'];
$reportPath = HelperFile::joinPathFilename(HelperFile::sanitizeFileName($supDir), $reportPath);
$pid = $page['pid'];
if (strlen($reportPath) > 4096) {
// Throw exception in case of infinite loop.
throw new \UserReportException(json_encode([
ERROR_MESSAGE_TO_USER => "Path too long.",
ERROR_MESSAGE_TO_DEVELOPER => "Report path too long: '$reportPath'"]),
ERROR_FORM_NOT_FOUND);
}
}
$reportPathFull = HelperFile::joinPathFilename(ReportAsFile::reportPath(), $reportPath);
// create path in filesystem if not exists
HelperFile::createPathRecursive($reportPathFull);
// add filename
$fileName = HelperFile::sanitizeFileName($ttContent['header']);
if ($fileName === null) {
$fileName = strval($uid);
}
$reportPathFileNameFull = HelperFile::joinPathFilename($reportPathFull, $fileName) . REPORT_FILE_EXTENSION;
// if file exists, throw exception
if (file_exists($reportPathFileNameFull)) {
throw new \UserReportException(json_encode([
ERROR_MESSAGE_TO_USER => "Writing file failed.",
ERROR_MESSAGE_TO_DEVELOPER => "Can't export report to file. File already exists: '$reportPathFileNameFull'"]),
ERROR_FORM_NOT_FOUND);
}
// create file with content bodytext
HelperFile::file_put_contents($reportPathFileNameFull, $ttContent['bodytext']);
// replace tt-content bodytext with file=<path>
$newBodytext = TOKEN_REPORT_FILE . "=" . HelperFile::joinPathFilename($reportPath, $fileName) . REPORT_FILE_EXTENSION;
$sql = "UPDATE `$dbT3`.`tt_content` SET `bodytext` = ?, `tstamp` = UNIX_TIMESTAMP(NOW()) WHERE `uid` = ?";
$databaseT3->sql($sql, ROW_REGULAR, [$newBodytext, $uid]);
// Clear cache
// Need to truncate cf_cache_pages because it is used to restore page-specific cache
$sql = "DELETE FROM `$dbT3`.`cf_cache_pages`";
$databaseT3->sql($sql);
return $reportPathFileNameFull;
}
/**
* Overwrites report file given by the 'file=<path>' keyword of the tt_content element with the given uid.
*
* @param int $uid
* @param string $newContent
* @param Database $databaseT3
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
* @throws \UserReportException
*/
public static function write_file_uid(int $uid, string $newContent, Database $databaseT3) // : void
{
// Get the report file path from tt_content
$dbT3 = Store::getInstance()->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
$sql = "SELECT `bodytext` FROM `$dbT3`.`tt_content` WHERE `uid` = ?";
$ttContent = $databaseT3->sql($sql, ROW_EXPECT_1,
[$uid], "Typo3 content element not found. Uid: $uid");
$reportPathFileNameFull = ReportAsFile::parseFileKeyword($ttContent['bodytext']);
if ($reportPathFileNameFull === null) {
throw new \UserReportException(json_encode([
ERROR_MESSAGE_TO_USER => "No report file defined.",
ERROR_MESSAGE_TO_DEVELOPER => "The keyword '" . TOKEN_REPORT_FILE . "' is not present in the typo3 content element with id $uid"]),
ERROR_FORM_NOT_FOUND);
}
// Update report file
HelperFile::file_put_contents($reportPathFileNameFull, $newContent);
}
/**
* Return the path of the report directory relative to CWD.
* Create path if it doesn't exist.
......@@ -46,10 +173,8 @@ class ReportAsFile
* @throws \UserFormException
* @throws \UserReportException
*/
public static function reportPath(): string
private static function reportPath(): string
{
// TODO: make private
$systemQfqProjectDir = Store::getInstance()->getVar(SYSTEM_QFQ_PROJECT_DIR_SECURE, STORE_SYSTEM);
$reportPath = HelperFile::correctRelativePathFileName(HelperFile::joinPathFilename($systemQfqProjectDir, 'report'));
HelperFile::createPathRecursive($reportPath);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment