1<?php
2
3declare(strict_types=1);
4
5/**
6 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
7 * @author Jakob Sack <jakob@owncloud.org>
8 * @author Jakob Sack <mail@jakobsack.de>
9 * @author Lukas Reschke <lukas@statuscode.ch>
10 * @author Richard Steinmetz <richard@steinmetz.cloud>
11 * @author Thomas Müller <thomas.mueller@tmit.eu>
12 *
13 * Mail
14 *
15 * This code is free software: you can redistribute it and/or modify
16 * it under the terms of the GNU Affero General Public License, version 3,
17 * as published by the Free Software Foundation.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU Affero General Public License for more details.
23 *
24 * You should have received a copy of the GNU Affero General Public License, version 3,
25 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26 *
27 */
28
29namespace OCA\Mail\Service;
30
31use Closure;
32use HTMLPurifier;
33use HTMLPurifier_Config;
34use HTMLPurifier_HTMLDefinition;
35use HTMLPurifier_URIDefinition;
36use HTMLPurifier_URISchemeRegistry;
37use OCA\Mail\Service\HtmlPurify\CidURIScheme;
38use OCA\Mail\Service\HtmlPurify\TransformStyleURLs;
39use OCA\Mail\Service\HtmlPurify\TransformHTMLLinks;
40use OCA\Mail\Service\HtmlPurify\TransformImageSrc;
41use OCA\Mail\Service\HtmlPurify\TransformNoReferrer;
42use OCA\Mail\Service\HtmlPurify\TransformURLScheme;
43use OCP\IRequest;
44use OCP\IURLGenerator;
45use OCP\Util;
46use Sabberworm\CSS\OutputFormat;
47use Sabberworm\CSS\Parser;
48use Sabberworm\CSS\Value\CSSString;
49use Sabberworm\CSS\Value\URL;
50use Youthweb\UrlLinker\UrlLinker;
51
52require_once __DIR__ . '/../../vendor/cerdic/css-tidy/class.csstidy.php';
53
54class Html {
55
56	/** @var IURLGenerator */
57	private $urlGenerator;
58
59	/** @var IRequest */
60	private $request;
61
62	public function __construct(IURLGenerator $urlGenerator, IRequest $request) {
63		$this->urlGenerator = $urlGenerator;
64		$this->request = $request;
65	}
66
67	/**
68	 * @param string $data
69	 * @return string
70	 */
71	public function convertLinks(string $data): string {
72		$linker = new UrlLinker([
73			'allowFtpAddresses' => true,
74			'allowUpperCaseUrlSchemes' => false,
75		]);
76		$data = $linker->linkUrlsAndEscapeHtml($data);
77
78		$config = HTMLPurifier_Config::createDefault();
79
80		// Append target="_blank" to all link (a) elements
81		$config->set('HTML.TargetBlank', true);
82
83		// allow cid, http and ftp
84		$config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'ftp' => true, 'mailto' => true]);
85		$config->set('URI.Host', Util::getServerHostName());
86
87		// Disable the cache since ownCloud has no really appcache
88		// TODO: Fix this - requires https://github.com/owncloud/core/issues/10767 to be fixed
89		$config->set('Cache.DefinitionImpl', null);
90
91		/** @var HTMLPurifier_HTMLDefinition $uri */
92		$uri = $config->getDefinition('HTML');
93		$uri->info_attr_transform_post['noreferrer'] = new TransformNoReferrer();
94
95		$purifier = new HTMLPurifier($config);
96
97		return $purifier->purify($data);
98	}
99
100	/**
101	 * split off the signature
102	 *
103	 * @param string $body
104	 * @return array
105	 */
106	public function parseMailBody(string $body): array {
107		$signature = null;
108		$parts = preg_split("/-- (\n|(\r\n))/", $body);
109		if (count($parts) > 1) {
110			$signature = array_pop($parts);
111			$body = implode("-- \r\n", $parts);
112		}
113
114		return [
115			$body,
116			$signature
117		];
118	}
119
120	public function sanitizeHtmlMailBody(string $mailBody, array $messageParameters, Closure $mapCidToAttachmentId): string {
121		$config = HTMLPurifier_Config::createDefault();
122
123		// Append target="_blank" to all link (a) elements
124		$config->set('HTML.TargetBlank', true);
125
126		// allow cid, http and ftp
127		$config->set('URI.AllowedSchemes', ['cid' => true, 'http' => true, 'https' => true, 'ftp' => true, 'mailto' => true]);
128		$config->set('URI.Host', Util::getServerHostName());
129
130		$config->set('Filter.ExtractStyleBlocks', true);
131		$config->set('Filter.ExtractStyleBlocks.TidyImpl', false);
132		$config->set('CSS.AllowTricky', true);
133		$config->set('CSS.Proprietary', true);
134
135		// Disable the cache since ownCloud has no really appcache
136		// TODO: Fix this - requires https://github.com/owncloud/core/issues/10767 to be fixed
137		$config->set('Cache.DefinitionImpl', null);
138
139		// Rewrite URL for redirection and proxying of content
140		/** @var HTMLPurifier_HTMLDefinition $html */
141		$html = $config->getDefinition('HTML');
142		$html->info_attr_transform_post['imagesrc'] = new TransformImageSrc($this->urlGenerator);
143		$html->info_attr_transform_post['cssbackground'] = new TransformStyleURLs($this->urlGenerator);
144		$html->info_attr_transform_post['htmllinks'] = new TransformHTMLLinks($this->urlGenerator);
145
146		/** @var HTMLPurifier_URIDefinition $uri */
147		$uri = $config->getDefinition('URI');
148		$uri->addFilter(new TransformURLScheme($messageParameters, $mapCidToAttachmentId, $this->urlGenerator, $this->request), $config);
149
150		HTMLPurifier_URISchemeRegistry::instance()->register('cid', new CidURIScheme());
151
152		$purifier = new HTMLPurifier($config);
153
154		$result = $purifier->purify($mailBody);
155		// eat xml parse errors within HTMLPurifier
156		libxml_clear_errors();
157
158		// Sanitize CSS rules
159		$styles = $purifier->context->get('StyleBlocks');
160		if ($styles) {
161			$joinedStyles = implode("\n", $styles);
162			$result = $this->sanitizeStyleSheet($joinedStyles) . $result;
163		}
164		return $result;
165	}
166
167	/**
168	 * Block all URLs in the given CSS style sheet and return a formatted html style tag.
169	 *
170	 * @param string $styles The CSS style sheet to sanitize.
171	 * @return string Rendered style tag to be used in a html response.
172	 */
173	public function sanitizeStyleSheet(string $styles): string {
174		$cssParser = new Parser($styles);
175		$css = $cssParser->parse();
176
177		// Replace urls with blocked image
178		$blockedUrl = new CSSString($this->urlGenerator->imagePath('mail', 'blocked-image.png'));
179		$hasBlockedContent = false;
180		foreach ($css->getAllValues() as $value) {
181			if ($value instanceof URL) {
182				$value->setURL($blockedUrl);
183				$hasBlockedContent = true;
184			}
185		}
186
187		// Save original styles to be able to restore them later
188		$savedStyles = '';
189		if ($hasBlockedContent) {
190			$savedStyles = 'data-original-content="' . htmlspecialchars($styles) . '"';
191			$styles = $css->render(OutputFormat::createCompact());
192		}
193
194		// Render style tag
195		return implode('', [
196			'<style type="text/css" ', $savedStyles, '>',
197			$styles,
198			'</style>',
199		]);
200	}
201}
202