Commit 736cbfc9 authored by Christian Kuhn's avatar Christian Kuhn
Browse files

[!!!][TASK] House of forms

This patch introduces a structural code refactoring to the
FormEngine class and its related friend classes.

FormEngine used to call itself over and over again with itself
and sub classes writing to public properties of FormEngine keeping
a global state that is then magically merged to sometimes working
output.

The patch introduces a tree approach with lots of small containers
doing an encapsulated part of the rendering process and calling
sub containers for inner details.
As main construct a "globalOptions" array is modified in containers
and given down to sub containers (tree knots) or elements (leaves),
while sub structures always return a defined array that is
merged by the parent and accumulates the full result.
Goal is to have a better encapsulated code structure with better
visible impact on changes done to this system.

The patch creates this main structure. There is still a lot of
mess around and additional patches can further improve the overall
situation with smaller changes.

Change-Id: I56b898dc0eaae8de4d31016997cfefe8d14ec53e
Releases: master
Resolves: #63846
Resolves: #63854
Resolves: #63856
Resolves: #63858
Resolves: #63859
Resolves: #63860
Resolves: #63861
Resolves: #63862
Resolves: #63863
Resolves: #63865
Resolves: #63876
Resolves: #63881
Resolves: #63882
Resolves: #63883
Resolves: #63893
Reviewed-on: http://review.typo3.org/38433

Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: default avatarMarkus Klein <klein.t3@reelworx.at>
Tested-by: default avatarMarkus Klein <klein.t3@reelworx.at>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 3115cbc8
......@@ -24,6 +24,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Frontend\Page\PageRepository;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
/**
* Script Class: Drawing the editing form for editing records in TYPO3.
......@@ -173,14 +174,6 @@ class EditDocumentController {
*/
public $recTitle;
/**
* Disable help... ?
*
* @var bool
* @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
*/
public $disHelp;
/**
* If set, then no SAVE/VIEW button is printed
*
......@@ -557,7 +550,7 @@ class EditDocumentController {
// If there was saved any new items, load them:
if (count($tce->substNEWwithIDs_table)) {
// save the expanded/collapsed states for new inline records, if any
\TYPO3\CMS\Backend\Form\Element\InlineElement::updateInlineView($this->uc, $tce);
FormEngineUtility::updateInlineView($this->uc, $tce);
$newEditConf = array();
foreach ($this->editconf as $tableName => $tableCmds) {
$keys = array_keys($tce->substNEWwithIDs_table, $tableName);
......@@ -845,7 +838,6 @@ class EditDocumentController {
if (is_array($this->editconf)) {
// Initialize TCEforms (rendering the forms)
$this->tceforms = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormEngine::class);
$this->tceforms->initDefaultBEMode();
$this->tceforms->doSaveFieldName = 'doSave';
$this->tceforms->localizationMode = GeneralUtility::inList('text,media', $this->localizationMode) ? $this->localizationMode : '';
// text,media is keywords defined in TYPO3 Core API..., see "l10n_cat"
......@@ -1044,8 +1036,6 @@ class EditDocumentController {
if (is_array($this->overrideVals) && is_array($this->overrideVals[$table])) {
$this->tceforms->hiddenFieldListArr = array_keys($this->overrideVals[$table]);
}
// Register default language labels, if any:
$this->tceforms->registerDefaultLanguageData($table, $rec);
// Create form for the record (either specific list of fields or the whole record):
$panel = '';
if ($this->columnsOnly) {
......@@ -1284,17 +1274,6 @@ class EditDocumentController {
return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:labels.openInNewWindow', TRUE) . '">' . IconUtility::getSpriteIcon('actions-window-open') . '</a>';
}
/**
* Reads comment messages from TCEforms and prints them in a HTML comment in the bottom of the page.
*
* @return string
* @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8
*/
public function tceformMessages() {
GeneralUtility::logDeprecatedFunction();
return '';
}
/***************************
*
* Localization stuff
......
......@@ -811,8 +811,6 @@ class PageLayoutController {
// If the record is an array (which it will always be... :-)
// Create instance of TCEforms, setting defaults:
$tceforms = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormEngine::class);
$tceforms->initDefaultBEMode();
$tceforms->fieldOrder = $this->modTSconfig['properties']['tt_content.']['fieldOrder'];
$tceforms->palettesCollapsed = !$this->MOD_SETTINGS['showPalettes'];
// Render form, wrap it:
$panel = '';
......
......@@ -130,8 +130,6 @@ class RteController extends AbstractWizardController {
}
// Initialize TCeforms - for rendering the field:
$tceforms = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormEngine::class);
// Init...
$tceforms->initDefaultBEMode();
// SPECIAL: Disables all wizards - we are NOT going to need them.
$tceforms->disableWizards = 1;
// Initialize style for RTE object:
......
<?php
namespace TYPO3\CMS\Backend\Form;
/*
* 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\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Base class for container and single elements - their abstracts extend from here.
*/
abstract class AbstractNode {
/**
* A list of global options given from parent to child elements
*
* @var array
*/
protected $globalOptions = array();
/**
* Handler for single nodes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
abstract public function render();
/**
* Set global options from parent instance
*
* @param array $globalOptions Global options like 'readonly' for all elements
* @return $this
*/
public function setGlobalOptions(array $globalOptions) {
$this->globalOptions = $globalOptions;
return $this;
}
/**
* Initialize the array that is returned to parent after calling. This structure
* is identical for *all* nodes. Parent will merge the return of a child with its
* own stuff and in itself return an array of the same structure.
*
* @return array
*/
protected function initializeResultArray() {
return array(
'requiredElements' => array(), // name => value
'requiredFields' => array(), // value => name
'requiredAdditional' => array(), // name => array
'requiredNested' => array(),
'additionalJavaScriptPost' => array(),
'additionalJavaScriptSubmit' => array(),
'additionalHiddenFields' => array(),
'additionalHeadTags' => array(),
'extJSCODE' => '',
'inlineData' => array(),
'html' => '',
);
}
/**
* Merge existing data with a child return array
*
* @param array $existing Currently merged array
* @param array $childReturn Array returned by child
* @return array Result array
*/
protected function mergeChildReturnIntoExistingResult(array $existing, array $childReturn) {
if (!empty($childReturn['html'])) {
$existing['html'] .= LF . $childReturn['html'];
}
if (!empty($childReturn['extJSCODE'])) {
$existing['extJSCODE'] .= LF . $childReturn['extJSCODE'];
}
foreach ($childReturn['requiredElements'] as $name => $value) {
$existing['requiredElements'][$name] = $value;
}
foreach ($childReturn['requiredFields'] as $value => $name) { // Params swapped ?!
$existing['requiredFields'][$value] = $name;
}
foreach ($childReturn['requiredAdditional'] as $name => $subArray) {
$existing['requiredAdditional'][$name] = $subArray;
}
foreach ($childReturn['requiredNested'] as $value => $name) {
$existing['requiredNested'][$value] = $name;
}
foreach ($childReturn['additionalJavaScriptPost'] as $value) {
$existing['additionalJavaScriptPost'][] = $value;
}
foreach ($childReturn['additionalJavaScriptSubmit'] as $value) {
$existing['additionalJavaScriptSubmit'][] = $value;
}
if (!empty($childReturn['inlineData'])) {
$existingInlineData = $existing['inlineData'];
$childInlineData = $childReturn['inlineData'];
ArrayUtility::mergeRecursiveWithOverrule($existingInlineData, $childInlineData);
$existing['inlineData'] = $existingInlineData;
}
return $existing;
}
/**
* Determine and get the value for the placeholder for an input field.
* Typically used in an inline relation where values from fields down the record chain
* are used as "default" values for fields.
*
* @param string $table
* @param array $config
* @param array $row
* @return mixed
*/
protected function getPlaceholderValue($table, array $config, array $row) {
$value = trim($config['placeholder']);
if (!$value) {
return '';
}
// Check if we have a reference to another field value from the current record
if (substr($value, 0, 6) === '__row|') {
/** @var FormDataTraverser $traverser */
$traverseFields = GeneralUtility::trimExplode('|', substr($value, 6));
$traverser = GeneralUtility::makeInstance(FormDataTraverser::class);
$value = $traverser->getTraversedFieldValue($traverseFields, $table, $row, $this->globalOptions['inlineFirstPid'], $this->globalOptions['prependFormFieldNames']);
}
return $value;
}
}
<?php
namespace TYPO3\CMS\Backend\Form\Container;
/*
* 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\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Form\ElementConditionMatcher;
use TYPO3\CMS\Backend\Utility\IconUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\Template\DocumentTemplate;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Abstract container has various methods used by the container classes
*/
abstract class AbstractContainer extends AbstractNode {
/**
* Array where records in the default language are stored. (processed by transferdata)
*
* @var array
*/
protected $defaultLanguageData = array();
/**
* Array where records in the default language are stored (raw without any processing. used for making diff).
* This is the unserialized content of configured TCA ['ctrl']['transOrigDiffSourceField'] field, typically l18n_diffsource
*
* @var array
*/
protected $defaultLanguageDataDiff = array();
/**
* Contains row data of "additional" language overlays
* array(
* $table:$uid => array(
* $additionalPreviewLanguageUid => $rowData
* )
* )
*
* @var array
*/
protected $additionalPreviewLanguageData = array();
/**
* Calculate and return the current type value of a record
*
* @param string $table The table name. MUST be in $GLOBALS['TCA']
* @param array $row The row from the table, should contain at least the "type" field, if applicable.
* @return string Return the "type" value for this record, ready to pick a "types" configuration from the $GLOBALS['TCA'] array.
* @throws \RuntimeException
*/
protected function getRecordTypeValue($table, array $row) {
$typeNum = 0;
$field = $GLOBALS['TCA'][$table]['ctrl']['type'];
if ($field) {
if (strpos($field, ':') !== FALSE) {
list($pointerField, $foreignTypeField) = explode(':', $field);
$fieldConfig = $GLOBALS['TCA'][$table]['columns'][$pointerField]['config'];
$relationType = $fieldConfig['type'];
if ($relationType === 'select') {
$foreignUid = $row[$pointerField];
$foreignTable = $fieldConfig['foreign_table'];
} elseif ($relationType === 'group') {
$values = FormEngineUtility::extractValuesOnlyFromValueLabelList($row[$pointerField]);
list(, $foreignUid) = GeneralUtility::revExplode('_', $values[0], 2);
$allowedTables = explode(',', $fieldConfig['allowed']);
// Always take the first configured table.
$foreignTable = $allowedTables[0];
} else {
throw new \RuntimeException('TCA Foreign field pointer fields are only allowed to be used with group or select field types.', 1325861239);
}
if ($foreignUid) {
$foreignRow = BackendUtility::getRecord($foreignTable, $foreignUid, $foreignTypeField);
$this->registerDefaultLanguageData($foreignTable, $foreignRow);
if ($foreignRow[$foreignTypeField]) {
$foreignTypeFieldConfig = $GLOBALS['TCA'][$table]['columns'][$field];
$typeNum = $this->overrideTypeWithValueFromDefaultLanguageRecord($foreignTable, $foreignRow, $foreignTypeField, $foreignTypeFieldConfig);
}
}
} else {
$typeFieldConfig = $GLOBALS['TCA'][$table]['columns'][$field];
$typeNum = $this->overrideTypeWithValueFromDefaultLanguageRecord($table, $row, $field, $typeFieldConfig);
}
}
if (empty($typeNum)) {
// If that value is an empty string, set it to "0" (zero)
$typeNum = 0;
}
// If current typeNum doesn't exist, set it to 0 (or to 1 for historical reasons, if 0 doesn't exist)
if (!$GLOBALS['TCA'][$table]['types'][$typeNum]) {
$typeNum = $GLOBALS['TCA'][$table]['types']['0'] ? 0 : 1;
}
// Force to string. Necessary for eg '-1' to be recognized as a type value.
return (string)$typeNum;
}
/**
* Producing an array of field names NOT to display in the form,
* based on settings from subtype_value_field, bitmask_excludelist_bits etc.
* Notice, this list is in NO way related to the "excludeField" flag
*
* @param string $table Table name, MUST be in $GLOBALS['TCA']
* @param array $row A record from table.
* @param string $typeNum A "type" pointer value, probably the one calculated based on the record array.
* @return array Array with field names as values. The field names are those which should NOT be displayed "anyways
*/
protected function getExcludeElements($table, $row, $typeNum) {
$excludeElements = array();
// If a subtype field is defined for the type
if ($GLOBALS['TCA'][$table]['types'][$typeNum]['subtype_value_field']) {
$subTypeField = $GLOBALS['TCA'][$table]['types'][$typeNum]['subtype_value_field'];
if (trim($GLOBALS['TCA'][$table]['types'][$typeNum]['subtypes_excludelist'][$row[$subTypeField]])) {
$excludeElements = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['types'][$typeNum]['subtypes_excludelist'][$row[$subTypeField]], TRUE);
}
}
// If a bitmask-value field has been configured, then find possible fields to exclude based on that:
if ($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_value_field']) {
$subTypeField = $GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_value_field'];
$sTValue = MathUtility::forceIntegerInRange($row[$subTypeField], 0);
if (is_array($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_excludelist_bits'])) {
foreach ($GLOBALS['TCA'][$table]['types'][$typeNum]['bitmask_excludelist_bits'] as $bitKey => $eList) {
$bit = substr($bitKey, 1);
if (MathUtility::canBeInterpretedAsInteger($bit)) {
$bit = MathUtility::forceIntegerInRange($bit, 0, 30);
if ($bitKey[0] === '-' && !($sTValue & pow(2, $bit)) || $bitKey[0] === '+' && $sTValue & pow(2, $bit)) {
$excludeElements = array_merge($excludeElements, GeneralUtility::trimExplode(',', $eList, TRUE));
}
}
}
}
}
return $excludeElements;
}
/**
* The requested field value will be overridden with the data from the default
* language if the field is configured accordingly.
*
* @param string $table Table name of the record being edited
* @param array $row Record array of the record being edited in current language
* @param string $field Field name represented by $item
* @param array $fieldConf Content of $PA['fieldConf']
* @return string Unprocessed field value merged with default language data if needed
*/
protected function overrideTypeWithValueFromDefaultLanguageRecord($table, array $row, $field, $fieldConf) {
$value = $row[$field];
if (is_array($this->defaultLanguageData[$table . ':' . $row['uid']])) {
// @todo: Is this a bug? Currently the field from default lang is picked in mergeIfNotBlank mode if the
// @todo: default value is not empty, but imho it should only be picked if the language overlay record *is* empty?!
if (
$fieldConf['l10n_mode'] === 'exclude'
|| $fieldConf['l10n_mode'] === 'mergeIfNotBlank' && trim($this->defaultLanguageData[$table . ':' . $row['uid']][$field]) !== ''
) {
$value = $this->defaultLanguageData[$table . ':' . $row['uid']][$field];
}
}
return $value;
}
/**
* Return a list without excluded elements.
*
* @param array $fieldsArray Typically coming from types show item
* @param array $excludeElements Field names to be excluded
* @return array $fieldsArray without excluded elements
*/
protected function removeExcludeElementsFromFieldArray(array $fieldsArray, array $excludeElements) {
$newFieldArray = array();
foreach ($fieldsArray as $fieldString) {
$fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
$fieldName = $fieldArray['fieldName'];
// It doesn't make sense to exclude palettes and tabs
if (!in_array($fieldName, $excludeElements, TRUE) || $fieldName === '--palette--' || $fieldName === '--div--') {
$newFieldArray[] = $fieldString;
}
}
return $newFieldArray;
}
/**
* A single field of TCA 'types' 'showitem' can have four semicolon separated configuration options:
* fieldName: Name of the field to be found in TCA 'columns' section
* fieldLabel: An alternative field label
* paletteName: Name of a palette to be found in TCA 'palettes' section that is rendered after this field
* extra: Special configuration options of this field
*
* @param string $field Semicolon separated field configuration
* @throws \RuntimeException
* @return array
*/
protected function explodeSingleFieldShowItemConfiguration($field) {
$fieldArray = GeneralUtility::trimExplode(';', $field);
if (empty($fieldArray[0])) {
throw new \RuntimeException('Field must not be empty', 1426448465);
}
return array(
'fieldName' => $fieldArray[0],
'fieldLabel' => $fieldArray[1] ?: NULL,
'paletteName' => $fieldArray[2] ?: NULL,
'fieldExtra' => $fieldArray[3] ?: NULL,
);
}
/**
* Will register data from original language records if the current record is a translation of another.
* The original data is shown with the edited record in the form.
* The information also includes possibly diff-views of what changed in the original record.
* Function called from outside (see alt_doc.php + quick edit) before rendering a form for a record
*
* @param string $table Table name of the record being edited
* @param array $rec Record array of the record being edited
* @return void
*/
protected function registerDefaultLanguageData($table, $rec) {
// @todo: early return here if the arrays are already filled?
// Add default language:
if (
$GLOBALS['TCA'][$table]['ctrl']['languageField'] && $rec[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0
&& $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']
&& (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0
) {
$lookUpTable = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']
? $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']
: $table;
// Get data formatted:
$this->defaultLanguageData[$table . ':' . $rec['uid']] = BackendUtility::getRecordWSOL(
$lookUpTable,
(int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]
);
// Get data for diff:
if ($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']) {
$this->defaultLanguageDataDiff[$table . ':' . $rec['uid']] = unserialize($rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);
}
// If there are additional preview languages, load information for them also:
foreach ($this->globalOptions['additionalPreviewLanguages'] as $prL) {
/** @var $translationConfigurationProvider TranslationConfigurationProvider */
$translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
$translationInfo = $translationConfigurationProvider->translationInfo($lookUpTable, (int)$rec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], $prL['uid']);
if (is_array($translationInfo['translations']) && is_array($translationInfo['translations'][$prL['uid']])) {
$this->additionalPreviewLanguageData[$table . ':' . $rec['uid']][$prL['uid']] = BackendUtility::getRecordWSOL($table, (int)$translationInfo['translations'][$prL['uid']]['uid']);
}
}
}
}
/**
* Evaluate condition of flex forms
*
* @param string $displayCondition The condition to evaluate
* @param array $flexFormData Given data the condition is based on
* @return bool TRUE if condition matched
*/
protected function evaluateFlexFormDisplayCondition($displayCondition, $flexFormData) {
$elementConditionMatcher = GeneralUtility::makeInstance(ElementConditionMatcher::class);
$splitCondition = GeneralUtility::trimExplode(':', $displayCondition);
$skipCondition = FALSE;
$fakeRow = array();
switch ($splitCondition[0]) {
case 'FIELD':
// @todo: Not 100% sure if that is correct this way
list($_sheetName, $fieldName) = GeneralUtility::trimExplode('.', $splitCondition[1]);
$fieldValue = $flexFormData[$fieldName];
$splitCondition[1] = $fieldName;
$dataStructure['ROOT']['TCEforms']['displayCond'] = join(':', $splitCondition);
$fakeRow = array($fieldName => $fieldValue);
break;
case 'HIDE_FOR_NON_ADMINS':
case 'VERSION':
case 'HIDE_L10N_SIBLINGS':
case 'EXT':
break;
case 'REC':
$fakeRow = array('uid' => $this->globalOptions['databaseRow']['uid']);
break;
default:
$skipCondition = TRUE;
}
if ($skipCondition) {
return TRUE;
} else {
return $elementConditionMatcher->match($displayCondition, $fakeRow, 'vDEF');
}
}
/**
* Rendering preview output of a field value which is not shown as a form field but just outputted.
*
* @param string $value The value to output
* @param array $config Configuration for field.
* @param string $field Name of field.
* @return string HTML formatted output
*/
protected function previewFieldValue($value, $config, $field = '') {
if ($config['config']['type'] === 'group' && ($config['config']['internal_type'] === 'file' || $config['config']['internal_type'] === 'file_reference')) {
// Ignore upload folder if internal_type is file_reference
if ($config['config']['internal_type'] === 'file_reference') {
$config['config']['uploadfolder'] = '';
}
$table = 'tt_content';
// Making the array of file items:
$itemArray = GeneralUtility::trimExplode(',', $value, TRUE);
// Showing thumbnails:
$thumbnail = '';
$imgs = array();
foreach ($itemArray as $imgRead) {
$imgParts = explode('|', $imgRead);
$imgPath = rawurldecode($imgParts[0]);
$rowCopy = array();
$rowCopy[$field] = $imgPath;
// Icon + click menu:
$absFilePath = GeneralUtility::getFileAbsFileName($config['config']['uploadfolder'] ? $config['config']['uploadfolder'] . '/' . $imgPath : $imgPath);
$fileInformation = pathinfo($imgPath);
$fileIcon = IconUtility::getSpriteIconForFile(
$imgPath,
array(
'title' => htmlspecialchars($fileInformation['basename'] . ($absFilePath && @is_file($absFilePath) ? ' (' . GeneralUtility::formatSize(filesize($absFilePath)) . 'bytes)' : ' - FILE NOT FOUND!'))
)
);
$imgs[] =
'<span class="text-nowrap">' .
BackendUtility::thumbCode(
$rowCopy,
$table,
$field,
'',
'thumbs.php',
$config['config']['uploadfolder'], 0, ' align="middle"'
) .
($absFilePath ? $this->get