2021-08-15 14:59:23 +02:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* API: loading for web-components modified eTemplate from server
|
|
|
|
*
|
|
|
|
* Usage: /egroupware/api/etemplate.php/<app>/templates/default/<name>.xet
|
|
|
|
*
|
|
|
|
* @link https://www.egroupware.org
|
|
|
|
* @author Ralf Becker <rb@egroupware-org>
|
|
|
|
* @package api
|
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
|
|
*/
|
|
|
|
|
|
|
|
use EGroupware\Api;
|
|
|
|
|
|
|
|
// add et2- prefix to following widgets/tags
|
2022-01-19 09:57:06 +01:00
|
|
|
const ADD_ET2_PREFIX_REGEXP = '#<((/?)([vh]?box|textbox|textarea|button|colorpicker|description))(/?|\s[^>]*)>#m';
|
2021-08-15 14:59:23 +02:00
|
|
|
|
|
|
|
// switch evtl. set output-compression off, as we cant calculate a Content-Length header with transparent compression
|
|
|
|
ini_set('zlib.output_compression', 0);
|
|
|
|
|
|
|
|
$GLOBALS['egw_info'] = array(
|
|
|
|
'flags' => array(
|
2021-08-19 01:41:23 +02:00
|
|
|
'currentapp' => 'api',
|
|
|
|
'noheader' => true,
|
2021-08-16 19:54:58 +02:00
|
|
|
// miss-use session creation callback to send the template, in case we have no session
|
2021-08-15 14:59:23 +02:00
|
|
|
'autocreate_session_callback' => 'send_template',
|
2021-08-19 01:41:23 +02:00
|
|
|
'nocachecontrol' => true,
|
2021-08-15 14:59:23 +02:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
$start = microtime(true);
|
|
|
|
include '../header.inc.php';
|
|
|
|
|
|
|
|
send_template();
|
|
|
|
|
|
|
|
function send_template()
|
|
|
|
{
|
|
|
|
$header_include = microtime(true);
|
|
|
|
|
|
|
|
// release session, as we don't need it and it blocks parallel requests
|
|
|
|
$GLOBALS['egw']->session->commit_session();
|
|
|
|
|
|
|
|
header('Content-Type: application/xml; charset=UTF-8');
|
|
|
|
|
|
|
|
//$path = EGW_SERVER_ROOT.$_SERVER['PATH_INFO'];
|
|
|
|
// check for customized template in VFS
|
|
|
|
list(, $app, , $template, $name) = explode('/', $_SERVER['PATH_INFO']);
|
2021-08-19 01:41:23 +02:00
|
|
|
$path = Api\Etemplate::rel2path(Api\Etemplate::relPath($app . '.' . basename($name, '.xet'), $template));
|
|
|
|
if(empty($path) || !file_exists($path) || !is_readable($path))
|
2021-08-15 14:59:23 +02:00
|
|
|
{
|
|
|
|
http_response_code(404);
|
|
|
|
exit;
|
|
|
|
}
|
|
|
|
/* disable caching for now, as you need to delete the cache, once you change ADD_ET2_PREFIX_REGEXP
|
|
|
|
$cache = $GLOBALS['egw_info']['server']['temp_dir'].'/egw_cache/eT2-Cache-'.$GLOBALS['egw_info']['server']['install_id'].$_SERVER['PATH_INFO'];
|
|
|
|
if (file_exists($cache) && filemtime($cache) > filemtime($path) &&
|
|
|
|
($str = file_get_contents($cache)) !== false)
|
|
|
|
{
|
|
|
|
$cache_read = microtime(true);
|
|
|
|
}
|
2021-08-19 01:41:23 +02:00
|
|
|
else*/
|
2022-01-20 21:09:48 +01:00
|
|
|
if(($str = file_get_contents($path)) !== false &&
|
|
|
|
// eTemplate marked as legacy, return unchanged template without web-components / et2-prefix
|
|
|
|
!preg_match('/<overlay[^>]* legacy="true"/', $str))
|
2021-08-15 14:59:23 +02:00
|
|
|
{
|
|
|
|
// fix <menulist...><menupopup type="select-*"/></menulist> --> <select type="select-*" .../>
|
|
|
|
$str = preg_replace('#<menulist([^>]*)>[\r\n\s]*<menupopup([^>]+>)[\r\n\s]*</menulist>#', '<select$1$2', $str);
|
|
|
|
|
2022-02-18 08:44:25 +01:00
|
|
|
// fix legacy options, so new client-side has not to deal with them
|
|
|
|
$str = preg_replace_callback('#<([^- ]+)(-[^ ]+)?[^>]* (options="([^"]+)")[ />]#', static function($matches)
|
|
|
|
{
|
|
|
|
// take care of (static) type attribute, if used
|
|
|
|
if (preg_match('/ type="([a-z-]+)"/', $matches[0], $type))
|
|
|
|
{
|
|
|
|
str_replace('<'.$matches[1].$matches[2], '<'.$type[1], $matches[0]);
|
|
|
|
str_replace($type[0], '', $matches[0]);
|
|
|
|
list($matches[1], $matches[2]) = explode('-', $type[1], 2);
|
|
|
|
}
|
|
|
|
static $legacy_options = array(
|
|
|
|
'select' => 'empty_label',
|
|
|
|
'box' => ',cellpadding,cellspacing,keep',
|
|
|
|
'hbox' => 'cellpadding,cellspacing,keep',
|
|
|
|
'vbox' => 'cellpadding,cellspacing,keep',
|
|
|
|
'groupbox' => 'cellpadding,cellspacing,keep',
|
|
|
|
'checkbox' => 'selected_value,unselected_value,ro_true,ro_false',
|
|
|
|
'radio' => 'set_value,ro_true,ro_false',
|
|
|
|
'customfields' => 'sub-type,use-private,field-names',
|
|
|
|
'date' => 'data_format,mode',
|
|
|
|
'description' => 'bold-italic,link,activate_links,label_for,link_target,link_popup_size,link_title',
|
|
|
|
);
|
|
|
|
// prefer more specific type-subtype over just type
|
|
|
|
$names = $legacy_options[$matches[1].$matches[2]] ?? $legacy_options[$matches[1]] ?? null;
|
|
|
|
if (isset($names))
|
|
|
|
{
|
|
|
|
$names = explode(',', $names);
|
|
|
|
$attrs = array_diff(array_combine($names, explode(',', $matches[4], count($names))), ['', null]);
|
|
|
|
// fix select options can be either multiple or empty_label
|
|
|
|
if ($matches[1] === 'select' && !empty($attrs['empty_label']) && (int)$attrs['empty_label'] > 0)
|
|
|
|
{
|
|
|
|
$attrs['multiple'] = (int)$attrs['empty_label'];
|
|
|
|
unset($matches['empty_label']);
|
|
|
|
}
|
|
|
|
$options = '';
|
|
|
|
foreach($attrs as $attr => $value)
|
|
|
|
{
|
|
|
|
$options .= $attr.'="'.$value.'" ';
|
|
|
|
}
|
|
|
|
return str_replace($matches[3], $options, $matches[0]);
|
|
|
|
}
|
|
|
|
return $matches[0];
|
|
|
|
}, $str);
|
|
|
|
|
2021-08-21 09:55:21 +02:00
|
|
|
// fix <textbox multiline="true" .../> --> <textarea .../> (et2-prefix and self-closing is handled below)
|
|
|
|
$str = preg_replace('#<textbox(.*?)\smultiline="true"(.*?)/>#u', '<textarea$1$2/>', $str);
|
|
|
|
|
2021-09-16 09:03:15 +02:00
|
|
|
// fix <buttononly.../> --> <button type="buttononly".../>
|
|
|
|
$str = preg_replace('#<buttononly\s(.*?)/>#u', '<button type="buttononly" $1/>', $str);
|
|
|
|
|
2021-08-19 01:41:23 +02:00
|
|
|
$str = preg_replace_callback(ADD_ET2_PREFIX_REGEXP, static function (array $matches)
|
|
|
|
{
|
|
|
|
return '<' . $matches[2] . 'et2-' . $matches[3] .
|
|
|
|
// web-components must not be self-closing (no "<et2-button .../>", but "<et2-button ...></et2-button>")
|
|
|
|
(substr($matches[4], -1) === '/' ? substr($matches[4], 0, -1) . '></et2-' . $matches[3] : $matches[4]) . '>';
|
2022-01-19 09:57:06 +01:00
|
|
|
}, $str);
|
|
|
|
|
|
|
|
// handling of partially implemented select and date widget (only readonly or simple select without tags or search attribute or options)
|
|
|
|
$str = preg_replace_callback('#<(select|date)(-[^ ]+)? ([^>]+)/>#', static function (array $matches)
|
|
|
|
{
|
|
|
|
preg_match_all('/(^| )([a-z0-9_-]+)="([^"]+)"/', $matches[3], $attrs, PREG_PATTERN_ORDER);
|
|
|
|
$attrs = array_combine($attrs[2], $attrs[3]);
|
2022-01-19 21:17:07 +01:00
|
|
|
|
2022-01-19 09:57:06 +01:00
|
|
|
// add et2-prefix for <select-* or <date-* readonly="true"
|
2022-02-15 08:16:55 +01:00
|
|
|
if (isset($attrs['readonly']) && !in_array($attrs['readonly'], ['false', '0']) ||
|
2022-02-16 09:38:28 +01:00
|
|
|
// also add it for <date* and <select* without search or tags attribute
|
|
|
|
$matches[1] === 'date' || $matches[1] === 'select' && !isset($attrs['search']) && !isset($attrs['tags']))
|
2022-01-19 09:57:06 +01:00
|
|
|
{
|
2022-02-16 09:38:28 +01:00
|
|
|
// type attribute need to go in widget type <select type="select-account" --> <et2-select-account
|
|
|
|
if (empty($matches[2]) && isset($attrs['type']))
|
|
|
|
{
|
|
|
|
$matches[1] = $attrs['type'];
|
|
|
|
$matches[3] = str_replace('type="'.$attrs['type'].'"', '', $matches[3]);
|
|
|
|
}
|
2022-01-19 09:57:06 +01:00
|
|
|
return '<et2-'.$matches[1].$matches[2].' '.$matches[3].'></et2-'.$matches[1].$matches[2].'>';
|
|
|
|
}
|
|
|
|
return $matches[0];
|
|
|
|
}, $str);
|
2021-08-15 14:59:23 +02:00
|
|
|
|
|
|
|
$processing = microtime(true);
|
|
|
|
|
2021-08-19 01:41:23 +02:00
|
|
|
if(isset($cache) && (file_exists($cache_dir = dirname($cache)) || mkdir($cache_dir, 0755, true)))
|
2021-08-15 14:59:23 +02:00
|
|
|
{
|
|
|
|
file_put_contents($cache, $str);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// stop here for not existing file path-traversal for both file and cache here
|
2021-08-19 01:41:23 +02:00
|
|
|
if(empty($str) || strpos($path, '..') !== false)
|
2021-08-15 14:59:23 +02:00
|
|
|
{
|
|
|
|
http_response_code(404);
|
|
|
|
exit;
|
|
|
|
}
|
|
|
|
|
|
|
|
// headers to allow caching, egw_framework specifies etag on url to force reload, even with Expires header
|
|
|
|
Api\Session::cache_control(86400); // cache for one day
|
|
|
|
$etag = '"' . md5($str) . '"';
|
|
|
|
Header('ETag: ' . $etag);
|
|
|
|
|
|
|
|
// if servers send a If-None-Match header, response with 304 Not Modified, if etag matches
|
2021-08-19 01:41:23 +02:00
|
|
|
if(isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag)
|
2021-08-15 14:59:23 +02:00
|
|
|
{
|
|
|
|
header("HTTP/1.1 304 Not Modified");
|
|
|
|
exit;
|
|
|
|
}
|
|
|
|
|
|
|
|
// we run our own gzip compression, to set a correct Content-Length of the encoded content
|
2021-08-19 01:41:23 +02:00
|
|
|
if(function_exists('gzencode') && in_array('gzip', explode(',', $_SERVER['HTTP_ACCEPT_ENCODING']), true))
|
2021-08-15 14:59:23 +02:00
|
|
|
{
|
|
|
|
$gzip_start = microtime(true);
|
|
|
|
$str = gzencode($str);
|
|
|
|
header('Content-Encoding: gzip');
|
2021-08-19 01:41:23 +02:00
|
|
|
$gziping = microtime(true) - $gzip_start;
|
2021-08-15 14:59:23 +02:00
|
|
|
}
|
2021-08-19 01:41:23 +02:00
|
|
|
header('X-Timing: header-include=' . number_format($header_include - $GLOBALS['start'], 3) .
|
|
|
|
(empty($processing) ? ', cache-read=' . number_format($cache_read - $header_include, 3) :
|
|
|
|
', processing=' . number_format($processing - $header_include, 3)) .
|
|
|
|
(!empty($gziping) ? ', gziping=' . number_format($gziping, 3) : '') .
|
|
|
|
', total=' . number_format(microtime(true) - $GLOBALS['start'], 3)
|
|
|
|
);
|
2021-08-15 14:59:23 +02:00
|
|
|
|
|
|
|
// Content-Length header is important, otherwise browsers dont cache!
|
|
|
|
Header('Content-Length: ' . bytes($str));
|
|
|
|
echo $str;
|
|
|
|
|
2021-08-19 01:41:23 +02:00
|
|
|
exit; // stop further processing eg. redirect to login
|
2022-02-15 08:16:55 +01:00
|
|
|
}
|