2 declare(strict_types
=1);
3 namespace wcf\system\html\output\node
;
4 use wcf\data\smiley\Smiley
;
5 use wcf\data\smiley\SmileyCache
;
6 use wcf\system\application\ApplicationHandler
;
7 use wcf\system\html\node\AbstractHtmlNodeProcessor
;
8 use wcf\system\request\LinkHandler
;
9 use wcf\system\request\RouteHandler
;
11 use wcf\util\exception\CryptoException
;
12 use wcf\util\CryptoUtil
;
14 use wcf\util\StringUtil
;
20 * @author Alexander Ebert
21 * @copyright 2001-2018 WoltLab GmbH
22 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
23 * @package WoltLabSuite\Core\System\Html\Output\Node
26 class HtmlOutputNodeImg
extends AbstractHtmlOutputNode
{
30 protected $tagName = 'img';
35 public function process(array $elements, AbstractHtmlNodeProcessor
$htmlNodeProcessor) {
36 /** @var \DOMElement $element */
37 foreach ($elements as $element) {
38 $class = $element->getAttribute('class');
39 if (preg_match('~\bsmiley\b~', $class)) {
40 $code = $element->getAttribute('alt');
42 /** @var Smiley $smiley */
43 $smiley = SmileyCache
::getInstance()->getSmileyByCode($code);
44 if ($smiley === null ||
$this->outputType
=== 'text/plain') {
45 // output as raw code instead
46 $htmlNodeProcessor->replaceElementWithText($element, ' ' . $code . ' ', false);
49 // enforce database values for src, srcset and style
50 $element->setAttribute('src', $smiley->getURL());
52 if ($smiley->getHeight()) $element->setAttribute('height', (string)$smiley->getHeight());
53 else $element->removeAttribute('height');
55 if ($smiley->smileyPath2x
) $element->setAttribute('srcset', $smiley->getURL2x() . ' 2x');
56 else $element->removeAttribute('srcset');
58 $element->setAttribute('title', WCF
::getLanguage()->get($smiley->smileyTitle
));
62 $src = $element->getAttribute('src');
64 DOMUtil
::removeNode($element);
68 $class = $element->getAttribute('class');
69 if ($class) $class .= ' ';
70 $class .= 'jsResizeImage';
71 $element->setAttribute('class', $class);
73 if (MODULE_IMAGE_PROXY
) {
75 // not a valid URL, discard it
76 DOMUtil
::removeNode($element);
80 $urlComponents = Url
::parse($src);
81 if (empty($urlComponents['host'])) {
82 // relative URL, ignore it
86 if (IMAGE_PROXY_INSECURE_ONLY
&& $urlComponents['scheme'] === 'https') {
87 // proxy is enabled for insecure connections only
91 if ($this->bypassProxy($urlComponents['host'])) {
92 // check if page was requested over a secure connection
93 // but the link is insecure
94 if ((MESSAGE_FORCE_SECURE_IMAGES || RouteHandler
::secureConnection()) && $urlComponents['scheme'] === 'http') {
95 // rewrite protocol to `https`
96 $element->setAttribute('src', preg_replace('~^http~', 'https', $src));
102 $element->setAttribute('data-valid', 'true');
104 if (!empty($urlComponents['path']) && preg_match('~\.svg~', basename($urlComponents['path']))) {
105 // we can't proxy SVG, ignore it
109 $element->setAttribute('src', $this->getProxyLink($src));
111 $srcset = $element->getAttribute('srcset');
113 // simplified regex to check if it appears to be a valid list of sources
114 if (!preg_match('~^[^\s]+\s+[0-9\.]+[wx](,\s*[^\s]+\s+[0-9\.]+[wx])*~', $srcset)) {
115 $element->removeAttribute('srcset');
119 $sources = explode(',', $srcset);
121 foreach ($sources as $source) {
122 $tmp = preg_split('~\s+~', StringUtil
::trim($source));
123 if (!empty($srcset)) $srcset .= ', ';
124 $srcset .= $this->getProxyLink($tmp[0]) . ' ' . $tmp[1];
127 $element->setAttribute('srcset', $srcset);
130 else if (!IMAGE_ALLOW_EXTERNAL_SOURCE
&& !$this->isAllowedOrigin($src)) {
131 $element->parentNode
->insertBefore($element->ownerDocument
->createTextNode('[IMG:'), $element);
133 $link = $element->ownerDocument
->createElement('a');
134 $link->setAttribute('href', $src);
135 $link->textContent
= $src;
136 HtmlOutputNodeA
::markLinkAsExternal($link);
138 $element->parentNode
->insertBefore($link, $element);
140 $element->parentNode
->insertBefore($element->ownerDocument
->createTextNode(']'), $element);
142 $element->parentNode
->removeChild($element);
144 else if (MESSAGE_FORCE_SECURE_IMAGES
&& Url
::parse($src)['scheme'] === 'http') {
145 // rewrite protocol to `https`
146 $element->setAttribute('src', preg_replace('~^http~', 'https', $src));
153 * Validates the domain name against the list of own domains
154 * and whitelisted ones with wildcard support.
156 * @param string $hostname
159 protected function bypassProxy($hostname) {
160 static $hosts = null;
161 static $validHosts = [];
163 if ($hosts === null) {
164 $whitelist = explode("\n", StringUtil
::unifyNewlines(IMAGE_PROXY_HOST_WHITELIST
));
165 foreach ($whitelist as $host) {
167 if (mb_strpos($host, '*') !== false) {
168 $host = preg_replace('~^(\*\.)+~', '', $host);
169 if (mb_strpos($host, '*') !== false ||
$host === '') {
177 $host = mb_strtolower($host);
178 if (!isset($hosts[$host])) $hosts[$host] = $isWildcard;
181 foreach (ApplicationHandler
::getInstance()->getApplications() as $application) {
182 $host = mb_strtolower($application->domainName
);
183 if (!isset($hosts[$host])) $hosts[$host] = false;
187 $hostname = mb_strtolower($hostname);
188 if (isset($hosts[$hostname]) ||
isset($validHosts[$hostname])) {
192 // check wildcard hosts
193 foreach ($hosts as $host => $isWildcard) {
194 if ($isWildcard && mb_strpos($hostname, $host) !== false) {
195 // the prepended dot will ensure that `example.com` matches only
196 // on domains like `foo.example.com` but not on `bar-example.com`
197 if (StringUtil
::endsWith($hostname, '.' . $host)) {
198 $validHosts[$hostname] = $hostname;
210 * Returns the link to fetch the image using the image proxy.
212 * @param string $link
216 protected function getProxyLink($link) {
218 $key = CryptoUtil
::createSignedString($link);
220 return LinkHandler
::getInstance()->getLink('ImageProxy', [
224 catch (CryptoException
$e) {
229 protected function isAllowedOrigin($src) {
231 if ($ownDomains === null) {
232 $ownDomains = array();
233 foreach (ApplicationHandler
::getInstance()->getApplications() as $application) {
234 if (!in_array($application->domainName
, $ownDomains)) {
235 $ownDomains[] = $application->domainName
;
240 $host = Url
::parse($src)['host'];
241 return !$host ||
in_array($host, $ownDomains);