/templates/default/.xet * * @link https://www.egroupware.org * @author Ralf Becker * @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, if NO array( 'currentapp' => 'api', 'noheader' => true, // miss-use session creation callback to send the template, in case we have no session 'autocreate_session_callback' => 'send_template', 'nocachecontrol' => true, ) ); $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']); $path = Api\Etemplate::rel2path(Api\Etemplate::relPath($app . '.' . basename($name, '.xet'), $template)); if(empty($path) || !file_exists($path) || !is_readable($path)) { http_response_code(404); exit; } $cache = $GLOBALS['egw_info']['server']['temp_dir'].'/egw_cache/eT2-Cache-'. $GLOBALS['egw_info']['server']['install_id'].'-'.str_replace('/', '-', $_SERVER['PATH_INFO']); if (file_exists($cache) && filemtime($cache) > max(filemtime($path), filemtime(__FILE__)) && ($str = file_get_contents($cache)) !== false) { $cache_read = microtime(true); } elseif(($str = file_get_contents($path)) !== false) { // replace single quote enclosing attribute values with double quotes $str = preg_replace_callback("#([a-z_-]+)='([^']*)'([ />])#i", static function($matches){ return $matches[1].'="'.str_replace('"', '"', $matches[2]).'"'.$matches[3]; }, $str); // fix --> '.$matches[5].''; }, $str); // nextmatch headers $str = preg_replace_callback('#<(nextmatch-)([^ ]+)(header|filter) ([^>]+?)/>#s', static function (array $matches) { $attrs = parseAttrs($matches[4]); if ($matches[2] === 'custom') { $attrs['widget_type'] = $attrs['type']; } if(!$matches[2] || in_array($matches[2], ['sort']) || ($matches[2] == "custom" && empty($attrs['widget_type']))) { return $matches[0]; } // No longer needed & type causes problems unset($attrs['type'], $attrs['tags']); if($matches[2] === 'taglist') { $matches[2] = "filter"; } return ''; }, $str); $str = preg_replace('#]+)(/|>#', '', $str); // fix <(button|buttononly|timestamper).../> --> $str = preg_replace_callback('#<(button|buttononly|timestamper|button-timestamp)\s(.*?)(/|>#s', function ($matches) use ($name) { $tag = 'et2-button'; $attrs = parseAttrs($matches[2]); switch ($matches[1]) { case 'buttononly': // replace buttononly tag with noSubmit="true" attribute $attrs['noSubmit'] = 'true'; break; case 'timestamper': case 'button-timestamp': $tag .= '-timestamp'; $attrs['background_image'] = 'true'; break; } // novalidation --> noValidation if (!empty($attrs['novalidation']) && in_array($attrs['novalidation'], ['true', '1'], true)) { unset($attrs['novalidation']); $attrs['noValidation'] = 'true'; } // replace not set background_image attribute with et2-image tag, if not in NM / lists if (!empty($attrs['image']) && (empty($attrs['background_image']) || $attrs['background_image'] === 'false') && !preg_match('/^(index|list)/', $name)) { $tag = 'et2-image'; $attrs['src'] = $attrs['image']; unset($attrs['image']); // Was expected to submit. Images don't have noValidation, so add directly if (!array_key_exists('onclick', $attrs) && empty($attrs['noSubmit'])) { $attrs['onclick'] = 'this.getInstanceManager().submit(this, undefined, ' . $attrs['noValidation'] . ')'; } } unset($attrs['background_image']); return "<$tag " . stringAttrs($attrs) . '>'; }, $str); $str = preg_replace_callback('#]+)/>#', static function($matches) { if ($matches[1] === '-time_today') $matches[1] = '-time-today'; return ""; }, $str); // ^^^^^^^^^^^^^^^^ above widgets get transformed independent of legacy="true" set in overlay ^^^^^^^^^^^^^^^^^^ // eTemplate marked as legacy --> replace only some widgets (eg. requiring jQueryUI) with web-components if (!preg_match('/]* legacy="true"/', $str)) { $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 "", but "") (substr($matches[ADD_ET2_PREFIX_LAST_GROUP], -1) === '/' ? substr($matches[ADD_ET2_PREFIX_LAST_GROUP], 0, -1) . '>'; }, $str); } // change all attribute-names of new et2-* widgets to camelCase, and other attribute modifications for all web-components $str = preg_replace_callback('/<(et2|records)-([a-z-]+)\s([^>]+)>/', static function(array $matches) { $attrs = parseAttrs($matches[3]); // fix deprecated attributes: needed, blur, ... static $deprecated = [ 'needed' => 'required', 'blur' => 'placeholder', ]; foreach($attrs as $name => $value) { if (isset($deprecated[$name])) { unset($attrs[$name]); $attrs[$name = $deprecated[$name]] = $value; } if (count($parts = preg_split('/[_-]/', $name)) > 1) { if ($name === 'parent_node') $parts[1] = 'Id'; // we can not use DOM property parentNode --> parentId $attrs[array_shift($parts).implode('', array_map('ucfirst', $parts))] = $value; unset($attrs[$name]); } } // remove no longer necessary et2_fullWidth class, it's the default now anyway if (isset($attrs['class']) && empty($attrs['class'] = trim(preg_replace('/(^| )et2_fullWidth( |$)/', ' ', $attrs['class'])))) { unset($attrs['class']); } // Change size=# attribute to width, size is small|medium|large with Shoelace if (isset($attrs['size'])) { $attrs['width'] = (int)$attrs['size'].'em'; unset($attrs['size']); } return str_replace($matches[3], stringAttrs($attrs).(substr($matches[3], -1) === '/' ? '/' : ''), $matches[0]); }, $str); $processing = microtime(true); if (isset($cache) && (file_exists($cache_dir = dirname($cache)) || mkdir($cache_dir, 0755, true) || is_dir($cache_dir))) { file_put_contents($cache, $str); } } // stop here for not existing file or path-traversal for both file and cache here if(empty($str) || strpos($path, '..') !== false) { 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 if(isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) { header("HTTP/1.1 304 Not Modified"); exit; } // we run our own gzip compression, to set a correct Content-Length of the encoded content if(function_exists('gzencode') && in_array('gzip', explode(',', $_SERVER['HTTP_ACCEPT_ENCODING']), true)) { $gzip_start = microtime(true); $str = gzencode($str); header('Content-Encoding: gzip'); $gziping = microtime(true) - $gzip_start; } 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) ); // Content-Length header is important, otherwise browsers dont cache! Header('Content-Length: ' . bytes($str)); echo $str; exit; // stop further processing eg. redirect to login } /** * Parse attributes in an array * * @param string $str * @return array */ function parseAttrs($str) { if (!preg_match_all('/(^|\s)([a-z\d_-]+)="([^"]*)"/i', $str, $attrs, PREG_PATTERN_ORDER)) { throw new Exception("Can NOT parse attributes from '$str'"); } return array_combine($attrs[2], $attrs[3]); } /** * Combine attribute array into a string * * @param array $attrs * @return string */ function stringAttrs(array $attrs) { return implode(' ', array_map(static function ($name, $value) { return $name . '="' . $value . '"'; }, array_keys($attrs), $attrs)); }