<?php

declare(strict_types=1);
/**
 * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */
namespace OC\Files;

use OCP\Files\EmptyFileNameException;
use OCP\Files\FileNameTooLongException;
use OCP\Files\IFilenameValidator;
use OCP\Files\InvalidCharacterInPathException;
use OCP\Files\InvalidDirectoryException;
use OCP\Files\InvalidPathException;
use OCP\Files\ReservedWordException;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\L10N\IFactory;
use Psr\Log\LoggerInterface;

/**
 * @since 30.0.0
 */
class FilenameValidator implements IFilenameValidator {

	public const INVALID_FILE_TYPE = 100;

	private IL10N $l10n;

	/**
	 * @var list<string>
	 */
	private array $forbiddenNames = [];

	/**
	 * @var list<string>
	 */
	private array $forbiddenBasenames = [];
	/**
	 * @var list<string>
	 */
	private array $forbiddenCharacters = [];

	/**
	 * @var list<string>
	 */
	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<string>
	 * @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<string>
	 * @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<string>
	 * @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<string>
	 * @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>
<a id='n21' href='#n21'>21</a>
<a id='n22' href='#n22'>22</a>
<a id='n23' href='#n23'>23</a>
<a id='n24' href='#n24'>24</a>
<a id='n25' href='#n25'>25</a>
<a id='n26' href='#n26'>26</a>
<a id='n27' href='#n27'>27</a>
<a id='n28' href='#n28'>28</a>
<a id='n29' href='#n29'>29</a>
<a id='n30' href='#n30'>30</a>
<a id='n31' href='#n31'>31</a>
<a id='n32' href='#n32'>32</a>
<a id='n33' href='#n33'>33</a>
<a id='n34' href='#n34'>34</a>
<a id='n35' href='#n35'>35</a>
<a id='n36' href='#n36'>36</a>
<a id='n37' href='#n37'>37</a>
<a id='n38' href='#n38'>38</a>
<a id='n39' href='#n39'>39</a>
<a id='n40' href='#n40'>40</a>
<a id='n41' href='#n41'>41</a>
<a id='n42' href='#n42'>42</a>
<a id='n43' href='#n43'>43</a>
<a id='n44' href='#n44'>44</a>
<a id='n45' href='#n45'>45</a>
<a id='n46' href='#n46'>46</a>
<a id='n47' href='#n47'>47</a>
<a id='n48' href='#n48'>48</a>
<a id='n49' href='#n49'>49</a>
<a id='n50' href='#n50'>50</a>
<a id='n51' href='#n51'>51</a>
<a id='n52' href='#n52'>52</a>
<a id='n53' href='#n53'>53</a>
<a id='n54' href='#n54'>54</a>
<a id='n55' href='#n55'>55</a>
<a id='n56' href='#n56'>56</a>
<a id='n57' href='#n57'>57</a>
<a id='n58' href='#n58'>58</a>
<a id='n59' href='#n59'>59</a>
<a id='n60' href='#n60'>60</a>
<a id='n61' href='#n61'>61</a>
<a id='n62' href='#n62'>62</a>
<a id='n63' href='#n63'>63</a>
<a id='n64' href='#n64'>64</a>
<a id='n65' href='#n65'>65</a>
<a id='n66' href='#n66'>66</a>
<a id='n67' href='#n67'>67</a>
<a id='n68' href='#n68'>68</a>
<a id='n69' href='#n69'>69</a>
<a id='n70' href='#n70'>70</a>
<a id='n71' href='#n71'>71</a>
<a id='n72' href='#n72'>72</a>
<a id='n73' href='#n73'>73</a>
<a id='n74' href='#n74'>74</a>
<a id='n75' href='#n75'>75</a>
<a id='n76' href='#n76'>76</a>
<a id='n77' href='#n77'>77</a>
<a id='n78' href='#n78'>78</a>
<a id='n79' href='#n79'>79</a>
<a id='n80' href='#n80'>80</a>
<a id='n81' href='#n81'>81</a>
<a id='n82' href='#n82'>82</a>
<a id='n83' href='#n83'>83</a>
<a id='n84' href='#n84'>84</a>
<a id='n85' href='#n85'>85</a>
<a id='n86' href='#n86'>86</a>
<a id='n87' href='#n87'>87</a>
<a id='n88' href='#n88'>88</a>
<a id='n89' href='#n89'>89</a>
<a id='n90' href='#n90'>90</a>
<a id='n91' href='#n91'>91</a>
<a id='n92' href='#n92'>92</a>
<a id='n93' href='#n93'>93</a>
<a id='n94' href='#n94'>94</a>
<a id='n95' href='#n95'>95</a>
<a id='n96' href='#n96'>96</a>
<a id='n97' href='#n97'>97</a>
<a id='n98' href='#n98'>98</a>
<a id='n99' href='#n99'>99</a>
<a id='n100' href='#n100'>100</a>
<a id='n101' href='#n101'>101</a>
<a id='n102' href='#n102'>102</a>
</pre></td>
<td class='lines'><pre><code><style>pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */</style><div class="highlight"><pre><span></span>---
title: Custom Layouts
order: 14
layout: page
---

[[layout.customlayout]]
= Custom Layouts

ifdef::web[]
[.sampler]
image:{live-demo-image}[alt=&quot;Live Demo&quot;, link=&quot;http://demo.vaadin.com/sampler/#ui/layout/custom-layout&quot;]
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]#++&lt;div&gt;++# 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=&quot;normal&quot;]
----
&amp;lt;table width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;
  &amp;lt;tr height=&quot;100%&quot;&amp;gt;
    &amp;lt;td&amp;gt;
      &amp;lt;table align=&quot;center&quot;&amp;gt;
        &amp;lt;tr&amp;gt;
          &amp;lt;td align=&quot;right&quot;&amp;gt;User&amp;amp;nbsp;name:&amp;lt;/td&amp;gt;
          &amp;lt;td&amp;gt;**&amp;lt;div location=&quot;username&quot;&amp;gt;&amp;lt;/div&amp;gt;**&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr&amp;gt;
          &amp;lt;td align=&quot;right&quot;&amp;gt;Password:&amp;lt;/td&amp;gt;
          &amp;lt;td&amp;gt;**&amp;lt;div location=&quot;password&quot;&amp;gt;&amp;lt;/div&amp;gt;**&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
      &amp;lt;/table&amp;gt;
    &amp;lt;/td&amp;gt;
  &amp;lt;/tr&amp;gt;
  &amp;lt;tr&amp;gt;
    &amp;lt;td align=&quot;right&quot; colspan=&quot;2&quot;&amp;gt;
      **&amp;lt;div location=&quot;okbutton&quot;&amp;gt;**&amp;lt;/div&amp;gt;
    &amp;lt;/td&amp;gt;
  &amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;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(&quot;Login&quot;);
CustomLayout content = new CustomLayout(&quot;layoutname&quot;);
content.setSizeUndefined();
loginPanel.setContent(content);
loginPanel.setSizeUndefined();

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

The resulting layout is shown below in &lt;&lt;figure.layout.customlayout&gt;&gt;.

[[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(&quot;&lt;b&gt;Template&lt;/b&gt;&quot;.getBytes()));
----

or


[source, java]
----
new CustomLayout(new FileInputStream(file));
----
</pre></div>
</code></pre></td></tr></table>
</div> <!-- class=content -->
<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit v1.2.3</a> (<a href='https://git-scm.com/'>git 2.39.1</a>) at 2025-07-29 14:56:34 +0000</div>
</div> <!-- id=cgit -->
</body>
</html>
