[MERGE] Branch 'master' of github_atticmedia:cjel/typo3-templates_aide

This commit is contained in:
Philipp Dieter
2020-12-03 03:48:39 +01:00
15 changed files with 1607 additions and 8 deletions

View File

@@ -0,0 +1,689 @@
<?php
namespace Cjel\TemplatesAide\Controller;
/***
*
* This file is part of the "Templates Aide" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* (c) 2018 Philipp Dieter <philippdieter@attic-media.net>
*
***/
use \Opis\JsonSchema\{
Validator, ValidationResult, ValidationError, Schema
};
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController as BaseController;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
use TYPO3\CMS\Extbase\Property\PropertyMapper;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationBuilder;
use TYPO3\CMS\Extbase\Service\ExtensionService;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
class ActionController extends BaseController
{
/*
* page type
*/
protected $pageType = null;
/*
* content object uid
*/
protected $contentObjectUid = null;
/*
* cacheManager
*/
protected $cacheManager = null;
/*
* cache
*/
protected $cache = null;
/**
* data mapper
*/
protected $dataMapper = null;
/*
* logManager
*/
protected $logManager = null;
/*
* logger
*/
protected $importLogger = null;
/*
* logger
*/
protected $generalLogger = null;
/**
* request body
* will only be set if page request action is post
*/
protected $requestBody = null;
/**
* page type for ajax requests
*/
protected $ajaxPageType = 5000;
/**
* response stus
*/
protected $responseStatus = 200;
/**
* component mode, used in frontend
*/
protected $componentMode = 'default';
/**
* redirect url
*/
protected $redirect = null;
/**
* is valid
*/
protected $isValid = true;
/**
* errors
*/
protected $errors = [];
/**
* errors labels
*/
protected $errorLabels = [];
/**
* ajaxEnv
*/
protected $ajaxEnv = [];
/**
* @var \TYPO3\CMS\Extbase\Service\ExtensionService
*/
protected $extensionService;
/**
* uribuilder
*/
protected $uriBuilder = null;
/**
* propertyMappginConfigrtationBuolder
*/
protected $propertyMapperConfigurationBuilder;
/**
* @param \TYPO3\CMS\Extbase\Service\ExtensionService $extensionService
*/
public function injectExtensionService(ExtensionService $extensionService)
{
$this->extensionService = $extensionService;
}
/**
* propertyMapper
*
* @var PropertyMapper
*/
protected $propertyMapper;
/**
* @param
*/
public function injectPropertyMapper(
PropertyMapper $propertyMapper
) {
$this->propertyMapper = $propertyMapper;
}
/**
* propertyMappingConfigurationBuilder
*
* @var PropertyMappingConfigurationBuilder
*/
protected $propertyMappingConfigurationBuilder;
/**
* @param
*/
public function injectPropertyMappingConfigurationBuilder(
PropertyMappingConfigurationBuilder $propertyMappingConfigurationBuilder
) {
$this->propertyMappingConfigurationBuilder
= $propertyMappingConfigurationBuilder;
}
/*
* initialize action
*
* @return void
*/
public function initializeAction()
{
$this->pageType = GeneralUtility::_GP('type');
if (!is_numeric($this->pageType)) {
$this->pageType = 0;
}
if ($this->request->getMethod() == 'POST') {
$this->requestBody = json_decode(
file_get_contents('php://input')
);
}
$this->contentObjectUid =
$this->configurationManager->getContentObject()->data['uid'];
$this->cacheManager = $this->objectManager->get(
CacheManager::class
);
//$this->cache = $this->cacheManager->getCache(
// 'tobereplaced' //TODO: Replaceme
//);
$this->logManager = $this->objectManager->get(
LogManager::Class
);
$this->importLogger = $this->logManager->getLogger(
'importLogger'
);
$this->generalLogger = $this->logManager->getLogger(
__CLASS__
);
$this->dataMapper = $this->objectManager->get(
DataMapper::Class
);
$this->arguments->addNewArgument('step', 'string', false, false);
$this->arguments->addNewArgument('submit', 'string', false, false);
}
/**
* shortcut
*
* @return void
*/
protected function getExtensionKey()
{
return $this->request->getControllerExtensionKey();
}
/**
* shortcut function to recieve typoscript
*
* @return array
*/
protected function getPluginTyposcript()
{
return $this->configurationManager->getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS,
str_replace('_', '', $this->getExtensionKey),
$this->request->getPluginName()
);
}
/**
* shortcut function to recieve typoscript
*
* @return array
*/
protected function getTyposcript()
{
return $this->configurationManager->getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS
);
}
/**
* shortcut to get to know if request is submittet via post
*
* @return void
*/
protected function isPost()
{
if ($this->request->getMethod() == 'POST'){
return true;
}
return false;
}
/**
* shortcut to get to know if request is submittet via post and specific
* step is set
*
* @return void
*/
protected function isPostStep(
$testValue = null
) {
return $this->isPostAndArgumentMatches('step', $testValue);
}
/**
*
*/
protected function isPostSubmit(
$testValue = null
) {
return $this->isPostAndArgumentMatches('submit', $testValue);
}
/**
*
*/
protected function isPostAndArgumentMatches(
$argument,
$testValue
) {
$value = null;
if ($this->arguments->hasArgument($argument)){
$value = $this->arguments->getArgument($argument)->getValue();
}
if (
$this->request->getMethod() == 'POST'
&& $value == $testValue
){
return true;
}
return false;
}
protected function einTest(
$actions = []
) {
}
/**
*
*/
protected function getPostSubmit()
{
return explode('#', $this->getPostValue('submit'))[0];
}
/**
*
*/
protected function getPostSubmitItem()
{
return explode('#', $this->getPostValue('submit'))[1];
}
/**
*
*/
protected function getPostValue(
$argument
) {
if ($this->arguments->hasArgument($argument)){
return $this->arguments->getArgument($argument)->getValue();
}
return false;
}
/**
*
*/
protected function getGetValue(
$argument
) {
if (GeneralUtility::_GP($argument)) {
return GeneralUtility::_GP($argument);
}
return false;
}
/**
* shortcut to get translation
*
* @return void
*/
protected function getTranslation($key, $arguments = null)
{
return LocalizationUtility::translate(
$key,
'tobereplaced', //TODO: Replace me
$arguments
);
}
/**
* gets error label based on field and keyword, uses predefined extensionkey
*/
protected function getErrorLabel($field, $keyword) {
$path = 'error.' . $field . '.' . $keyword;
$errorLabel = $this->getTranslation($path);
if ($errorLabel == null) {
return $path;
}
return $errorLabel;
}
/**
* function to add validation error manually in the controller
*/
protected function addValidationError($field, $keyword) {
$this->responseStatus = [400 => 'validationError'];
$this->errors[$field] = [
'keyword' => $keyword,
];
$this->errorLabels[$field] = $this->getErrorLabel(
$field,
$keyword
);
}
public function arrayRemoveEmptyStrings($array)
{
foreach ($array as $key => &$value) {
if (is_array($value)) {
$value = $this->arrayRemoveEmptyStrings($value);
} else {
if (is_string($value) && !strlen($value)) {
unset($array[$key]);
}
}
}
unset($value);
return $array;
}
public static function arrayToObject($array) {
if (is_array($array)) {
return (object) array_map([__CLASS__, __METHOD__], $array);
} else {
return $array;
}
}
/**
* validate objects
*
* @param $input
* @param schema
* @return void
*/
protected function validateInput($input, $schema)
{
$validator = new Validator();
$input = $this->arrayRemoveEmptyStrings($input);
//@todo make optional when usiing rest api
//array_walk_recursive(
// $input,
// function (&$value) {
// if (filter_var($value, FILTER_VALIDATE_INT)) {
// $value = (int)$value;
// }
// }
//);
$input = $this->arrayToObject($input);
$validationResult = $validator->dataValidation(
$input,
json_encode($schema),
-1
);
if (!$validationResult->isValid()) {
$this->isValid = false;
$this->responseStatus = [400 => 'validationError'];
foreach ($validationResult->getErrors() as $error){
$errorLabel = null;
$field = implode('.', $error->dataPointer());
if ($error->keyword() == 'required') {
$tmp = $error->dataPointer();
array_push($tmp, $error->keywordArgs()['missing']);
$field = implode('.', $tmp);
}
if ($error->keyword() == 'additionalProperties') {
continue;
}
$this->errors[$field] = [
'keyword' => $error->keyword(),
'details' => $error->keywordArgs()
];
if ($error->keyword() != 'required') {
$errorLabel = $this->getTranslation(
'error.' . $field . '.' . $error->keyword()
);
//if ($errorLabel == null) {
// $errorLabel = $this->getTranslation(
// 'error.' . $field . '.required'
// );
//}
if ($errorLabel == null) {
$errorLabel = 'error.'
. $field
. '.'
. $error->keyword();
}
$this->errorLabels[$field] = $errorLabel;
} else {
$errorLabel = $this->getTranslation(
'error.' . $field . '.required'
);
if ($errorLabel == null) {
$errorLabel = 'error.'
. $field
. '.'
. $error->keyword();
}
$this->errorLabels[$field] = $errorLabel;
}
}
}
return $validationResult->isValid();
}
/**
* returns plugin namespace to build js post request
*
* @return void
*/
protected function getPluginNamespace()
{
$extensionName = $this->request->getControllerExtensionName();
$pluginName = $this->request->getPluginName();
return $this->extensionService->getPluginNamespace(
$extensionName,
$pluginName
);
}
/**
* sets vars which are needed by the ajax requests
*
* @return void
*/
protected function setAjaxEnv($object = null)
{
if ($object == null) {
$object = $this->arguments->getArgumentNames()[0];
}
$uri = $this->getControllerContext()
->getUriBuilder()
->reset()
->setCreateAbsoluteUri(true)
->setTargetPageType($this->ajaxPageType)
->setArguments(['cid' => $this->contentObjectUid])
->uriFor($this->request->getControllerActionName());
$this->ajaxEnv = [
'uri' => $uri,
'object' => $object,
'namespace' => $this->getPluginNamespace(),
];
}
/**
* The hash service class to use
*
* @var \TYPO3\CMS\Extbase\Security\Cryptography\HashService
*/
protected $hashService;
/**
* @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
*/
public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
{
$this->hashService = $hashService;
}
/**
* get property mapper config
*
* @return void
*/
protected function getPropertyMappingConfiguration($attribute)
{
$propertyMappingConfiguration = $this
->propertyMappingConfigurationBuilder->build();
$this->initializePropertyMappingConfigurationFromRequest(
$this->request,
$propertyMappingConfiguration,
$attribute
);
return $propertyMappingConfiguration;
}
/**
* Initialize the property mapping configuration in $controllerArguments if
* the trusted properties are set inside the request.
*
* @param \TYPO3\CMS\Extbase\Mvc\Request $request
* @param \TYPO3\CMS\Extbase\Mvc\Controller\Arguments $controllerArguments
* @throws BadRequestException
*/
public function initializePropertyMappingConfigurationFromRequest(\TYPO3\CMS\Extbase\Mvc\Request $request, $propertyMappingConfiguration, $propertyNameTest)
{
$trustedPropertiesToken = $request->getInternalArgument('__trustedProperties');
if (!is_string($trustedPropertiesToken)) {
return;
}
try {
$serializedTrustedProperties = $this->hashService->validateAndStripHmac($trustedPropertiesToken);
} catch (InvalidHashException | InvalidArgumentForHashGenerationException $e) {
throw new BadRequestException('The HMAC of the form could not be validated.', 1581862822);
}
$trustedProperties = unserialize($serializedTrustedProperties, ['allowed_classes' => false]);
foreach ($trustedProperties as $propertyName => $propertyConfiguration) {
//if (!$controllerArguments->hasArgument($propertyName)) {
// continue;
//}
if ($propertyName != $propertyNameTest) {
continue;
}
//$propertyMappingConfiguration = $controllerArguments->getArgument($propertyName)->getPropertyMappingConfiguration();
$this->modifyPropertyMappingConfiguration($propertyConfiguration, $propertyMappingConfiguration);
}
}
/**
* Modify the passed $propertyMappingConfiguration according to the $propertyConfiguration which
* has been generated by Fluid. In detail, if the $propertyConfiguration contains
* an __identity field, we allow modification of objects; else we allow creation.
*
* All other properties are specified as allowed properties.
*
* @param array $propertyConfiguration
* @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration
*/
protected function modifyPropertyMappingConfiguration($propertyConfiguration, \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration)
{
if (!is_array($propertyConfiguration)) {
return;
}
if (isset($propertyConfiguration['__identity'])) {
$propertyMappingConfiguration->setTypeConverterOption(\TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter::class, \TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED, true);
unset($propertyConfiguration['__identity']);
} else {
$propertyMappingConfiguration->setTypeConverterOption(\TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter::class, \TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, true);
}
foreach ($propertyConfiguration as $innerKey => $innerValue) {
if (is_array($innerValue)) {
$this->modifyPropertyMappingConfiguration($innerValue, $propertyMappingConfiguration->forProperty($innerKey));
}
$propertyMappingConfiguration->allowProperties($innerKey);
}
}
/**
* return function, checks for page type and decides
*
* @param array $result
* @return void
*/
protected function returnFunction($result = [], $errorStatus = null)
{
$this->setAjaxEnv();
if ($result == null) {
$result = [];
}
if (!empty($this->errors)) {
$result = array_merge(
$result,
['errors' => $this->errors]
);
}
if (!empty($this->errorLabels)) {
$result = array_merge(
$result,
['errorLabels' => $this->errorLabels]
);
}
if (is_array($this->responseStatus)) {
$result = array_merge(
$result,
['errorType' => reset($this->responseStatus)]
);
}
if ($this->pageType) {
if (is_array($this->responseStatus)) {
$this->response->setStatus(
array_key_first($this->responseStatus)
);
} else {
$this->response->setStatus($this->responseStatus);
}
if ($this->pageType == $this->ajaxPageType) {
$GLOBALS['TSFE']->setContentType('application/json');
}
unset($result['data']);
if ($this->redirect) {
$result['redirect'] = $this->redirect;
}
return json_encode($result);
}
$result = array_merge(
$result,
['cid' => $this->contentObjectUid],
['isValid' => $this->isValid],
['componentMode' => $this->componentMode]
);
if (!empty($this->ajaxEnv)) {
$result = array_merge(
$result,
['ajaxEnv' => $this->ajaxEnv]
);
}
$this->view->assignMultiple($result);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Cjel\TemplatesAide\Property\TypeConverter;
/***
*
* This file is part of the "Templates Aide" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* (c) 2020 Philipp Dieter <philippdieter@attic-media.net>
*
***/
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
/**
* Converter which transforms arrays to arrays.
*/
class Double2Converter extends AbstractTypeConverter
{
/**
* @var array<string>
*/
protected $sourceTypes = ['integer', 'string'];
/**
* @var string
*/
protected $targetType = 'double2';
/**
* @var int
*/
protected $priority = 10;
/**
* @param mixed $source
* @param string $targetType
* @return bool
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function canConvertFrom($source, $targetType)
{
return is_string($source) ||is_integer($source);
}
/**
* Copied from
* TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval
*
* @param string|array $source
* @param string $targetType
* @param array $convertedChildProperties
* @param PropertyMappingConfigurationInterface $configuration
* @return array
*/
public function convertFrom(
$source,
$targetType,
array $convertedChildProperties = [],
PropertyMappingConfigurationInterface $configuration = null
) {
$value = preg_replace('/[^0-9,\\.-]/', '', $source);
$negative = $value[0] === '-';
$value = strtr($value, [',' => '.', '-' => '']);
if (strpos($value, '.') === false) {
$value .= '.0';
}
$valueArray = explode('.', $value);
$dec = array_pop($valueArray);
$value = implode('', $valueArray) . '.' . $dec;
if ($negative) {
$value *= -1;
}
$value = number_format($value, 2, '.', '');
//\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump(
// $source, null, 3
//);
//if (is_string($source)) {
// if ($source === '') {
// $source = [];
// }
//}
return $value;
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace Cjel\TemplatesAide\Utility;
/***
*
* This file is part of the "Templates Aide" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* (c) 2020 Philipp Dieter <philipp.dieter@attic-media.net>
*
***/
use TYPO3\CMS\Core\Mail\MailMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Fluid\View\TemplatePaths;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
/**
*
*/
class MailUtility
{
/**
* tages maildata, builds html and text mails an decides where to send them
* allows to intercept sender for testing
*
* @param string $target email or group identifier
* @param string $subject mail subject, prefixed by setting in ts
* @param array $data content for email, gets parsed in different ways
* @return void
*/
public static function sendMail(
$target,
$sender,
$subject,
$data,
$templateNameHtml = null,
$templateNameText = null
) {
if (!$templateNameHtml) {
$templateNameHtml = 'Mails/DefaultHtml';
}
if (!$templateNameText) {
$templateNameText = 'Mails/DefaultText';
}
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
$configurationManager = $objectManager->get(
ConfigurationManagerInterface::class
);
$typoScript = $configurationManager->getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT
);
$settings =
(array)$typoScript['module.']['tx_templatesaide.']['settings.'];
$settings = GeneralUtility::removeDotsFromTS($settings);
$htmlView = $objectManager->get(StandaloneView::class);
$htmlView->getTemplatePaths()->fillDefaultsByPackageName(
'templates_aide'
);
$htmlView->setTemplate($templateNameHtml);
$textView = $objectManager->get(StandaloneView::class);
$textView->getTemplatePaths()->fillDefaultsByPackageName(
'templates_aide'
);
$textView->setTemplate($templateNameText);
$mail = GeneralUtility::makeInstance(MailMessage::class);
$mail->setFrom($sender);
$mail->setSubject($subject);
$bodydataText = [];
$bodydataHtml = [];
foreach ($data as $row) {
switch($row['type']) {
case 'text':
case 'headline':
$htmlRow = $row;
$htmlRow['data'] = preg_replace_callback(
'/\[.*\]/mU',
function($matches) {
foreach ($matches as $match) {
return preg_replace_callback(
'/\[(\S*)\s(.*)\]/mU',
function($matchesInner) {
return '<a href="'
. $matchesInner[1]
. '">'
. $matchesInner[2]
. '</a>';
},
$match
);
}
},
$htmlRow['data']
);
$textRow = $row;
$textRow['data'] = preg_replace_callback(
'/\[.*\]/mU',
function($matches) {
foreach ($matches as $match) {
return preg_replace_callback(
'/\[(\S*)\s(.*)\]/mU',
function($matchesInner) {
return $matchesInner[2]
. ': '
. $matchesInner[1];
},
$match
);
}
},
$textRow['data']
);
$bodydataText[] = $textRow;
$bodydataHtml[] = $htmlRow;
break;
case 'button':
case 'buttons':
$htmlRow = $row;
//$htmlRow['targets'] = preg_replace_callback(
// '/\[.*\]/mU',
// function($matches) {
// foreach ($matches as $match) {
// return preg_replace_callback(
// '/\[(\S*)\s(.*)\]/mU',
// function($matchesInner) {
// return $matchesInner;
// //return '<a href="'
// // . $matchesInner[1]
// // . '">'
// // . $matchesInner[2]
// // . '</a>';
// },
// $match
// );
// }
// },
// $htmlRow['targets']
//);
$textRow = $row;
//$textRow['targets'] = preg_replace_callback(
// '/\[.*\]/mU',
// function($matches) {
// foreach ($matches as $match) {
// return preg_replace_callback(
// '/\[(\S*)\s(.*)\]/mU',
// function($matchesInner) {
// return $matchesInner;
// //return $matchesInner[2]
// // . ': '
// // . $matchesInner[1];
// },
// $match
// );
// }
// },
// $textRow['targets']
//);
$bodydataText[] = $textRow;
$bodydataHtml[] = $htmlRow;
break;
case 'attachmentBase64':
$attachmentdata = explode(',', $row['data']);
preg_match('/\w*:(.*);\w*/', $attachmentdata[0], $matches);
$mimetype = $matches[1];
preg_match('/\w*\/(.*);\w*/', $attachmentdata[0], $matches);
$fileextension = $matches[1];
$mail->attach(new \Swift_Attachment(
base64_decode($attachmentdata[1]),
'attachment.' . $fileextension,
$mimetype
));
break;
}
}
$textView->assign('content', $bodydataText);
$htmlView->assign('content', $bodydataHtml);
$domain = $settings['mailDomain'];
$htmlView->assign('domain', $domain);
$textBody = $textView->render();
$htmlBody = $htmlView->render();
$mail->setBody($textBody);
$mail->addPart($htmlBody, 'text/html');
$recipients = explode(
',',
$target
);
if ($GLOBALS['TYPO3_CONF_VARS']['MAIL']['intercept_to']) {
$subjectOrig = $mail->getSubject();
$recipientsIntercecpted = explode(
',',
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['intercept_to']
);
foreach ($recipientsIntercecpted as $recipientIntercepted) {
foreach ($recipients as $recipient) {
$mail->setSubject(
$subjectOrig . ' [ORIG-TO: ' . trim($recipient) . ']'
);
$mail->setTo(trim($recipientIntercepted));
$mail->send();
}
}
} else {
foreach ($recipients as $recipient) {
$mail->setTo(trim($recipient));
$mail->send();
}
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Cjel\TemplatesAide\Utility;
/***
*
* This file is part of the "" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* (c) 2019 Philipp Dieter <philipp@glanzstueck.agency>, Glanzstück GmbH
*
***/
class RandomStringUtility
{
public static function getToken(
int $length = 64,
string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
): string {
if ($length < 1) {
throw new \RangeException("Length must be a positive integer");
}
$pieces = [];
$max = mb_strlen($keyspace, '8bit') - 1;
for ($i = 0; $i < $length; ++$i) {
$pieces []= $keyspace[random_int(0, $max)];
}
return implode('', $pieces);
}
}