<?php declare (strict_types=1); /* * This file is part of the Monolog package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WPMailSMTP\Vendor\Monolog\Handler; use WPMailSMTP\Vendor\Monolog\Logger; /** * Stores to any socket - uses fsockopen() or pfsockopen(). * * @author Pablo de Leon Belloc <pablolb@gmail.com> * @see http://php.net/manual/en/function.fsockopen.php * * @phpstan-import-type Record from \Monolog\Logger * @phpstan-import-type FormattedRecord from AbstractProcessingHandler */ class SocketHandler extends \WPMailSMTP\Vendor\Monolog\Handler\AbstractProcessingHandler { /** @var string */ private $connectionString; /** @var float */ private $connectionTimeout; /** @var resource|null */ private $resource; /** @var float */ private $timeout; /** @var float */ private $writingTimeout; /** @var ?int */ private $lastSentBytes = null; /** @var ?int */ private $chunkSize; /** @var bool */ private $persistent; /** @var ?int */ private $errno = null; /** @var ?string */ private $errstr = null; /** @var ?float */ private $lastWritingAt = null; /** * @param string $connectionString Socket connection string * @param bool $persistent Flag to enable/disable persistent connections * @param float $timeout Socket timeout to wait until the request is being aborted * @param float $writingTimeout Socket timeout to wait until the request should've been sent/written * @param float|null $connectionTimeout Socket connect timeout to wait until the connection should've been * established * @param int|null $chunkSize Sets the chunk size. Only has effect during connection in the writing cycle * * @throws \InvalidArgumentException If an invalid timeout value (less than 0) is passed. */ public function __construct(string $connectionString, $level = \WPMailSMTP\Vendor\Monolog\Logger::DEBUG, bool $bubble = \true, bool $persistent = \false, float $timeout = 0.0, float $writingTimeout = 10.0, ?float $connectionTimeout = null, ?int $chunkSize = null) { parent::__construct($level, $bubble); $this->connectionString = $connectionString; if ($connectionTimeout !== null) { $this->validateTimeout($connectionTimeout); } $this->connectionTimeout = $connectionTimeout ?? (float) \ini_get('default_socket_timeout'); $this->persistent = $persistent; $this->validateTimeout($timeout); $this->timeout = $timeout; $this->validateTimeout($writingTimeout); $this->writingTimeout = $writingTimeout; $this->chunkSize = $chunkSize; } /** * Connect (if necessary) and write to the socket * * {@inheritDoc} * * @throws \UnexpectedValueException * @throws \RuntimeException */ protected function write(array $record) : void { $this->connectIfNotConnected(); $data = $this->generateDataStream($record); $this->writeToSocket($data); } /** * We will not close a PersistentSocket instance so it can be reused in other requests. */ public function close() : void { if (!$this->isPersistent()) { $this->closeSocket(); } } /** * Close socket, if open */ public function closeSocket() : void { if (\is_resource($this->resource)) { \fclose($this->resource); $this->resource = null; } } /** * Set socket connection to be persistent. It only has effect before the connection is initiated. */ public function setPersistent(bool $persistent) : self { $this->persistent = $persistent; return $this; } /** * Set connection timeout. Only has effect before we connect. * * @see http://php.net/manual/en/function.fsockopen.php */ public function setConnectionTimeout(float $seconds) : self { $this->validateTimeout($seconds); $this->connectionTimeout = $seconds; return $this; } /** * Set write timeout. Only has effect before we connect. * * @see http://php.net/manual/en/function.stream-set-timeout.php */ public function setTimeout(float $seconds) : self { $this->validateTimeout($seconds); $this->timeout = $seconds; return $this; } /** * Set writing timeout. Only has effect during connection in the writing cycle. * * @param float $seconds 0 for no timeout */ public function setWritingTimeout(float $seconds) : self { $this->validateTimeout($seconds); $this->writingTimeout = $seconds; return $this; } /** * Set chunk size. Only has effect during connection in the writing cycle. */ public function setChunkSize(int $bytes) : self { $this->chunkSize = $bytes; return $this; } /** * Get current connection string */ public function getConnectionString() : string { return $this->connectionString; } /** * Get persistent setting */ public function isPersistent() : bool { return $this->persistent; } /** * Get current connection timeout setting */ public function getConnectionTimeout() : float { return $this->connectionTimeout; } /** * Get current in-transfer timeout */ public function getTimeout() : float { return $this->timeout; } /** * Get current local writing timeout * * @return float */ public function getWritingTimeout() : float { return $this->writingTimeout; } /** * Get current chunk size */ public function getChunkSize() : ?int { return $this->chunkSize; } /** * Check to see if the socket is currently available. * * UDP might appear to be connected but might fail when writing. See http://php.net/fsockopen for details. */ public function isConnected() : bool { return \is_resource($this->resource) && !\feof($this->resource); // on TCP - other party can close connection. } /** * Wrapper to allow mocking * * @return resource|false */ protected function pfsockopen() { return @\pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); } /** * Wrapper to allow mocking * * @return resource|false */ protected function fsockopen() { return @\fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout); } /** * Wrapper to allow mocking * * @see http://php.net/manual/en/function.stream-set-timeout.php * * @return bool */ protected function streamSetTimeout() { $seconds = \floor($this->timeout); $microseconds = \round(($this->timeout - $seconds) * 1000000.0); if (!\is_resource($this->resource)) { throw new \LogicException('streamSetTimeout called but $this->resource is not a resource'); } return \stream_set_timeout($this->resource, (int) $seconds, (int) $microseconds); } /** * Wrapper to allow mocking * * @see http://php.net/manual/en/function.stream-set-chunk-size.php * * @return int|bool */ protected function streamSetChunkSize() { if (!\is_resource($this->resource)) { throw new \LogicException('streamSetChunkSize called but $this->resource is not a resource'); } if (null === $this->chunkSize) { throw new \LogicException('streamSetChunkSize called but $this->chunkSize is not set'); } return \stream_set_chunk_size($this->resource, $this->chunkSize); } /** * Wrapper to allow mocking * * @return int|bool */ protected function fwrite(string $data) { if (!\is_resource($this->resource)) { throw new \LogicException('fwrite called but $this->resource is not a resource'); } return @\fwrite($this->resource, $data); } /** * Wrapper to allow mocking * * @return mixed[]|bool */ protected function streamGetMetadata() { if (!\is_resource($this->resource)) { throw new \LogicException('streamGetMetadata called but $this->resource is not a resource'); } return \stream_get_meta_data($this->resource); } private function validateTimeout(float $value) : void { if ($value < 0) { throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got {$value})"); } } private function connectIfNotConnected() : void { if ($this->isConnected()) { return; } $this->connect(); } /** * @phpstan-param FormattedRecord $record */ protected function generateDataStream(array $record) : string { return (string) $record['formatted']; } /** * @return resource|null */ protected function getResource() { return $this->resource; } private function connect() : void { $this->createSocketResource(); $this->setSocketTimeout(); $this->setStreamChunkSize(); } private function createSocketResource() : void { if ($this->isPersistent()) { $resource = $this->pfsockopen(); } else { $resource = $this->fsockopen(); } if (\is_bool($resource)) { throw new \UnexpectedValueException("Failed connecting to {$this->connectionString} ({$this->errno}: {$this->errstr})"); } $this->resource = $resource; } private function setSocketTimeout() : void { if (!$this->streamSetTimeout()) { throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()"); } } private function setStreamChunkSize() : void { if ($this->chunkSize && !$this->streamSetChunkSize()) { throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()"); } } private function writeToSocket(string $data) : void { $length = \strlen($data); $sent = 0; $this->lastSentBytes = $sent; while ($this->isConnected() && $sent < $length) { if (0 == $sent) { $chunk = $this->fwrite($data); } else { $chunk = $this->fwrite(\substr($data, $sent)); } if ($chunk === \false) { throw new \RuntimeException("Could not write to socket"); } $sent += $chunk; $socketInfo = $this->streamGetMetadata(); if (\is_array($socketInfo) && $socketInfo['timed_out']) { throw new \RuntimeException("Write timed-out"); } if ($this->writingIsTimedOut($sent)) { throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent {$sent} of {$length})"); } } if (!$this->isConnected() && $sent < $length) { throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent {$sent} of {$length})"); } } private function writingIsTimedOut(int $sent) : bool { // convert to ms if (0.0 == $this->writingTimeout) { return \false; } if ($sent !== $this->lastSentBytes) { $this->lastWritingAt = \microtime(\true); $this->lastSentBytes = $sent; return \false; } else { \usleep(100); } if (\microtime(\true) - $this->lastWritingAt >= $this->writingTimeout) { $this->closeSocket(); return \true; } return \false; } }