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