*/ private array $forbiddenNames = []; /** * @var list */ private array $forbiddenBasenames = []; /** * @var list */ private array $forbiddenCharacters = []; /** * @var list */ private array $forbiddenExtensions = []; public function __construct( IFactory $l10nFactory, private IDBConnection $database, private IConfig $config, private LoggerInterface $logger, ) { $this->l10n = $l10nFactory->get('core'); } /** * Get a list of reserved filenames that must not be used * This list should be checked case-insensitive, all names are returned lowercase. * @return list * @since 30.0.0 */ public function getForbiddenExtensions(): array { if (empty($this->forbiddenExtensions)) { $forbiddenExtensions = $this->getConfigValue('forbidden_filename_extensions', ['.filepart']); // Always forbid .part files as they are used internally $forbiddenExtensions[] = '.part'; $this->forbiddenExtensions = array_values($forbiddenExtensions); } return $this->forbiddenExtensions; } /** * Get a list of forbidden filename extensions that must not be used * This list should be checked case-insensitive, all names are returned lowercase. * @return list * @since 30.0.0 */ public function getForbiddenFilenames(): array { if (empty($this->forbiddenNames)) { $forbiddenNames = $this->getConfigValue('forbidden_filenames', ['.htaccess']); // Handle legacy config option // TODO: Drop with Nextcloud 34 $legacyForbiddenNames = $this->getConfigValue('blacklisted_files', []); if (!empty($legacyForbiddenNames)) { $this->logger->warning('System config option "blacklisted_files" is deprecated and will be removed in Nextcloud 34, use "forbidden_filenames" instead.'); } $forbiddenNames = array_merge($legacyForbiddenNames, $forbiddenNames); // Ensure we are having a proper string list $this->forbiddenNames = array_values($forbiddenNames); } return $this->forbiddenNames; } /** * Get a list of forbidden file basenames that must not be used * This list should be checked case-insensitive, all names are returned lowercase. * @return list * @since 30.0.0 */ public function getForbiddenBasenames(): array { if (empty($this->forbiddenBasenames)) { $forbiddenBasenames = $this->getConfigValue('forbidden_filename_basenames', []); // Ensure we are having a proper string list $this->forbiddenBasenames = array_values($forbiddenBasenames); } return $this->forbiddenBasenames; } /** * Get a list of characters forbidden in filenames * * Note: Characters in the range [0-31] are always forbidden, * even if not inside this list (see OCP\Files\Storage\IStorage::verifyPath). * * @return list * @since 30.0.0 */ public function getForbiddenCharacters(): array { if (empty($this->forbiddenCharacters)) { // Get always forbidden characters $forbiddenCharacters = str_split(\OCP\Constants::FILENAME_INVALID_CHARS); if ($forbiddenCharacters === false) { $forbiddenCharacters = []; } // Get admin defined invalid characters $additionalChars = $this->config->getSystemValue('forbidden_filename_characters', []); if (!is_array($additionalChars)) { $this->logger->error('Invalid system config value for "forbidden_filename_characters" is ignored.'); $additionalChars = []; } $forbiddenCharacters = array_merge($forbiddenCharacters, $additionalChars); // Handle legacy config option // TODO: Drop with Nextcloud 34 $legacyForbiddenCharacters = $this->config->getSystemValue('forbidden_chars', []); if (!is_array($legacyForbiddenCharacters)) { $this->logger->error('Invalid system config value for "forbidden_chars" is ignored.'); $legacyForbiddenCharacters = []; } if (!empty($legacyForbiddenCharacters)) { $this->logger->warning('System config option "forbidden_chars" is deprecated and will be removed in Nextcloud 34, use "forbidden_filename_characters" instead.'); } $forbiddenCharacters = array_merge($legacyForbiddenCharacters, $forbiddenCharacters); $this->forbiddenCharacters = array_values($forbiddenCharacters); } return $this->forbiddenCharacters; } /** * @inheritdoc */ public function isFilenameValid(string $filename): bool { try { $this->validateFilename($filename); } catch (\OCP\Files\InvalidPathException) { return false; } return true; } /** * @inheritdoc */ public function validateFilename(string $filename): void { $trimmed = trim($filename); if ($trimmed === '') { throw new EmptyFileNameException(); } // the special directories . and .. would cause never ending recursion // we check the trimmed name here to ensure unexpected trimming will not cause severe issues if ($trimmed === '.' || $trimmed === '..') { throw new InvalidDirectoryException($this->l10n->t('Dot files are not allowed')); } // 255 characters is the limit on common file systems (ext/xfs) // oc_filecache has a 250 char length limit for the filename if (isset($filename[250])) { throw new FileNameTooLongException(); } if (!$this->database->supports4ByteText()) { // verify database - e.g. mysql only 3-byte chars if (preg_match('%(?: \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 )%xs', $filename)) { throw new InvalidCharacterInPathException(); } } $this->checkForbiddenName($filename); $this->checkForbiddenExtension($filename); $this->checkForbiddenCharacters($filename); } /** * Check if the filename is forbidden * @param string $path Path to check the filename * @return bool True if invalid name, False otherwise */ public function isForbidden(string $path): bool { // We support paths here as this function is also used in some storage internals $filename = basename($path); $filename = mb_strtolower($filename); if ($filename === '') { return false; } // Check for forbidden filenames $forbiddenNames = $this->getForbiddenFilenames(); if (in_array($filename, $forbiddenNames)) { return true; } // Filename is not forbidden return false; } protected function checkForbiddenName($filename): void { if ($this->isForbidden($filename)) { throw new ReservedWordException($this->l10n->t('"%1$s" is a forbidden file or folder name.', [$filename])); } // Check for forbidden basenames - basenames are the part of the file until the first dot // (except if the dot is the first character as this is then part of the basename "hidden files") $basename = substr($filename, 0, strpos($filename, '.', 1) ?: null); $forbiddenNames = $this->getForbiddenBasenames(); if (in_array($basename, $forbiddenNames)) { throw new ReservedWordException($this->l10n->t('"%1$s" is a forbidden prefix for file or folder names.', [$filename])); } } /** * Check if a filename contains any of the forbidden characters * @param string $filename * @throws InvalidCharacterInPathException */ protected function checkForbiddenCharacters(string $filename): void { $sanitizedFileName = filter_var($filename, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW); if ($sanitizedFileName !== $filename) { throw new InvalidCharacterInPathException(); } foreach ($this->getForbiddenCharacters() as $char) { if (str_contains($filename, $char)) { throw new InvalidCharacterInPathException($this->l10n->t('"%1$s" is not allowed inside a file or folder name.', [$char])); } } } /** * Check if a filename has a forbidden filename extension * @param string $filename The filename to validate * @throws InvalidPathException */ protected function checkForbiddenExtension(string $filename): void { $filename = mb_strtolower($filename); // Check for forbidden filename extensions $forbiddenExtensions = $this->getForbiddenExtensions(); foreach ($forbiddenExtensions as $extension) { if (str_ends_with($filename, $extension)) { if (str_starts_with($extension, '.')) { throw new InvalidPathException($this->l10n->t('"%1$s" is a forbidden file type.', [$extension]), self::INVALID_FILE_TYPE); } else { throw new InvalidPathException($this->l10n->t('Filenames must not end with "%1$s".', [$extension])); } } } } /** * Helper to get lower case list from config with validation * @return string[] */ private function getConfigValue(string $key, array $fallback): array { $values = $this->config->getSystemValue($key, $fallback); if (!is_array($values)) { $this->logger->error('Invalid system config value for "' . $key . '" is ignored.'); $values = $fallback; } return array_map('mb_strtolower', $values); } }; /a> 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
---
title: Custom Layouts
order: 14
layout: page
---

[[layout.customlayout]]
= Custom Layouts

ifdef::web[]
[.sampler]
image:{live-demo-image}[alt="Live Demo", link="http://demo.vaadin.com/sampler/#ui/layout/custom-layout"]
endif::web[]

While it is possible to create almost any typical layout with the standard
layout components, it is sometimes best to separate the layout completely from
code. With the [classname]#CustomLayout# component, you can write your layout as
a template in HTML that provides locations of any contained components. The
layout template is included in a theme. This separation allows the layout to be
designed separately from code, for example using WYSIWYG web designer tools such
as Adobe Dreamweaver.

A template is a HTML file located under [filename]#layouts# folder under a theme
folder under the [filename]#/VAADIN/themes/# folder, for example,
[filename]#/VAADIN/themes/__themename/layouts/mylayout.html__#.
(Notice that the root path [filename]#/VAADIN/themes/# for themes is
fixed.) A template can also be provided dynamically from an
[classname]#InputStream#, as explained below. A template includes
[literal]#++<div>++# elements with a [parameter]#location# attribute that
defines the location identifier. All custom layout HTML-files must be saved
using UTF-8 character encoding.

[subs="normal"]
----
&lt;table width="100%" height="100%"&gt;
  &lt;tr height="100%"&gt;
    &lt;td&gt;
      &lt;table align="center"&gt;
        &lt;tr&gt;
          &lt;td align="right"&gt;User&amp;nbsp;name:&lt;/td&gt;
          &lt;td&gt;**&lt;div location="username"&gt;&lt;/div&gt;**&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;td align="right"&gt;Password:&lt;/td&gt;
          &lt;td&gt;**&lt;div location="password"&gt;&lt;/div&gt;**&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/table&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td align="right" colspan="2"&gt;
      **&lt;div location="okbutton"&gt;**&lt;/div&gt;
    &lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
----
The client-side engine of Vaadin will replace contents of the location elements
with the components. The components are bound to the location elements by the
location identifier given to [methodname]#addComponent()#, as shown in the
example below.


[source, java]
----
Panel loginPanel = new Panel("Login");
CustomLayout content = new CustomLayout("layoutname");
content.setSizeUndefined();
loginPanel.setContent(content);
loginPanel.setSizeUndefined();

// No captions for fields is they are provided in the template
content.addComponent(new TextField(), "username");
content.addComponent(new TextField(), "password");
content.addComponent(new Button("Login"), "okbutton");
----

The resulting layout is shown below in <<figure.layout.customlayout>>.

[[figure.layout.customlayout]]
.Example of a Custom Layout Component
image::img/customlayout-example1.png[width=40%, scaledwidth=70%]

You can use [methodname]#addComponent()# also to replace an existing component
in the location given in the second parameter.

In addition to a static template file, you can provide a template dynamically
with the [classname]#CustomLayout# constructor that accepts an
[classname]#InputStream# as the template source. For example:


[source, java]
----
new CustomLayout(new ByteArrayInputStream("<b>Template</b>".getBytes()));
----

or


[source, java]
----
new CustomLayout(new FileInputStream(file));
----