Initial Commit
This commit is contained in:
132
dependencies/spatie/ssl-certificate/src/Downloader.php
vendored
Normal file
132
dependencies/spatie/ssl-certificate/src/Downloader.php
vendored
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate;
|
||||
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate;
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\InvalidIpAddress;
|
||||
class Downloader
|
||||
{
|
||||
/** @var int */
|
||||
protected $port = 443;
|
||||
/** @var string */
|
||||
protected $ipAddress = null;
|
||||
/** @var bool */
|
||||
protected $usingIpAddress = \false;
|
||||
/** @var int */
|
||||
protected $timeout = 30;
|
||||
/** @var bool */
|
||||
protected $enableSni = \true;
|
||||
/** @var bool */
|
||||
protected $capturePeerChain = \false;
|
||||
/** @var array */
|
||||
protected $socketContextOptions = [];
|
||||
/** @var bool */
|
||||
protected $verifyPeer = \true;
|
||||
/** @var bool */
|
||||
protected $verifyPeerName = \true;
|
||||
/** @var int */
|
||||
protected $followLocation = 1;
|
||||
public function usingPort(int $port)
|
||||
{
|
||||
$this->port = $port;
|
||||
return $this;
|
||||
}
|
||||
public function usingSni(bool $sni)
|
||||
{
|
||||
$this->enableSni = $sni;
|
||||
return $this;
|
||||
}
|
||||
public function withSocketContextOptions(array $socketContextOptions)
|
||||
{
|
||||
$this->socketContextOptions = $socketContextOptions;
|
||||
return $this;
|
||||
}
|
||||
public function withFullChain(bool $fullChain)
|
||||
{
|
||||
$this->capturePeerChain = $fullChain;
|
||||
return $this;
|
||||
}
|
||||
public function withVerifyPeer(bool $verifyPeer)
|
||||
{
|
||||
$this->verifyPeer = $verifyPeer;
|
||||
return $this;
|
||||
}
|
||||
public function withVerifyPeerName(bool $verifyPeerName)
|
||||
{
|
||||
$this->verifyPeerName = $verifyPeerName;
|
||||
return $this;
|
||||
}
|
||||
public function setTimeout(int $timeOutInSeconds)
|
||||
{
|
||||
$this->timeout = $timeOutInSeconds;
|
||||
return $this;
|
||||
}
|
||||
public function setFollowLocation(int $followLocation)
|
||||
{
|
||||
$this->followLocation = $followLocation;
|
||||
return $this;
|
||||
}
|
||||
public function fromIpAddress(string $ipAddress)
|
||||
{
|
||||
if (!\filter_var($ipAddress, \FILTER_VALIDATE_IP)) {
|
||||
throw InvalidIpAddress::couldNotValidate($ipAddress);
|
||||
}
|
||||
$this->ipAddress = $ipAddress;
|
||||
$this->usingIpAddress = \true;
|
||||
return $this;
|
||||
}
|
||||
public function getCertificates(string $hostName) : array
|
||||
{
|
||||
$response = $this->fetchCertificates($hostName);
|
||||
$remoteAddress = $response['remoteAddress'];
|
||||
$peerCertificate = $response['options']['ssl']['peer_certificate'];
|
||||
$peerCertificateChain = $response['options']['ssl']['peer_certificate_chain'] ?? [];
|
||||
$fullCertificateChain = \array_merge([$peerCertificate], $peerCertificateChain);
|
||||
$certificates = \array_map(function ($certificate) use($remoteAddress) {
|
||||
$certificateFields = \openssl_x509_parse($certificate);
|
||||
$fingerprint = \openssl_x509_fingerprint($certificate);
|
||||
$fingerprintSha256 = \openssl_x509_fingerprint($certificate, 'sha256');
|
||||
return new SslCertificate($certificateFields, $fingerprint, $fingerprintSha256, $remoteAddress);
|
||||
}, $fullCertificateChain);
|
||||
return \array_unique($certificates);
|
||||
}
|
||||
public function forHost(string $hostName) : SslCertificate
|
||||
{
|
||||
$hostName = (new Url($hostName))->getHostName();
|
||||
$certificates = $this->getCertificates($hostName);
|
||||
return $certificates[0] ?? \false;
|
||||
}
|
||||
public static function downloadCertificateFromUrl(string $url, int $timeout = 30, bool $verifyCertificate = \true) : SslCertificate
|
||||
{
|
||||
return (new static())->setTimeout($timeout)->withVerifyPeer($verifyCertificate)->withVerifyPeerName($verifyCertificate)->forHost($url);
|
||||
}
|
||||
protected function fetchCertificates(string $hostName) : array
|
||||
{
|
||||
$hostName = (new Url($hostName))->getHostName();
|
||||
$sslOptions = ['capture_peer_cert' => \true, 'capture_peer_cert_chain' => $this->capturePeerChain, 'SNI_enabled' => $this->enableSni, 'peer_name' => $hostName, 'verify_peer' => $this->verifyPeer, 'verify_peer_name' => $this->verifyPeerName, 'follow_location' => $this->followLocation];
|
||||
$streamContext = \stream_context_create(['socket' => $this->socketContextOptions, 'ssl' => $sslOptions]);
|
||||
$connectTo = $this->usingIpAddress ? $this->ipAddress : $hostName;
|
||||
$client = @\stream_socket_client("ssl://{$connectTo}:{$this->port}", $errorNumber, $errorDescription, $this->timeout, \STREAM_CLIENT_CONNECT, $streamContext);
|
||||
if (!empty($errorDescription)) {
|
||||
throw $this->buildFailureException($connectTo, $errorDescription);
|
||||
}
|
||||
if (!$client) {
|
||||
$clientErrorMessage = $this->usingIpAddress ? "Could not connect to `{$connectTo}` or it does not have a certificate matching `{$hostName}`." : "Could not connect to `{$connectTo}`.";
|
||||
throw CouldNotDownloadCertificate::unknownError($hostName, $clientErrorMessage);
|
||||
}
|
||||
$response = \stream_context_get_params($client);
|
||||
$response['remoteAddress'] = \stream_socket_get_name($client, \true);
|
||||
\fclose($client);
|
||||
return $response;
|
||||
}
|
||||
protected function buildFailureException(string $hostName, string $errorDescription)
|
||||
{
|
||||
if (\str_contains($errorDescription, 'getaddrinfo failed')) {
|
||||
return CouldNotDownloadCertificate::hostDoesNotExist($hostName);
|
||||
}
|
||||
if (\str_contains($errorDescription, 'error:14090086')) {
|
||||
return CouldNotDownloadCertificate::noCertificateInstalled($hostName);
|
||||
}
|
||||
return CouldNotDownloadCertificate::unknownError($hostName, $errorDescription);
|
||||
}
|
||||
}
|
23
dependencies/spatie/ssl-certificate/src/Exceptions/CouldNotDownloadCertificate.php
vendored
Normal file
23
dependencies/spatie/ssl-certificate/src/Exceptions/CouldNotDownloadCertificate.php
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate\HostDoesNotExist;
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate\NoCertificateInstalled;
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate\UnknownError;
|
||||
class CouldNotDownloadCertificate extends Exception
|
||||
{
|
||||
public static function hostDoesNotExist(string $hostName) : self
|
||||
{
|
||||
return new HostDoesNotExist($hostName);
|
||||
}
|
||||
public static function noCertificateInstalled(string $hostName) : self
|
||||
{
|
||||
return new NoCertificateInstalled($hostName);
|
||||
}
|
||||
public static function unknownError(string $hostName, string $errorMessage) : self
|
||||
{
|
||||
return new UnknownError($hostName, $errorMessage);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate;
|
||||
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate;
|
||||
class HostDoesNotExist extends CouldNotDownloadCertificate
|
||||
{
|
||||
public function __construct(string $hostName)
|
||||
{
|
||||
parent::__construct("The host named `{$hostName}` does not exist.");
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate;
|
||||
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate;
|
||||
class NoCertificateInstalled extends CouldNotDownloadCertificate
|
||||
{
|
||||
public function __construct(string $hostName)
|
||||
{
|
||||
parent::__construct("Could not find a certificate on host named `{$hostName}`.");
|
||||
}
|
||||
}
|
12
dependencies/spatie/ssl-certificate/src/Exceptions/CouldNotDownloadCertificate/UnknownError.php
vendored
Normal file
12
dependencies/spatie/ssl-certificate/src/Exceptions/CouldNotDownloadCertificate/UnknownError.php
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate;
|
||||
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\CouldNotDownloadCertificate;
|
||||
class UnknownError extends CouldNotDownloadCertificate
|
||||
{
|
||||
public function __construct(string $hostName, string $errorMessage)
|
||||
{
|
||||
parent::__construct("Could not download certificate for host `{$hostName}` because {$errorMessage}");
|
||||
}
|
||||
}
|
12
dependencies/spatie/ssl-certificate/src/Exceptions/InvalidIpAddress.php
vendored
Normal file
12
dependencies/spatie/ssl-certificate/src/Exceptions/InvalidIpAddress.php
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions;
|
||||
|
||||
use Exception;
|
||||
class InvalidIpAddress extends Exception
|
||||
{
|
||||
public static function couldNotValidate(string $ipAddress) : self
|
||||
{
|
||||
return new static("String `{$ipAddress}` is not a valid IP address.");
|
||||
}
|
||||
}
|
16
dependencies/spatie/ssl-certificate/src/Exceptions/InvalidUrl.php
vendored
Normal file
16
dependencies/spatie/ssl-certificate/src/Exceptions/InvalidUrl.php
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions;
|
||||
|
||||
use Exception;
|
||||
class InvalidUrl extends Exception
|
||||
{
|
||||
public static function couldNotValidate(string $url) : self
|
||||
{
|
||||
return new static("String `{$url}` is not a valid url.");
|
||||
}
|
||||
public static function couldNotDetermineHost(string $url) : self
|
||||
{
|
||||
return new static("Could not determine host from url `{$url}`.");
|
||||
}
|
||||
}
|
225
dependencies/spatie/ssl-certificate/src/SslCertificate.php
vendored
Normal file
225
dependencies/spatie/ssl-certificate/src/SslCertificate.php
vendored
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate;
|
||||
|
||||
use WP_Ultimo\Dependencies\Carbon\Carbon;
|
||||
use WP_Ultimo\Dependencies\Spatie\Macroable\Macroable;
|
||||
class SslCertificate
|
||||
{
|
||||
use Macroable;
|
||||
/** @var array */
|
||||
protected $rawCertificateFields = [];
|
||||
/** @var string */
|
||||
protected $fingerprint = '';
|
||||
/** @var string */
|
||||
private $fingerprintSha256 = '';
|
||||
/** @var string */
|
||||
private $remoteAddress = '';
|
||||
public static function download() : Downloader
|
||||
{
|
||||
return new Downloader();
|
||||
}
|
||||
public static function createForHostName(string $url, int $timeout = 30, bool $verifyCertificate = \true) : self
|
||||
{
|
||||
return Downloader::downloadCertificateFromUrl($url, $timeout, $verifyCertificate);
|
||||
}
|
||||
public static function createFromFile(string $pathToCertificate) : self
|
||||
{
|
||||
return self::createFromString(\file_get_contents($pathToCertificate));
|
||||
}
|
||||
public static function createFromString(string $certificatePem) : self
|
||||
{
|
||||
$certificateFields = \openssl_x509_parse($certificatePem);
|
||||
$fingerprint = \openssl_x509_fingerprint($certificatePem);
|
||||
$fingerprintSha256 = \openssl_x509_fingerprint($certificatePem, 'sha256');
|
||||
return new self($certificateFields, $fingerprint, $fingerprintSha256);
|
||||
}
|
||||
public function __construct(array $rawCertificateFields, string $fingerprint = '', string $fingerprintSha256 = '', string $remoteAddress = '')
|
||||
{
|
||||
$this->rawCertificateFields = $rawCertificateFields;
|
||||
$this->fingerprint = $fingerprint;
|
||||
$this->fingerprintSha256 = $fingerprintSha256;
|
||||
$this->remoteAddress = $remoteAddress;
|
||||
}
|
||||
public function getRawCertificateFields() : array
|
||||
{
|
||||
return $this->rawCertificateFields;
|
||||
}
|
||||
public function getIssuer() : string
|
||||
{
|
||||
return $this->rawCertificateFields['issuer']['CN'] ?? '';
|
||||
}
|
||||
public function getDomain() : string
|
||||
{
|
||||
if (!\array_key_exists('CN', $this->rawCertificateFields['subject'])) {
|
||||
return '';
|
||||
}
|
||||
if (\is_string($this->rawCertificateFields['subject']['CN'])) {
|
||||
return $this->rawCertificateFields['subject']['CN'];
|
||||
}
|
||||
if (\is_array($this->rawCertificateFields['subject']['CN'])) {
|
||||
return $this->rawCertificateFields['subject']['CN'][0];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
public function getSignatureAlgorithm() : string
|
||||
{
|
||||
return $this->rawCertificateFields['signatureTypeSN'] ?? '';
|
||||
}
|
||||
public function getOrganization() : string
|
||||
{
|
||||
return $this->rawCertificateFields['issuer']['O'] ?? '';
|
||||
}
|
||||
public function getFingerprint() : string
|
||||
{
|
||||
return $this->fingerprint;
|
||||
}
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFingerprintSha256() : string
|
||||
{
|
||||
return $this->fingerprintSha256;
|
||||
}
|
||||
public function getAdditionalDomains() : array
|
||||
{
|
||||
$additionalDomains = \explode(', ', $this->rawCertificateFields['extensions']['subjectAltName'] ?? '');
|
||||
return \array_map(function (string $domain) {
|
||||
return \str_replace('DNS:', '', $domain);
|
||||
}, $additionalDomains);
|
||||
}
|
||||
public function validFromDate() : Carbon
|
||||
{
|
||||
return Carbon::createFromTimestampUTC($this->rawCertificateFields['validFrom_time_t']);
|
||||
}
|
||||
public function expirationDate() : Carbon
|
||||
{
|
||||
return Carbon::createFromTimestampUTC($this->rawCertificateFields['validTo_time_t']);
|
||||
}
|
||||
public function lifespanInDays() : int
|
||||
{
|
||||
return $this->validFromDate()->diffInDays($this->expirationDate());
|
||||
}
|
||||
public function isExpired() : bool
|
||||
{
|
||||
return $this->expirationDate()->isPast();
|
||||
}
|
||||
public function isValid(string $url = null)
|
||||
{
|
||||
if (!Carbon::now()->between($this->validFromDate(), $this->expirationDate())) {
|
||||
return \false;
|
||||
}
|
||||
if (!empty($url)) {
|
||||
return $this->appliesToUrl($url ?? $this->getDomain());
|
||||
}
|
||||
return \true;
|
||||
}
|
||||
public function isSelfSigned() : bool
|
||||
{
|
||||
return $this->getIssuer() === $this->getDomain();
|
||||
}
|
||||
public function usesSha1Hash() : bool
|
||||
{
|
||||
$certificateFields = $this->getRawCertificateFields();
|
||||
if ($certificateFields['signatureTypeSN'] === 'RSA-SHA1') {
|
||||
return \true;
|
||||
}
|
||||
if ($certificateFields['signatureTypeLN'] === 'sha1WithRSAEncryption') {
|
||||
return \true;
|
||||
}
|
||||
return \false;
|
||||
}
|
||||
public function isValidUntil(Carbon $carbon, string $url = null) : bool
|
||||
{
|
||||
if ($this->expirationDate()->lte($carbon)) {
|
||||
return \false;
|
||||
}
|
||||
return $this->isValid($url);
|
||||
}
|
||||
public function daysUntilExpirationDate() : int
|
||||
{
|
||||
$endDate = $this->expirationDate();
|
||||
$interval = Carbon::now()->diff($endDate);
|
||||
return (int) $interval->format('%r%a');
|
||||
}
|
||||
public function getDomains() : array
|
||||
{
|
||||
$allDomains = $this->getAdditionalDomains();
|
||||
$allDomains[] = $this->getDomain();
|
||||
$uniqueDomains = \array_unique($allDomains);
|
||||
return \array_values(\array_filter($uniqueDomains));
|
||||
}
|
||||
public function appliesToUrl(string $url) : bool
|
||||
{
|
||||
if (\filter_var($url, \FILTER_VALIDATE_IP)) {
|
||||
$host = $url;
|
||||
} else {
|
||||
$host = (new Url($url))->getHostName();
|
||||
}
|
||||
$certificateHosts = $this->getDomains();
|
||||
foreach ($certificateHosts as $certificateHost) {
|
||||
$certificateHost = \str_replace('ip address:', '', \strtolower($certificateHost));
|
||||
if ($host === $certificateHost) {
|
||||
return \true;
|
||||
}
|
||||
if ($this->wildcardHostCoversHost($certificateHost, $host)) {
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
return \false;
|
||||
}
|
||||
protected function wildcardHostCoversHost(string $wildcardHost, string $host) : bool
|
||||
{
|
||||
if ($host === $wildcardHost) {
|
||||
return \true;
|
||||
}
|
||||
if (!starts_with($wildcardHost, '*')) {
|
||||
return \false;
|
||||
}
|
||||
if (\substr_count($wildcardHost, '.') < \substr_count($host, '.')) {
|
||||
return \false;
|
||||
}
|
||||
$wildcardHostWithoutWildcard = \substr($wildcardHost, 1);
|
||||
$hostWithDottedPrefix = ".{$host}";
|
||||
return ends_with($hostWithDottedPrefix, $wildcardHostWithoutWildcard);
|
||||
}
|
||||
public function getRawCertificateFieldsJson() : string
|
||||
{
|
||||
return \json_encode($this->getRawCertificateFields());
|
||||
}
|
||||
public function getHash() : string
|
||||
{
|
||||
return \md5($this->getRawCertificateFieldsJson());
|
||||
}
|
||||
public function getRemoteAddress() : string
|
||||
{
|
||||
return $this->remoteAddress;
|
||||
}
|
||||
public function __toString() : string
|
||||
{
|
||||
return $this->getRawCertificateFieldsJson();
|
||||
}
|
||||
public function containsDomain(string $domain) : bool
|
||||
{
|
||||
$certificateHosts = $this->getDomains();
|
||||
foreach ($certificateHosts as $certificateHost) {
|
||||
if ($certificateHost == $domain) {
|
||||
return \true;
|
||||
}
|
||||
if (ends_with($domain, '.' . $certificateHost)) {
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
return \false;
|
||||
}
|
||||
public function isPreCertificate() : bool
|
||||
{
|
||||
if (!\array_key_exists('extensions', $this->rawCertificateFields)) {
|
||||
return \false;
|
||||
}
|
||||
if (!\array_key_exists('ct_precert_poison', $this->rawCertificateFields['extensions'])) {
|
||||
return \false;
|
||||
}
|
||||
return \true;
|
||||
}
|
||||
}
|
37
dependencies/spatie/ssl-certificate/src/Url.php
vendored
Normal file
37
dependencies/spatie/ssl-certificate/src/Url.php
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate;
|
||||
|
||||
use WP_Ultimo\Dependencies\Spatie\SslCertificate\Exceptions\InvalidUrl;
|
||||
class Url
|
||||
{
|
||||
/** @var string */
|
||||
protected $url;
|
||||
/** @var array */
|
||||
protected $parsedUrl;
|
||||
public function __construct(string $url)
|
||||
{
|
||||
if (!starts_with($url, ['http://', 'https://', 'ssl://'])) {
|
||||
$url = "https://{$url}";
|
||||
}
|
||||
if (\function_exists('idn_to_ascii') && \strlen($url) < 61) {
|
||||
$url = \idn_to_ascii($url, \false, \INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
if (!\filter_var($url, \FILTER_VALIDATE_URL)) {
|
||||
throw InvalidUrl::couldNotValidate($url);
|
||||
}
|
||||
$this->url = $url;
|
||||
$this->parsedUrl = \parse_url($url);
|
||||
if (!isset($this->parsedUrl['host'])) {
|
||||
throw InvalidUrl::couldNotDetermineHost($this->url);
|
||||
}
|
||||
}
|
||||
public function getHostName() : string
|
||||
{
|
||||
return $this->parsedUrl['host'];
|
||||
}
|
||||
public function getPort() : int
|
||||
{
|
||||
return $this->parsedUrl['port'] ?? 443;
|
||||
}
|
||||
}
|
71
dependencies/spatie/ssl-certificate/src/helpers.php
vendored
Normal file
71
dependencies/spatie/ssl-certificate/src/helpers.php
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Spatie\SslCertificate;
|
||||
|
||||
function starts_with($haystack, $needles) : bool
|
||||
{
|
||||
foreach ((array) $needles as $needle) {
|
||||
if ($needle != '' && \mb_strpos($haystack, $needle) === 0) {
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
return \false;
|
||||
}
|
||||
/**
|
||||
* Determine if a given string ends with a given substring.
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string|array $needles
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function ends_with(string $haystack, $needles) : bool
|
||||
{
|
||||
foreach ((array) $needles as $needle) {
|
||||
if ((string) $needle === \substr($haystack, -length($needle))) {
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
return \false;
|
||||
}
|
||||
/**
|
||||
* Returns the portion of string specified by the start and length parameters.
|
||||
*
|
||||
* @param string $string
|
||||
* @param int $start
|
||||
* @param int|null $length
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function substr(string $string, int $start, int $length = null) : string
|
||||
{
|
||||
return \mb_substr($string, $start, $length, 'UTF-8');
|
||||
}
|
||||
/**
|
||||
* Return the length of the given string.
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
function length(string $value) : int
|
||||
{
|
||||
return \mb_strlen($value);
|
||||
}
|
||||
/**
|
||||
* Determine if a given string contains a given substring.
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string|array $needles
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function str_contains(string $haystack, $needles) : bool
|
||||
{
|
||||
foreach ((array) $needles as $needle) {
|
||||
if ($needle != '' && \mb_strpos($haystack, $needle) !== \false) {
|
||||
return \true;
|
||||
}
|
||||
}
|
||||
return \false;
|
||||
}
|
Reference in New Issue
Block a user