Split Utilities apart into individual categories.

This commit is contained in:
Buster "Silver Eagle" Neece 2020-12-10 20:43:58 -06:00
parent a228e5def3
commit 4ca8ce0bc6
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
28 changed files with 506 additions and 428 deletions

View File

@ -117,34 +117,51 @@ class BackupCommand extends CommandAbstract
$io->section(__('Creating backup archive...'));
// Strip leading slashes from backup paths.
$files_to_backup = array_map(function ($val) {
if (0 === strpos($val, '/')) {
return substr($val, 1);
}
return $val;
}, $files_to_backup);
$files_to_backup = array_map(
function ($val) {
if (0 === strpos($val, '/')) {
return substr($val, 1);
}
return $val;
},
$files_to_backup
);
switch ($file_ext) {
case 'gz':
case 'tgz':
$this->passThruProcess($io, array_merge([
'tar',
'zcvf',
$tmpPath,
], $files_to_backup), '/');
$this->passThruProcess(
$io,
array_merge(
[
'tar',
'zcvf',
$tmpPath,
],
$files_to_backup
),
'/'
);
break;
case 'zip':
default:
$dont_compress = ['.tar.gz', '.zip', '.jpg', '.mp3', '.ogg', '.flac', '.aac', '.wav'];
$this->passThruProcess($io, array_merge([
'zip',
'-r',
'-n',
implode(':', $dont_compress),
$tmpPath,
], $files_to_backup), '/');
$this->passThruProcess(
$io,
array_merge(
[
'zip',
'-r',
'-n',
implode(':', $dont_compress),
$tmpPath,
],
$files_to_backup
),
'/'
);
break;
}
@ -158,16 +175,18 @@ class BackupCommand extends CommandAbstract
// Cleanup
$io->section(__('Cleaning up temporary files...'));
Utilities::rmdirRecursive($tmp_dir_mariadb);
Utilities\File::rmdirRecursive($tmp_dir_mariadb);
$io->newLine();
$end_time = microtime(true);
$time_diff = $end_time - $start_time;
$io->success([
__('Backup complete in %.2f seconds.', $time_diff),
]);
$io->success(
[
__('Backup complete in %.2f seconds.', $time_diff),
]
);
return 0;
}
}

View File

@ -43,19 +43,27 @@ class RestoreCommand extends CommandAbstract
switch ($file_ext) {
case 'gz':
case 'tgz':
$this->passThruProcess($io, [
'tar',
'zxvf',
$path,
], '/');
$this->passThruProcess(
$io,
[
'tar',
'zxvf',
$path,
],
'/'
);
break;
case 'zip':
default:
$this->passThruProcess($io, [
'unzip',
$path,
], '/');
$this->passThruProcess(
$io,
[
'unzip',
$path,
],
'/'
);
break;
}
@ -88,7 +96,7 @@ class RestoreCommand extends CommandAbstract
]
);
Utilities::rmdirRecursive($tmp_dir_mariadb);
Utilities\File::rmdirRecursive($tmp_dir_mariadb);
$io->newLine();
// Update from current version to latest.
@ -99,9 +107,11 @@ class RestoreCommand extends CommandAbstract
$end_time = microtime(true);
$time_diff = $end_time - $start_time;
$io->success([
'Restore complete in ' . round($time_diff, 3) . ' seconds.',
]);
$io->success(
[
'Restore complete in ' . round($time_diff, 3) . ' seconds.',
]
);
return 0;
}
}

View File

@ -24,7 +24,7 @@ class ListCommand extends CommandAbstract
$all_settings = $settingsTableRepo->readSettingsArray();
foreach ($all_settings as $setting_key => $setting_value) {
$value = print_r($setting_value, true);
$value = Utilities::truncateText($value, 600);
$value = Utilities\Strings::truncateText($value, 600);
$rows[] = [$setting_key, $value];
}

View File

@ -21,7 +21,7 @@ class ResetPasswordCommand extends CommandAbstract
->findOneBy(['email' => $email]);
if ($user instanceof Entity\User) {
$temp_pw = Utilities::generatePassword(15);
$temp_pw = Utilities\Strings::generatePassword(15);
$user->setNewPassword($temp_pw);
$user->setTwoFactorSecret(null);

View File

@ -6,7 +6,6 @@ use App\Config;
use App\Controller\AbstractLogViewerController;
use App\Entity;
use App\Exception\NotFoundException;
use App\File;
use App\Flysystem\Filesystem;
use App\Form\BackupSettingsForm;
use App\Form\Form;
@ -15,6 +14,7 @@ use App\Http\ServerRequest;
use App\Message\BackupMessage;
use App\Session\Flash;
use App\Sync\Task\RunBackupTask;
use App\Utilities\File;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Messenger\MessageBus;

View File

@ -5,7 +5,6 @@ namespace App\Controller\Admin;
use App\Console\Application;
use App\Controller\AbstractLogViewerController;
use App\Entity;
use App\File;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Message\RunSyncTaskMessage;
@ -14,6 +13,7 @@ use App\Radio\AutoDJ;
use App\Radio\Backend\Liquidsoap;
use App\Session\Flash;
use App\Sync\Runner;
use App\Utilities\File;
use Doctrine\ORM\EntityManagerInterface;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
@ -53,11 +53,15 @@ class DebugController extends AbstractLogViewerController
$queueTotals[$queue] = $queueManager->getQueueCount($queue);
}
return $request->getView()->renderToResponse($response, 'admin/debug/index', [
'sync_times' => $sync->getSyncTimes(),
'queue_totals' => $queueTotals,
'stations' => $stationRepo->fetchArray(),
]);
return $request->getView()->renderToResponse(
$response,
'admin/debug/index',
[
'sync_times' => $sync->getSyncTimes(),
'queue_totals' => $queueTotals,
'stations' => $stationRepo->fetchArray(),
]
);
}
public function syncAction(
@ -73,10 +77,14 @@ class DebugController extends AbstractLogViewerController
$this->messageBus->dispatch($message);
return $request->getView()->renderToResponse($response, 'admin/debug/sync', [
'title' => __('Run Synchronized Task'),
'outputLog' => basename($tempFile),
]);
return $request->getView()->renderToResponse(
$response,
'admin/debug/sync',
[
'title' => __('Run Synchronized Task'),
'outputLog' => basename($tempFile),
]
);
}
public function logAction(
@ -112,11 +120,15 @@ class DebugController extends AbstractLogViewerController
$this->logger->popHandler();
return $request->getView()->renderToResponse($response, 'system/log_view', [
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $this->testHandler->getRecords(),
]);
return $request->getView()->renderToResponse(
$response,
'system/log_view',
[
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $this->testHandler->getRecords(),
]
);
}
public function telnetAction(
@ -132,18 +144,25 @@ class DebugController extends AbstractLogViewerController
$command = $request->getParam('command');
$telnetResponse = $backend->command($station, $command);
$this->logger->debug('Telnet Command Response', [
'response' => $telnetResponse,
]);
$this->logger->debug(
'Telnet Command Response',
[
'response' => $telnetResponse,
]
);
}
$this->logger->popHandler();
return $request->getView()->renderToResponse($response, 'system/log_view', [
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $this->testHandler->getRecords(),
]);
return $request->getView()->renderToResponse(
$response,
'system/log_view',
[
'sidebar' => null,
'title' => __('Debug Output'),
'log_records' => $this->testHandler->getRecords(),
]
);
}
public function clearCacheAction(

View File

@ -54,7 +54,7 @@ abstract class AbstractApiCrudController
// Older jQuery Bootgrid requests should be "flattened".
if ($is_bootgrid && !$is_internal) {
return Utilities::flattenArray($return, '_');
return Utilities\Arrays::flattenArray($return, '_');
}
return $return;

View File

@ -44,7 +44,8 @@ class ListAction
: $currentDir . '/%';
$media_query = $em->createQueryBuilder()
->select('partial sm.{
->select(
'partial sm.{
id,
unique_id,
art_updated_at,
@ -55,7 +56,8 @@ class ListAction
title,
album,
genre
}')
}'
)
->addSelect('partial spm.{id}, partial sp.{id, name}')
->addSelect('partial smcf.{id, field_id, value}')
->from(Entity\StationMedia::class, 'sm')
@ -178,9 +180,12 @@ class ListAction
$files[] = $short_path;
}
} else {
$filesIterator = $fs->createIterator($currentDir, [
Options::OPTION_IS_RECURSIVE => false,
]);
$filesIterator = $fs->createIterator(
$currentDir,
[
Options::OPTION_IS_RECURSIVE => false,
]
);
$protectedPaths = [Entity\StationMedia::DIR_ALBUM_ART, Entity\StationMedia::DIR_WAVEFORMS];
@ -253,7 +258,7 @@ class ListAction
$sort_by[] = SORT_ASC;
}
$result = Utilities::arrayOrderBy($result, $sort_by);
$result = Utilities\Arrays::arrayOrderBy($result, $sort_by);
$num_results = count($result);
@ -276,11 +281,13 @@ class ListAction
$return_result = [];
}
return $response->withJson([
'current' => $page,
'rowCount' => $row_count,
'total' => $num_results,
'rows' => $return_result,
]);
return $response->withJson(
[
'current' => $page,
'rowCount' => $row_count,
'total' => $num_results,
'rows' => $return_result,
]
);
}
}

View File

@ -152,7 +152,7 @@ class HistoryController
$row->resolveUrls($router->getBaseUrl());
if ($is_bootgrid) {
return App\Utilities::flattenArray($row, '_');
return App\Utilities\Arrays::flattenArray($row, '_');
}
return $row;

View File

@ -144,7 +144,7 @@ class ListAction
$row->resolveUrls($router->getBaseUrl());
$list[] = Utilities::flattenArray($row, '_');
$list[] = Utilities\Arrays::flattenArray($row, '_');
}
}

View File

@ -108,24 +108,29 @@ class RequestsController
$is_bootgrid = $paginator->isFromBootgrid();
$router = $request->getRouter();
$paginator->setPostprocessor(function ($media_row) use ($station, $is_bootgrid, $router) {
/** @var Entity\StationMedia $media_row */
$row = new Entity\Api\StationRequest();
$row->song = ($this->songApiGenerator)($media_row);
$row->request_id = $media_row->getUniqueId();
$row->request_url = (string)$router->named('api:requests:submit', [
'station_id' => $station->getId(),
'media_id' => $media_row->getUniqueId(),
]);
$paginator->setPostprocessor(
function ($media_row) use ($station, $is_bootgrid, $router) {
/** @var Entity\StationMedia $media_row */
$row = new Entity\Api\StationRequest();
$row->song = ($this->songApiGenerator)($media_row);
$row->request_id = $media_row->getUniqueId();
$row->request_url = (string)$router->named(
'api:requests:submit',
[
'station_id' => $station->getId(),
'media_id' => $media_row->getUniqueId(),
]
);
$row->resolveUrls($router->getBaseUrl());
$row->resolveUrls($router->getBaseUrl());
if ($is_bootgrid) {
return Utilities::flattenArray($row, '_');
if ($is_bootgrid) {
return Utilities\Arrays::flattenArray($row, '_');
}
return $row;
}
return $row;
});
);
return $paginator->write($response);
}
@ -175,7 +180,13 @@ class RequestsController
$isAuthenticated = ($user instanceof Entity\User);
try {
$this->requestRepo->submit($station, $media_id, $isAuthenticated, $request->getIp());
$this->requestRepo->submit(
$station,
$media_id,
$isAuthenticated,
$request->getIp(),
$request->getHeaderLine('User-Agent')
);
return $response->withJson(new Entity\Api\Status(true, __('Request submitted successfully.')));
} catch (Exception $e) {

View File

@ -4,12 +4,12 @@ namespace App\Controller\Api\Stations\Streamers;
use App\Controller\Api\AbstractApiCrudController;
use App\Entity;
use App\File;
use App\Flysystem\FilesystemManager;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Paginator\QueryPaginator;
use App\Utilities;
use App\Utilities\File;
use Psr\Http\Message\ResponseInterface;
class BroadcastsController extends AbstractApiCrudController
@ -55,46 +55,48 @@ class BroadcastsController extends AbstractApiCrudController
$fs = $filesystem->getForStation($station);
$paginator->setPostprocessor(function ($row) use ($is_bootgrid, $router, $fs) {
/** @var Entity\StationStreamerBroadcast $row */
$return = $this->toArray($row);
$paginator->setPostprocessor(
function ($row) use ($is_bootgrid, $router, $fs) {
/** @var Entity\StationStreamerBroadcast $row */
$return = $this->toArray($row);
unset($return['recordingPath']);
unset($return['recordingPath']);
$recordingPath = $row->getRecordingPath();
$recordingUri = FilesystemManager::PREFIX_RECORDINGS . '://' . $recordingPath;
$recordingPath = $row->getRecordingPath();
$recordingUri = FilesystemManager::PREFIX_RECORDINGS . '://' . $recordingPath;
if ($fs->has($recordingUri)) {
$recordingMeta = $fs->getMetadata($recordingUri);
if ($fs->has($recordingUri)) {
$recordingMeta = $fs->getMetadata($recordingUri);
$return['recording'] = [
'path' => $recordingPath,
'size' => $recordingMeta['size'],
'links' => [
'download' => $router->fromHere(
'api:stations:streamer:broadcast:download',
['broadcast_id' => $row->getId()],
[],
true
),
'delete' => $router->fromHere(
'api:stations:streamer:broadcast:delete',
['broadcast_id' => $row->getId()],
[],
true
),
],
];
} else {
$return['recording'] = [];
$return['recording'] = [
'path' => $recordingPath,
'size' => $recordingMeta['size'],
'links' => [
'download' => $router->fromHere(
'api:stations:streamer:broadcast:download',
['broadcast_id' => $row->getId()],
[],
true
),
'delete' => $router->fromHere(
'api:stations:streamer:broadcast:delete',
['broadcast_id' => $row->getId()],
[],
true
),
],
];
} else {
$return['recording'] = [];
}
if ($is_bootgrid) {
return Utilities\Arrays::flattenArray($return, '_');
}
return $return;
}
if ($is_bootgrid) {
return Utilities::flattenArray($return, '_');
}
return $return;
});
);
return $paginator->write($response);
}
@ -177,20 +179,24 @@ class BroadcastsController extends AbstractApiCrudController
protected function getRecord(Entity\Station $station, int $id): ?Entity\StationStreamerBroadcast
{
/** @var Entity\StationStreamerBroadcast|null $broadcast */
$broadcast = $this->em->getRepository(Entity\StationStreamerBroadcast::class)->findOneBy([
'id' => $id,
'station' => $station,
]);
$broadcast = $this->em->getRepository(Entity\StationStreamerBroadcast::class)->findOneBy(
[
'id' => $id,
'station' => $station,
]
);
return $broadcast;
}
protected function getStreamer(Entity\Station $station, int $id): ?Entity\StationStreamer
{
/** @var Entity\StationStreamer|null $streamer */
$streamer = $this->em->getRepository(Entity\StationStreamer::class)->findOneBy([
'id' => $id,
'station' => $station,
]);
$streamer = $this->em->getRepository(Entity\StationStreamer::class)->findOneBy(
[
'id' => $id,
'station' => $station,
]
);
return $streamer;
}
}

View File

@ -2,7 +2,7 @@
namespace App\Entity\Api;
use App\File;
use App\Utilities\File;
use OpenApi\Annotations as OA;
use Symfony\Component\Validator\Constraints as Assert;

View File

@ -292,7 +292,7 @@ class StationRepository extends Repository
// Remove media folders.
$radio_dir = $station->getRadioBaseDir();
Utilities::rmdirRecursive($radio_dir);
Utilities\File::rmdirRecursive($radio_dir);
// Save changes and continue to the last setup step.
$this->em->flush();

View File

@ -7,7 +7,6 @@ use App\Entity;
use App\Environment;
use App\Exception;
use App\Radio\AutoDJ;
use App\Utilities;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Doctrine\ORM\EntityManagerInterface;
@ -34,10 +33,11 @@ class StationRequestRepository extends Repository
Entity\Station $station,
string $trackId,
bool $isAuthenticated,
string $ip
string $ip,
string $userAgent
): int {
// Forbid web crawlers from using this feature.
if (Utilities::isCrawler()) {
if ($this->isCrawler($userAgent)) {
throw new Exception(__('Search engine crawlers are not permitted to use this feature.'));
}
@ -84,9 +84,11 @@ class StationRequestRepository extends Repository
->getArrayResult();
if (count($recent_requests) > 0) {
throw new Exception(__(
'You have submitted a request too recently! Please wait before submitting another one.'
));
throw new Exception(
__(
'You have submitted a request too recently! Please wait before submitting another one.'
)
);
}
}
@ -98,6 +100,26 @@ class StationRequestRepository extends Repository
return $record->getId();
}
protected function isCrawler(string $userAgent): bool
{
$userAgent = strtolower($userAgent);
// phpcs:disable Generic.Files.LineLength
$crawlers_agents = strtolower(
'Bloglines subscriber|Dumbot|Sosoimagespider|QihooBot|FAST-WebCrawler|Superdownloads Spiderman|LinkWalker|msnbot|ASPSeek|WebAlta Crawler|Lycos|FeedFetcher-Google|Yahoo|YoudaoBot|AdsBot-Google|Googlebot|Scooter|Gigabot|Charlotte|eStyle|AcioRobot|GeonaBot|msnbot-media|Baidu|CocoCrawler|Google|Charlotte t|Yahoo! Slurp China|Sogou web spider|YodaoBot|MSRBOT|AbachoBOT|Sogou head spider|AltaVista|IDBot|Sosospider|Yahoo! Slurp|Java VM|DotBot|LiteFinder|Yeti|Rambler|Scrubby|Baiduspider|accoona'
);
// phpcs:enable
$crawlers = explode('|', $crawlers_agents);
foreach ($crawlers as $crawler) {
if (strpos($userAgent, trim($crawler)) !== false) {
return true;
}
}
return false;
}
/**
* Check if the song is already enqueued as a request.
*
@ -211,9 +233,11 @@ class StationRequestRepository extends Repository
$isDuplicate = (null === AutoDJ\Queue::getDistinctTrack($eligibleTracks, $recentTracks));
if ($isDuplicate) {
throw new Exception(__(
'This song or artist has been played too recently. Wait a while before requesting it again.'
));
throw new Exception(
__(
'This song or artist has been played too recently. Wait a while before requesting it again.'
)
);
}
return true;

View File

@ -6,9 +6,9 @@ namespace App\Entity;
use App\Annotations\AuditLog;
use App\Environment;
use App\File;
use App\Normalizer\Annotation\DeepNormalize;
use App\Radio\Adapters;
use App\Utilities\File;
use App\Validator\Constraints as AppAssert;
use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;

View File

@ -530,7 +530,7 @@ class StationRemote implements StationMountInterface
return $this->autodj_bitrate . 'kbps ' . strtoupper($this->autodj_format);
}
return Utilities::truncateUrl($this->url);
return Utilities\Strings::truncateUrl($this->url);
}
/**

View File

@ -48,7 +48,7 @@ class GetId3MetadataManager implements MetadataManagerInterface
if (!empty($tagContents[0]) && !$metaTags->containsKey($tagName)) {
$tagValue = $tagContents[0];
if (is_array($tagValue)) {
$flatValue = Utilities::flattenArray($tagValue);
$flatValue = Utilities\Arrays::flattenArray($tagValue);
$tagValue = implode(', ', $flatValue);
}

View File

@ -129,10 +129,10 @@ class Icecast extends AbstractFrontend
'burst-size' => 65535,
],
'authentication' => [
'source-password' => Utilities::generatePassword(),
'relay-password' => Utilities::generatePassword(),
'source-password' => Utilities\Strings::generatePassword(),
'relay-password' => Utilities\Strings::generatePassword(),
'admin-user' => 'admin',
'admin-password' => Utilities::generatePassword(),
'admin-password' => Utilities\Strings::generatePassword(),
],
'listen-socket' => [

View File

@ -212,8 +212,8 @@ class SHOUTcast extends AbstractFrontend
$config_path = $station->getRadioConfigDir();
return [
'password' => Utilities::generatePassword(),
'adminpassword' => Utilities::generatePassword(),
'password' => Utilities\Strings::generatePassword(),
'adminpassword' => Utilities\Strings::generatePassword(),
'logfile' => $config_path . '/sc_serv.log',
'w3clog' => $config_path . '/sc_w3c.log',
'banfile' => $this->writeIpBansFile($station),

View File

@ -26,9 +26,9 @@
namespace App\Service;
use App\Exception;
use App\File;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Utilities\File;
use Normalizer;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
@ -104,11 +104,13 @@ class Flow
}
if ($file->getSize() !== $currentChunkSize) {
throw new Exception(sprintf(
'File size of %s does not match expected size of %s',
$file->getSize(),
$currentChunkSize
));
throw new Exception(
sprintf(
'File size of %s does not match expected size of %s',
$file->getSize(),
$currentChunkSize
)
);
}
$file->moveTo($chunkPath);

View File

@ -1,259 +0,0 @@
<?php
/**
* Miscellaneous Utilities Class
**/
namespace App;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use function is_array;
class Utilities
{
/**
* Generate a randomized password of specified length.
*
* @param int $char_length
*/
public static function generatePassword($char_length = 8): string
{
// String of all possible characters. Avoids using certain letters and numbers that closely resemble others.
$numeric_chars = str_split('234679');
$uppercase_chars = str_split('ACDEFGHJKLMNPQRTWXYZ');
$lowercase_chars = str_split('acdefghjkmnpqrtwxyz');
$chars = [$numeric_chars, $uppercase_chars, $lowercase_chars];
$password = '';
for ($i = 1; $i <= $char_length; $i++) {
$char_array = $chars[$i % 3];
$password .= $char_array[random_int(0, count($char_array) - 1)];
}
return str_shuffle($password);
}
/**
* Truncate URL in text-presentable format (i.e. "http://www.example.com" becomes "example.com")
*
* @param string $url
* @param int $length
*/
public static function truncateUrl($url, $length = 40): string
{
$url = str_replace(['http://', 'https://', 'www.'], '', $url);
return self::truncateText(rtrim($url, '/'), $length);
}
/**
* Truncate text (adding "..." if needed)
*
* @param string $text
* @param int $limit
* @param string $pad
*/
public static function truncateText($text, $limit = 80, $pad = '...'): string
{
mb_internal_encoding('UTF-8');
if (mb_strlen($text) <= $limit) {
return $text;
}
$wrapped_text = self::mbWordwrap($text, $limit, '{N}', true);
$shortened_text = mb_substr($wrapped_text, 0, strpos($wrapped_text, '{N}'));
// Prevent the padding string from bumping up against punctuation.
$punctuation = ['.', ',', ';', '?', '!'];
if (in_array(mb_substr($shortened_text, -1), $punctuation, true)) {
$shortened_text = mb_substr($shortened_text, 0, -1);
}
return $shortened_text . $pad;
}
/**
* UTF-8 capable replacement for wordwrap function.
*
* @param string $str
* @param int $width
* @param string $break
* @param bool $cut
*/
public static function mbWordwrap($str, $width = 75, $break = "\n", $cut = false): string
{
$lines = explode($break, $str);
foreach ($lines as &$line) {
$line = rtrim($line);
if (mb_strlen($line) <= $width) {
continue;
}
$words = explode(' ', $line);
$line = '';
$actual = '';
foreach ($words as $word) {
if (mb_strlen($actual . $word) <= $width) {
$actual .= $word . ' ';
} else {
if ($actual != '') {
$line .= rtrim($actual) . $break;
}
$actual = $word;
if ($cut) {
while (mb_strlen($actual) > $width) {
$line .= mb_substr($actual, 0, $width) . $break;
$actual = mb_substr($actual, $width);
}
}
$actual .= ' ';
}
}
$line .= trim($actual);
}
return implode($break, $lines);
}
/**
* Sort a supplied array (the first argument) by one or more indices, specified in this format:
* arrayOrderBy($data, [ 'index_name', SORT_ASC, 'index2_name', SORT_DESC ])
*
* Internally uses array_multisort().
*
* @param array $data
* @param array $args
*
* @return mixed
*/
public static function arrayOrderBy($data, array $args = [])
{
if (empty($args)) {
return $data;
}
foreach ($args as $n => $field) {
if (is_string($field)) {
$tmp = [];
foreach ($data as $key => $row) {
$tmp[$key] = $row[$field];
}
$args[$n] = $tmp;
}
}
$args[] = &$data;
array_multisort(...$args);
return array_pop($args);
}
/**
* Detect if the User-Agent matches common crawler UAs.
* Not expected to be 100% accurate or trustworthy, just used to prevent
* common crawlers from accessing features like API endpoints.
*/
public static function isCrawler(): bool
{
$ua = strtolower($_SERVER['HTTP_USER_AGENT']);
// phpcs:disable Generic.Files.LineLength
$crawlers_agents = strtolower('Bloglines subscriber|Dumbot|Sosoimagespider|QihooBot|FAST-WebCrawler|Superdownloads Spiderman|LinkWalker|msnbot|ASPSeek|WebAlta Crawler|Lycos|FeedFetcher-Google|Yahoo|YoudaoBot|AdsBot-Google|Googlebot|Scooter|Gigabot|Charlotte|eStyle|AcioRobot|GeonaBot|msnbot-media|Baidu|CocoCrawler|Google|Charlotte t|Yahoo! Slurp China|Sogou web spider|YodaoBot|MSRBOT|AbachoBOT|Sogou head spider|AltaVista|IDBot|Sosospider|Yahoo! Slurp|Java VM|DotBot|LiteFinder|Yeti|Rambler|Scrubby|Baiduspider|accoona');
// phpcs:enable
$crawlers = explode('|', $crawlers_agents);
foreach ($crawlers as $crawler) {
if (strpos($ua, trim($crawler)) !== false) {
return true;
}
}
return false;
}
/**
* Recursively remove a directory and its contents.
*
* @param string $source
*/
public static function rmdirRecursive(string $source): bool
{
if (empty($source) || !file_exists($source)) {
return true;
}
if (is_file($source) || is_link($source)) {
return @unlink($source);
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
/** @var SplFileInfo $fileinfo */
if ('link' !== $fileinfo->getType() && $fileinfo->isDir()) {
if (!rmdir($fileinfo->getRealPath())) {
return false;
}
} elseif (!unlink($fileinfo->getRealPath())) {
return false;
}
}
return rmdir($source);
}
/**
* Flatten an array from format:
* [
* 'user' => [
* 'id' => 1,
* 'name' => 'test',
* ]
* ]
*
* to format:
* [
* 'user.id' => 1,
* 'user.name' => 'test',
* ]
*
* This function is used to create replacements for variables in strings.
*
* @param array|object $array
* @param string $separator
* @param null $prefix
*
* @return mixed[]
*/
public static function flattenArray($array, $separator = '.', $prefix = null): array
{
if (!is_array($array)) {
if (is_object($array)) {
// Quick and dirty conversion from object to array.
$array = json_decode(json_encode($array, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
} else {
return $array;
}
}
$return = [];
foreach ($array as $key => $value) {
$return_key = $prefix ? $prefix . $separator . $key : $key;
if (is_array($value)) {
$return = array_merge($return, self::flattenArray($value, $separator, $return_key));
} else {
$return[$return_key] = $value;
}
}
return $return;
}
}

87
src/Utilities/Arrays.php Normal file
View File

@ -0,0 +1,87 @@
<?php
namespace App\Utilities;
class Arrays
{
/**
* Sort a supplied array (the first argument) by one or more indices, specified in this format:
* arrayOrderBy($data, [ 'index_name', SORT_ASC, 'index2_name', SORT_DESC ])
*
* Internally uses array_multisort().
*
* @param array $data
* @param array $args
*
* @return mixed
*/
public static function arrayOrderBy($data, array $args = [])
{
if (empty($args)) {
return $data;
}
foreach ($args as $n => $field) {
if (is_string($field)) {
$tmp = [];
foreach ($data as $key => $row) {
$tmp[$key] = $row[$field];
}
$args[$n] = $tmp;
}
}
$args[] = &$data;
array_multisort(...$args);
return array_pop($args);
}
/**
* Flatten an array from format:
* [
* 'user' => [
* 'id' => 1,
* 'name' => 'test',
* ]
* ]
*
* to format:
* [
* 'user.id' => 1,
* 'user.name' => 'test',
* ]
*
* This function is used to create replacements for variables in strings.
*
* @param array|object $array
* @param string $separator
* @param null $prefix
*
* @return mixed[]
*/
public static function flattenArray($array, $separator = '.', $prefix = null): array
{
if (!is_array($array)) {
if (is_object($array)) {
// Quick and dirty conversion from object to array.
$array = json_decode(json_encode($array, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
} else {
return $array;
}
}
$return = [];
foreach ($array as $key => $value) {
$return_key = $prefix ? $prefix . $separator . $key : $key;
if (is_array($value)) {
$return = array_merge($return, self::flattenArray($value, $separator, $return_key));
} else {
$return[$return_key] = $value;
}
}
return $return;
}
}

View File

@ -1,6 +1,10 @@
<?php
namespace App;
namespace App\Utilities;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
/**
* Static class that facilitates the uploading, reading and deletion of files in a controlled directory.
@ -72,4 +76,38 @@ class File
return $fullPath;
}
/**
* Recursively remove a directory and its contents.
*
* @param string $source
*/
public static function rmdirRecursive(string $source): bool
{
if (empty($source) || !file_exists($source)) {
return true;
}
if (is_file($source) || is_link($source)) {
return @unlink($source);
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
/** @var SplFileInfo $fileinfo */
if ('link' !== $fileinfo->getType() && $fileinfo->isDir()) {
if (!rmdir($fileinfo->getRealPath())) {
return false;
}
} elseif (!unlink($fileinfo->getRealPath())) {
return false;
}
}
return rmdir($source);
}
}

111
src/Utilities/Strings.php Normal file
View File

@ -0,0 +1,111 @@
<?php
namespace App\Utilities;
class Strings
{
/**
* Truncate text (adding "..." if needed)
*
* @param string $text
* @param int $limit
* @param string $pad
*/
public static function truncateText($text, $limit = 80, $pad = '...'): string
{
mb_internal_encoding('UTF-8');
if (mb_strlen($text) <= $limit) {
return $text;
}
$wrapped_text = self::mbWordwrap($text, $limit, '{N}', true);
$shortened_text = mb_substr($wrapped_text, 0, strpos($wrapped_text, '{N}'));
// Prevent the padding string from bumping up against punctuation.
$punctuation = ['.', ',', ';', '?', '!'];
if (in_array(mb_substr($shortened_text, -1), $punctuation, true)) {
$shortened_text = mb_substr($shortened_text, 0, -1);
}
return $shortened_text . $pad;
}
/**
* Generate a randomized password of specified length.
*
* @param int $char_length
*/
public static function generatePassword($char_length = 8): string
{
// String of all possible characters. Avoids using certain letters and numbers that closely resemble others.
$numeric_chars = str_split('234679');
$uppercase_chars = str_split('ACDEFGHJKLMNPQRTWXYZ');
$lowercase_chars = str_split('acdefghjkmnpqrtwxyz');
$chars = [$numeric_chars, $uppercase_chars, $lowercase_chars];
$password = '';
for ($i = 1; $i <= $char_length; $i++) {
$char_array = $chars[$i % 3];
$password .= $char_array[random_int(0, count($char_array) - 1)];
}
return str_shuffle($password);
}
/**
* UTF-8 capable replacement for wordwrap function.
*
* @param string $str
* @param int $width
* @param string $break
* @param bool $cut
*/
public static function mbWordwrap($str, $width = 75, $break = "\n", $cut = false): string
{
$lines = explode($break, $str);
foreach ($lines as &$line) {
$line = rtrim($line);
if (mb_strlen($line) <= $width) {
continue;
}
$words = explode(' ', $line);
$line = '';
$actual = '';
foreach ($words as $word) {
if (mb_strlen($actual . $word) <= $width) {
$actual .= $word . ' ';
} else {
if ($actual != '') {
$line .= rtrim($actual) . $break;
}
$actual = $word;
if ($cut) {
while (mb_strlen($actual) > $width) {
$line .= mb_substr($actual, 0, $width) . $break;
$actual = mb_substr($actual, $width);
}
}
$actual .= ' ';
}
}
$line .= trim($actual);
}
return implode($break, $lines);
}
/**
* Truncate URL in text-presentable format (i.e. "http://www.example.com" becomes "example.com")
*
* @param string $url
* @param int $length
*/
public static function truncateUrl($url, $length = 40): string
{
$url = str_replace(['http://', 'https://', 'www.'], '', $url);
return self::truncateText(rtrim($url, '/'), $length);
}
}

View File

@ -100,14 +100,14 @@ class View extends Engine
$this->registerFunction(
'truncate',
function ($text, $length = 80) {
return Utilities::truncateText($text, $length);
return Utilities\Strings::truncateText($text, $length);
}
);
$this->registerFunction(
'truncateUrl',
function ($url) {
return Utilities::truncateUrl($url);
return Utilities\Strings::truncateUrl($url);
}
);
@ -121,7 +121,7 @@ class View extends Engine
$a[] = 'target="_blank"';
}
$a_body = ($truncate) ? Utilities::truncateUrl($url) : $url;
$a_body = ($truncate) ? Utilities\Strings::truncateUrl($url) : $url;
return '<a ' . implode(' ', $a) . '>' . $a_body . '</a>';
}
);

View File

@ -49,7 +49,7 @@ abstract class AbstractConnector implements ConnectorInterface
*/
public function replaceVariables(array $raw_vars, Entity\Api\NowPlaying $np): array
{
$values = Utilities::flattenArray($np, '.');
$values = Utilities\Arrays::flattenArray($np, '.');
$vars = [];
foreach ($raw_vars as $var_key => $var_value) {

View File

@ -1,4 +1,7 @@
<?php
use App\Utilities\Strings;
class UtilitiesTest extends \Codeception\Test\Unit
{
/**
@ -8,16 +11,16 @@ class UtilitiesTest extends \Codeception\Test\Unit
public function testUtilities()
{
$test_result = \App\Utilities::generatePassword(10);
$test_result = Strings::generatePassword(10);
$this->assertTrue(strlen($test_result) == 10);
$test_string = 'Lorem ipsum dolor sit amet lorem ipsum dolor sit amet lorem ipsum dolor sit amet';
$test_result = \App\Utilities::truncateText($test_string, 15);
$test_result = Strings::truncateText($test_string, 15);
$expected_result = 'Lorem ipsum...';
$this->assertEquals($test_result, $expected_result);
$test_url = 'https://www.twitter.com/';
$test_result = \App\Utilities::truncateUrl($test_url);
$test_result = Strings::truncateUrl($test_url);
$expected_result = 'twitter.com';
$this->assertEquals($test_result, $expected_result);
}