From: Raphaël Gertz Date: Tue, 14 Oct 2025 15:02:54 +0000 (+0200) Subject: Version 0.5.7 X-Git-Tag: 0.5.7 X-Git-Url: https://git.rapsys.eu/packbundle/commitdiff_plain/HEAD?ds=sidebyside;hp=8b3e5065464a5734fb7259b71c2d64b7cc463699 Version 0.5.7 --- diff --git a/Controller.php b/Controller.php index 77a3e14..7728f2e 100644 --- a/Controller.php +++ b/Controller.php @@ -220,6 +220,57 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac return $response; } + /** + * Return download file + * + * @param Request $request The Request instance + * @param string $hash The hash + * @param string $path The image path + * @return Response The rendered image + */ + public function download(Request $request, string $hash, string $path/*, string $_format*/): Response { + //Without matching hash + if ($hash !== $this->slugger->hash($path)) { + //Throw new exception + throw new NotFoundHttpException('Invalid download hash'); + //Without valid format + #} elseif ($_format !== 'jpeg' && $_format !== 'png' && $_format !== 'webp') { + # //Throw new exception + # throw new NotFoundHttpException('Invalid download format'); + } + + //Unshort path + $path = $this->slugger->unshort($short = $path); + + //Without file + if (!is_file($path) || !($mtime = stat($path)['mtime'])) { + //Throw new exception + throw new NotFoundHttpException('Unable to get thumb file'); + } + + //Read thumb from cache + $response = new BinaryFileResponse($path); + + //Set file name + $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($path)); + + //Set etag + //TODO: set etag to file content md5 ? cache it ? + $response->setEtag(md5($hash)); + + //Set last modified + $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime))); + + //Set as public + $response->setPublic(); + + //Return 304 response if not modified + $response->isNotModified($request); + + //Return response + return $response; + } + /** * Return facebook image * @@ -257,6 +308,7 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'facebook-'.$hash.'.'.$_format); //Set etag + //TODO: set etag to file content md5 ? cache it ? $response->setEtag(md5($hash)); //Set last modified @@ -443,6 +495,7 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map)); //Set etag + //TODO: set etag to file content md5 ? cache it ? $response->setEtag(md5(serialize([$height, $width, $zoom, $latitude, $longitude]))); //Set last modified @@ -708,6 +761,7 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, basename($map)); //Set etag + //TODO: set etag to file content md5 ? cache it ? $response->setEtag(md5(serialize([$height, $width, $zoom, $coordinate]))); //Set last modified @@ -751,16 +805,16 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac $path = $this->slugger->unshort($short = $path); //Set thumb - $thumb = $this->config['cache'].'/'.$this->config['prefixes']['thumb'].$path.'.'.$_format; + $thumb = $this->config['cache'].'/'.$this->config['prefixes']['thumb'].$path.'.'.$width.'x'.$height.'.'.$_format; //Without file - if (!is_file($path) || !($updated = stat($path)['mtime'])) { + if (!is_file($path) || !($mtime = stat($path)['mtime'])) { //Throw new exception throw new NotFoundHttpException('Unable to get thumb file'); } //Without thumb up to date file - if (!is_file($thumb) || !($mtime = stat($thumb)['mtime']) || $mtime < $updated) { + if (!is_file($thumb) || !($updated = stat($thumb)['mtime']) || $updated < $mtime) { //Without existing thumb path if (!is_dir($dir = dirname($thumb))) { //Create filesystem object @@ -803,8 +857,8 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac throw new \Exception(sprintf('Unable to write image "%s"', $thumb)); } - //Set mtime - $mtime = stat($thumb)['mtime']; + //Set updated + $updated = stat($thumb)['mtime']; } //Read thumb from cache @@ -814,10 +868,11 @@ class Controller extends AbstractController implements ServiceSubscriberInterfac $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'thumb-'.$hash.'.'.$_format); //Set etag + //TODO: set etag to file content md5 ? cache it ? $response->setEtag(md5($hash)); //Set last modified - $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime))); + $response->setLastModified(\DateTime::createFromFormat('U', strval($updated))); //Set as public $response->setPublic(); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 2f29209..be4bb2a 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -110,6 +110,16 @@ class Configuration implements ConfigurationInterface { 'lemon' => dirname(__DIR__).'/public/woff2/lemon.woff2', 'notoemoji' => dirname(__DIR__).'/public/woff2/notoemoji.woff2' ], + 'formats' => [ + 'ico', + 'jpeg', + 'jpg', + 'png', + 'svg', + 'tiff', + 'webm', + 'webp', + ], 'map' => [ 'border' => '#00c3f9', 'fill' => '#cff', @@ -166,6 +176,7 @@ class Configuration implements ConfigurationInterface { 'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png' ], 'thumb' => [ + 'format' => 'jpeg', 'height' => 128, 'width' => 128 ], @@ -310,6 +321,11 @@ class Configuration implements ConfigurationInterface { ->defaultValue($defaults['fonts']) ->scalarPrototype()->end() ->end() + ->arrayNode('formats') + ->treatNullLike([]) + ->defaultValue($defaults['formats']) + ->scalarPrototype()->end() + ->end() ->arrayNode('map') ->addDefaultsIfNotSet() ->children() @@ -371,6 +387,7 @@ class Configuration implements ConfigurationInterface { ->addDefaultsIfNotSet() ->children() ->scalarNode('height')->cannotBeEmpty()->defaultValue($defaults['thumb']['height'])->end() + ->scalarNode('format')->cannotBeEmpty()->defaultValue($defaults['thumb']['format'])->end() ->scalarNode('width')->cannotBeEmpty()->defaultValue($defaults['thumb']['width'])->end() ->end() ->end() diff --git a/Form/CaptchaType.php b/Form/CaptchaType.php index f8cf3d0..9d9db16 100644 --- a/Form/CaptchaType.php +++ b/Form/CaptchaType.php @@ -37,8 +37,9 @@ class CaptchaType extends AbstractType { * @param ?ImageUtil $image The image instance * @param ?SluggerUtil $slugger The slugger instance * @param ?TranslatorInterface $translator The translator instance + * @param bool $enable Use captcha */ - public function __construct(protected ?ImageUtil $image = null, protected ?SluggerUtil $slugger = null, protected ?TranslatorInterface $translator = null) { + public function __construct(protected ?ImageUtil $image = null, protected ?SluggerUtil $slugger = null, protected ?TranslatorInterface $translator = null, protected bool $enable = false) { } /** @@ -56,7 +57,7 @@ class CaptchaType extends AbstractType { $builder->add('_captcha_token', HiddenType::class, ['data' => $captcha['token'], 'empty_data' => $captcha['token'], 'mapped' => false]); //Add captcha - $builder->add('captcha', IntegerType::class, ['label_attr' => ['class' => 'captcha'], 'label' => ''.htmlentities($captcha['equation']).'', 'label_html' => true, 'mapped' => false, 'translation_domain' => false]); + $builder->add('captcha', IntegerType::class, ['label_attr' => ['class' => 'captcha'], 'label' => ''.htmlentities($captcha['equation']).'', 'label_html' => true, 'mapped' => false, 'translation_domain' => false, 'required' => true]); //Add event listener on captcha $builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'validateCaptcha']); @@ -71,7 +72,7 @@ class CaptchaType extends AbstractType { parent::configureOptions($resolver); //Set defaults - $resolver->setDefaults(['captcha' => false, 'error_bubbling' => true, 'translation_domain' => RapsysPackBundle::getAlias()]); + $resolver->setDefaults(['captcha' => $this->enable, 'error_bubbling' => true, 'translation_domain' => RapsysPackBundle::getAlias()]); //Add extra captcha option $resolver->setAllowedTypes('captcha', 'boolean'); @@ -95,6 +96,8 @@ class CaptchaType extends AbstractType { //Without captcha if (empty($data['captcha'])) { //Add error on captcha + //XXX: we need to add error on form + //XXX: see https://github.com/symfony/symfony/issues/35831 $form->addError(new FormError($this->translator->trans('Captcha is empty'))); //Reset captcha token @@ -105,6 +108,8 @@ class CaptchaType extends AbstractType { //With invalid captcha } elseif ($this->slugger->hash($data['captcha']) !== $data['_captcha_token']) { //Add error on captcha + //XXX: we need to add error on form + //XXX: see https://github.com/symfony/symfony/issues/35831 $form->addError(new FormError($this->translator->trans('Captcha is invalid'))); //Reset captcha token diff --git a/Form/ContactType.php b/Form/ContactType.php new file mode 100644 index 0000000..b740921 --- /dev/null +++ b/Form/ContactType.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Rapsys\PackBundle\Form; + +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Validator\Constraints\NotBlank; + +/** + * {@inheritdoc} + */ +class ContactType extends CaptchaType { + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options): void { + //Add fields + $builder + ->add('name', TextType::class, ['attr' => ['placeholder' => 'Your name'], 'required' => true]) + ->add('subject', TextType::class, ['attr' => ['placeholder' => 'Subject'], 'required' => true]) + ->add('mail', EmailType::class, ['attr' => ['placeholder' => 'Your mail'], 'required' => true, 'invalid_message' => 'Your mail doesn\'t seems to be valid']) + ->add('message', TextareaType::class, ['attr' => ['placeholder' => 'Your message', 'cols' => 50, 'rows' => 15], 'required' => true]) + ->add('submit', SubmitType::class, ['label' => 'Send', 'attr' => ['class' => 'submit']]); + + //Call parent + parent::buildForm($builder, $options); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver): void { + //Call parent configure options + parent::configureOptions($resolver); + + //Set defaults + $resolver->setDefaults(['captcha' => true]); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return 'contact_form'; + } +} diff --git a/Package/PathPackage.php b/Package/PathPackage.php index c53b10f..d5b4b6f 100644 --- a/Package/PathPackage.php +++ b/Package/PathPackage.php @@ -61,7 +61,7 @@ class PathPackage extends Package { * * Returns an absolute or root-relative public path * - * Transform @BundleBundle to bundle and remove /Resources/public fragment from path + * Transform @BundleBundle to bundle and remove [/Resources|]/public fragment from path * This bundle name conversion and bundle prefix are the same as in asset:install command * * @link https://symfony.com/doc/current/bundles.html#overridding-the-bundle-directory-structure @@ -70,7 +70,7 @@ class PathPackage extends Package { */ public function getUrl(string $path): string { //Match url starting with a bundle name - if (preg_match('%^@([A-Z][a-zA-Z]*?)(?:Bundle/Resources/public)?/(.*)$%', $path, $matches)) { + if (preg_match('%^@([A-Z][a-zA-Z]*?)(?:Bundle/(?:Resources/)?public)?/(.*)$%', $path, $matches)) { //Handle empty or without replacement pattern basePath if (empty($this->basePath) || strpos($this->basePath, '%s') === false) { //Set path from hardcoded format diff --git a/Parser/TokenParser.php b/Parser/TokenParser.php index 7d73117..a1fe4c5 100644 --- a/Parser/TokenParser.php +++ b/Parser/TokenParser.php @@ -26,8 +26,10 @@ use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\RouterInterface; use Twig\Error\Error; -use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Node\Node; +use Twig\Node\Nodes; +use Twig\Node\EmptyNode; use Twig\Node\SetNode; use Twig\Node\TextNode; use Twig\Source; @@ -245,7 +247,7 @@ class TokenParser extends AbstractTokenParser { #throw new Error('Empty inputs token', $token->getLine(), $stream->getSourceContext()); //Send an empty node without inputs - return new Node(); + return new EmptyNode(); } //Check filters @@ -329,13 +331,13 @@ class TokenParser extends AbstractTokenParser { } //Set name in context key - $ref = new AssignNameExpression($this->token, $token->getLine()); + $ref = new AssignContextVariable($this->token, $token->getLine()); //Set output in context value $value = new TextNode($asset, $token->getLine()); //Send body with context set - return new Node([ + return new Nodes([ //This define name in twig template by prepending $context[''] = ''; new SetNode(true, $ref, $value, $token->getLine(), $this->getTag()), //The tag captured body diff --git a/README.md b/README.md index 81cd07a..bd7ebc5 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ class AppKernel extends Kernel ### Step 3: Configure the Bundle -Setup configuration file `config/packages/rapsys_pack.yaml` with the following -content available in `Resources/config/packages/rapsys_pack.yaml`: +Setup configuration file `config/packages/rapsyspack.yaml` with the following +content available in `vendor/rapsys/packbundle/config/packages/rapsyspack.yaml`: ```yaml #Services configuration @@ -110,23 +110,23 @@ services: #Replace assets.packages definition assets.packages: class: 'Symfony\Component\Asset\Packages' - arguments: [ '@rapsys_pack.path_package' ] + arguments: [ '@rapsyspack.path_package' ] #Replace assets.context definition assets.context: class: 'Rapsys\PackBundle\Context\RequestStackContext' arguments: [ '@request_stack', '%asset.request_context.base_path%', '%asset.request_context.secure%' ] #Register assets pack package - rapsys_pack.path_package: + rapsyspack.path_package: class: 'Rapsys\PackBundle\Package\PathPackage' arguments: [ '/', '@assets.empty_version_strategy', '@assets.context' ] public: true #Register twig pack extension - rapsys_pack.pack_extension: + rapsyspack.pack_extension: class: 'Rapsys\PackBundle\Extension\PackExtension' - arguments: [ '@service_container', '@rapsys_pack.intl_util', '@file_locator', '@rapsys_pack.path_package', '@rapsys_pack.slugger_util' ] + arguments: [ '@service_container', '@rapsyspack.intl_util', '@file_locator', '@rapsyspack.path_package', '@rapsyspack.slugger_util' ] tags: [ 'twig.extension' ] #Register intl util service - rapsys_pack.intl_util: + rapsyspack.intl_util: class: 'Rapsys\PackBundle\Util\IntlUtil' public: true #Register facebook event subscriber @@ -135,54 +135,54 @@ services: tags: [ 'kernel.event_subscriber' ] #Register intl util class alias Rapsys\PackBundle\Util\IntlUtil: - alias: 'rapsys_pack.intl_util' + alias: 'rapsyspack.intl_util' #Register facebook util service - rapsys_pack.facebook_util: + rapsyspack.facebook_util: class: 'Rapsys\PackBundle\Util\FacebookUtil' - arguments: [ '@router', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%' ] + arguments: [ '@router', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ] public: true #Register facebook util class alias Rapsys\PackBundle\Util\FacebookUtil: - alias: 'rapsys_pack.facebook_util' + alias: 'rapsyspack.facebook_util' #Register image util service - rapsys_pack.image_util: + rapsyspack.image_util: class: 'Rapsys\PackBundle\Util\ImageUtil' - arguments: [ '@router', '@rapsys_pack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%' ] + arguments: [ '@router', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ] public: true #Register image util class alias Rapsys\PackBundle\Util\ImageUtil: - alias: 'rapsys_pack.image_util' + alias: 'rapsyspack.image_util' #Register map util service - rapsys_pack.map_util: + rapsyspack.map_util: class: 'Rapsys\PackBundle\Util\MapUtil' - arguments: [ '@router', '@rapsys_pack.slugger_util' ] + arguments: [ '@router', '@rapsyspack.slugger_util' ] public: true #Register map util class alias Rapsys\PackBundle\Util\MapUtil: - alias: 'rapsys_pack.map_util' + alias: 'rapsyspack.map_util' #Register slugger util service - rapsys_pack.slugger_util: + rapsyspack.slugger_util: class: 'Rapsys\PackBundle\Util\SluggerUtil' arguments: [ '%kernel.secret%' ] public: true #Register slugger util class alias Rapsys\PackBundle\Util\SluggerUtil: - alias: 'rapsys_pack.slugger_util' + alias: 'rapsyspack.slugger_util' #Register image controller Rapsys\PackBundle\Controller\ImageController: - arguments: [ '@service_container', '@rapsys_pack.image_util', '@rapsys_pack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%' ] + arguments: [ '@service_container', '@rapsyspack.image_util', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ] tags: [ 'controller.service_arguments' ] #Register map controller Rapsys\PackBundle\Controller\MapController: - arguments: [ '@service_container', '@rapsys_pack.map_util', '@rapsys_pack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%' ] + arguments: [ '@service_container', '@rapsyspack.map_util', '@rapsyspack.slugger_util', '%kernel.project_dir%/var/cache', '%rapsyspack.path%' ] tags: [ 'controller.service_arguments' ] Rapsys\PackBundle\Form\CaptchaType: - arguments: [ '@rapsys_pack.image_util', '@rapsys_pack.slugger_util', '@translator' ] + arguments: [ '@rapsyspack.image_util', '@rapsyspack.slugger_util', '@translator' ] tags: [ 'form.type' ] ``` Setup configuration file `config/packages/myproject.yaml` with the following -content available in `Resources/config/packages/rapsys_pack.yaml`: +content available in `vendor/rapsys/packbundle/config/packages/rapsyspack.yaml`: ```yaml #Services configuration @@ -192,9 +192,9 @@ services: arguments: [ '@router', [ 'en', 'en_gb', 'en_us', 'fr', 'fr_fr' ] ] tags: [ 'kernel.event_subscriber' ] #Register facebook util service - rapsys_blog.facebook_util: + myproject.facebook_util: class: 'Rapsys\PackBundle\Util\FacebookUtil' - arguments: [ '@router', '%kernel.project_dir%/var/cache', '%rapsys_pack.path%', 'facebook', '%kernel.project_dir%/public/png/facebook.png' ] + arguments: [ '@router', '%kernel.project_dir%/var/cache', '%rapsyspack.path%', 'facebook', '%kernel.project_dir%/public/png/facebook.png' ] public: true ``` @@ -214,7 +214,7 @@ $ bin/console debug:config RapsysPackBundle ### Step 4: Use the twig extension in your Template -You can use a template like this to generate your first `rapsys_pack` enabled +You can use a template like this to generate your first `rapsyspack` enabled template: ```twig @@ -223,7 +223,7 @@ template: {% block title %}Welcome!{% endblock %} - {% stylesheet '//fonts.googleapis.com/css?family=Irish+Grover|La+Belle+Aurore' '@NamedBundle/Resources/public/css/{reset,screen}.css' '@Short/css/example.css' %} + {% stylesheet '//fonts.googleapis.com/css?family=Irish+Grover|La+Belle+Aurore' '@NamedBundle/public/css/{reset,screen}.css' '@Short/css/example.css' %} {% endstylesheet %} @@ -273,9 +273,9 @@ namespace Rapsys\PackBundle\Filter; use Twig\Error\Error; -//This class will be defined in the parameter rapsys_pack.filters.(css|img|js).[x].class string +//This class will be defined in the parameter rapsyspack.filters.(css|img|js).[x].class string class MyPackFilter implements FilterInterface { - //The constructor arguments ... will be replaced with values defined in the parameter rapsys_pack.filters.(css|img|js).[x].args array + //The constructor arguments ... will be replaced with values defined in the parameter rapsyspack.filters.(css|img|js).[x].args array public function __construct(string $fileName, int $line, string $bin = 'mypack', ...) { //Set fileName $this->fileName = $fileName; diff --git a/RapsysPackBundle.php b/RapsysPackBundle.php index 08360b0..7844d73 100644 --- a/RapsysPackBundle.php +++ b/RapsysPackBundle.php @@ -64,6 +64,6 @@ class RapsysPackBundle extends Bundle { */ public static function getVersion(): string { //Return version - return '0.5.4'; + return '0.5.7'; } } diff --git a/Util/FileUtil.php b/Util/FileUtil.php new file mode 100644 index 0000000..814892e --- /dev/null +++ b/Util/FileUtil.php @@ -0,0 +1,309 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Rapsys\PackBundle\Util; + +use Psr\Container\ContainerInterface; + +use Rapsys\PackBundle\RapsysPackBundle; +use Rapsys\PackBundle\Util\ImageUtil; +use Rapsys\PackBundle\Util\IntlUtil; +use Rapsys\PackBundle\Util\SluggerUtil; + +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Manages file informations + */ +class FileUtil { + /** + * Alias string + */ + protected string $alias; + + /** + * Config array + */ + protected array $config; + + /** + * Creates a new file util + * + * @param ContainerInterface $container The container instance + * @param ImageUtil $image The ImageUtil instance + * @param IntlUtil $intl The IntlUtil instance + * @param RouterInterface $router The router instance + * @param SluggerUtil $slugger The SluggerUtil instance + * @param TranslatorInterface $translator The translator instance + */ + public function __construct(protected ContainerInterface $container, protected ImageUtil $image, protected IntlUtil $intl, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator) { + //Retrieve config + $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias()); + } + + /** + * Get file infos + * + * @param string $path The base path + * @param string $realpath The real path + * @param string $name The route name + * @param array $parameters The route parameters + * @param bool $si Use si unit + */ + public function getFile(string $path, string $realpath, string $name, array $parameters = [], bool $si = true): array { + //Set file + $file = new \SplFileObject($realpath); + + //Set size + $size = $file->getSize(); + + //Set unit + $unit = $si ? 1000 : 1024; + + //Set index + $index = [ '', $si ? 'k' : 'K', 'M', 'G', 'T', 'P', 'E' ]; + + //Get exp + $exp = intval((log($size) / log($unit))); + + //Rebase number + $number = round($size / pow($unit, $exp), 2); + + //Set ext, height, image, mime, preview, thumb and width + $ext = $height = $image = $mime = $preview = $thumb = $width = false; + + //With supporter format extension + if (($pos = strrpos($realpath, '.')) && ($ext = substr($realpath, $pos + 1)) && in_array($ext, $this->config['formats'])) { + //With mime type + //XXX: getimagesize is too slow to run against all files + if (($image = getimagesize($realpath)) !== false) { + //Set ext + if (($ext = image_type_to_extension($image[2], false)) === false) { + //Throw error + throw new \Exception(sprintf('Unable to get "%s" file image extension', $realpath)); + } + + //Set height + $height = $image[1]; + + //Set mime + $mime = image_type_to_mime_type($image[2]); + + //Set width + $width = $image[0]; + + //Set preview + $preview = $this->image->getThumb($realpath, $height, $width, $ext); + + //Set preview + $thumb = $this->image->getThumb($realpath, $this->config['thumb']['height'], $this->config['thumb']['weight'], $ext); + } + } + + //Return file infos + return [ + 'created' => (new \DateTime())->setTimestamp($file->getCTime()), + 'height' => $height, + 'intlsize' => $intlsize = $this->intl->number($number), + 'intlunit' => $intlunit = $this->translator->trans($index[$exp].($si ? '' : 'i').'B'), + 'link' => $this->router->generate($name, ['path' => substr($realpath, strlen($path) + 1)] + $parameters), + 'download' => $this->router->generate('rapsyspack_download', ['path' => $short = $this->slugger->short($realpath), 'hash' => $this->slugger->hash($short), 'u' => $mtime = $file->getMTime()]), + 'mime' => $mime, + 'modified' => (new \DateTime())->setTimestamp($mtime), + 'name' => basename($realpath).' ('.$intlsize.' '.$intlunit.')', + 'preview' => $preview, + 'size' => $size, + 'thumb' => $thumb, + 'width' => $width, + //TODO: preview for images ? + //TODO: extra fields ? (preview, miniature, etc ?) + //TODO: mimetype decided extra fields ? (.pdf for doc, webm preview, img preview, etc ?) + //TODO: XXX: finish this !!! + ]; + } + + /** + * Get path infos + * + * @param string $path The base path + * @param string $realpath The real path + * @param string $name The route name + * @param array $parameters The route parameters + * @param bool $si Use si unit + */ + //Route object instead of shitty array ? + public function getInfos(string $path, string $realpath, string $slug, string $name, array $parameters = []) { + //Set result + $result = [ + 'breadcrumbs' => [/*$breadcrumb*/], + 'directories' => [], + 'file' => [], + 'files' => [], + ]; + + //Set base + $base = ''; + + //Iterate on breadcrumbs + foreach (explode('/', substr($realpath, strlen($path))) as $value) { + $result['breadcrumbs'][] = [ + 'name' => $value ? '/ '.$value : $slug, + 'link' => $this->router->generate($name, ['path' => ($base .= ($base == '' ? '' : '/').$value)] + $parameters) + ]; + } + + //With file + if (is_file($realpath)) { + //Set file + $result['file'] = $this->getFile($path, $realpath, $name, $parameters); + //With directory + //TODO: for pagination, files and directories are to be placed in a single array + //TODO: only get informations on files and directories inside the pagination window ! + } elseif (is_dir($realpath)) { + //Set dir + $dir = dir($realpath); + + //Iterate on directory + while (($item = $dir->read()) !== false) { + //Skip ., .., .git, .svn, .htpasswd, .htgroup and .*.un~ items + //TODO: set this regexp in config ? + if (preg_match('/^(\.|\.\.|\.git|\.svn|\.ht(access|passwd|group)|\..*\.un~)$/', $item)) { + //Skip it + continue; + } + + //Check item + if ( + //Without item realpath + !($itempath = realpath($realpath.'/'.$item)) || + //Item realpath not matching album path + //XXX: skip files outside of album/element path (symlink outside of tree) + //TODO: make it configurable ? by album/element/admin ? + $path !== substr($itempath, 0, strlen($path)) + ) { + //Skip it + continue; + } + + //With directory + if (is_dir($itempath)) { + //Append directory + //TODO: use this structure or revert to old $item.'/' => $link form + $result['directories'][] = [ + 'name' => $item.'/', + 'link' => $this->router->generate($name, ['path' => substr($itempath, strlen($path) + 1)] + $parameters) + //TODO: add stats here ? like number of files ? + ]; + //With file + } elseif (is_file($itempath)) { + //Set file + $result['files'][] = $this->getFile($path, $itempath, $name, $parameters); + //With unknown type + } else { + //Throw 404 + throw new \Exception(sprintf('Unknown file "%s" type', $itempath)); + } + } + //With unknown type + } else { + //Throw 404 + throw new \Exception(sprintf('Unknown file "%s" type', $realpath)); + } + + //Return result + return $result; + } + + /** + * Get file infos + * + * @param string $path The file path + * @param bool $si Use si units + * @return array The file infos + * / + public function infos(string $path, bool $si = true): array { + //Stat file + $stat = stat($path); + + //Set unit + $unit = $si ? 1000 : 1024; + + //Set index + $index = [ '', $si ? 'k' : 'K', 'M', 'G', 'T', 'P', 'E' ]; + + //Get exp + $exp = intval((log($stat['size']) / log($unit))); + + //Rebase number + $number = round($stat['size'] / pow($unit, $exp), 2); + + //Set file infos + $fileinfos = [ + 'intlsize' => $intlsize = $this->intl->number($number), + 'intlunit' => $intlunit = $this->translator->trans($index[$exp].($si ? '' : 'i').'B'), + 'name' => basename($path).' ('.$intlsize.' '.$intlunit.')', + 'size' => $stat['size'], + 'ctime' => $stat['ctime'], + 'mtime' => $stat['mtime'], + //TODO: preview for images ? + //TODO: extra fields ? (preview, miniature, etc ?) + //TODO: mimetype decided extra fields ? (.pdf for doc, webm preview, img preview, etc ?) + //TODO: XXX: finish this !!! + ]; + + //With mimetype + if (($mimetype = mime_content_type($path)) !== false) { + //Set file mimetype + $fileinfos['mimetype'] = $mimetype; + + //TODO: with image preview, movie webm preview, others a imagemagick file with format initials ? + + //With image + if ( + $mimetype == 'image/jpeg' || + $mimetype == 'image/png' || + $mimetype == 'image/bmp' || + $mimetype == 'image/tiff' || + $mimetype == 'image/svg+xml' + ) { + //Get image width and height + if (($fileinfos['image'] = getimagesize($path)) === false) { + //Throw error + throw new \Exception(sprintf('Unable to get "%s" file image size', $path)); + } + + //Set thumb + $fileinfos['thumb'] = $this->image->getThumb($path, 64, 64); + } + } + /* + 'src' => + //With location user source image + if (($isFile = is_file($source = $this->config['path'].'/location/'.$location['id'].'/'.$id.'.png')) && ($mtime = stat($source)['mtime'])) { + //Set location image + $this->context['locations'][$locationId]['image'] = $this->image->getThumb($location['miniature'], $mtime, $source); + + //With image mimetype + if (in_array($fileinfos['mimetype'], [ 'image/jpeg', 'image/png', 'image/webp' ])) { + header('Content-Type: text/plain'); + var_dump($fileinfos); + exit; + $file['thumb'] = $this->router->generate('rapsyspack_thumb', [ 'hash' => $this->slugger->hash(''), 'path' => $path, 'slug' => $slug ]); + #public function thumb(Request $request, string $hash, int $updated, string $path, int $width, int $height): Response { + } + * / + + //Return file infos + return $fileinfos; + }*/ +} diff --git a/Util/ImageUtil.php b/Util/ImageUtil.php index 44afe28..8a93c52 100644 --- a/Util/ImageUtil.php +++ b/Util/ImageUtil.php @@ -73,10 +73,10 @@ class ImageUtil { $a = $random % 10; //Set b - $b = $random / 10 % 10; + $b = intval($random / 10) % 10; //Set c - $c = $random / 100 % 10; + $c = intval($random / 100) % 10; //Set equation $equation = $a.' * '.$b.' + '.$c; @@ -597,9 +597,16 @@ class ImageUtil { * @param string $path The path * @param ?int $height The height * @param ?int $width The width + * @param ?string $format The format * @return array The thumb data */ - public function getThumb(string $path, ?int $height = null, ?int $width = null): array { + public function getThumb(string $path, ?int $height = null, ?int $width = null, ?string $format = null): array { + //Without format + if ($format === null || !in_array($format, $this->config['formats'])) { + //Set format from config + $format = $this->config['thumb']['format']; + } + //Without height if ($height === null) { //Set height from config @@ -624,9 +631,9 @@ class ImageUtil { //Return array return [ - 'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width]), - 'width' => $width, - 'height' => $height + 'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, '_format' => $format]), + 'height' => $height, + 'width' => $width ]; } diff --git a/Util/IntlUtil.php b/Util/IntlUtil.php index 80b9530..52bd546 100644 --- a/Util/IntlUtil.php +++ b/Util/IntlUtil.php @@ -123,7 +123,7 @@ class IntlUtil { //Set styles static $styles = [ 'decimal' => \NumberFormatter::DECIMAL, - 'currency' => \NumberFormatter::CURRENCY, + #'currency' => \NumberFormatter::CURRENCY, 'percent' => \NumberFormatter::PERCENT, 'scientific' => \NumberFormatter::SCIENTIFIC, 'spellout' => \NumberFormatter::SPELLOUT, @@ -150,7 +150,7 @@ class IntlUtil { 'int32' => \NumberFormatter::TYPE_INT32, 'int64' => \NumberFormatter::TYPE_INT64, 'double' => \NumberFormatter::TYPE_DOUBLE, - 'currency' => \NumberFormatter::TYPE_CURRENCY + #'currency' => \NumberFormatter::TYPE_CURRENCY ]; //Get formatter diff --git a/config/packages/rapsyspack.yaml b/config/packages/rapsyspack.yaml index 0efaa0b..d2bc1c5 100644 --- a/config/packages/rapsyspack.yaml +++ b/config/packages/rapsyspack.yaml @@ -24,7 +24,7 @@ services: # Register file util service rapsyspack.file_util: class: 'Rapsys\PackBundle\Util\FileUtil' - arguments: [ '@rapsyspack.image_util', '@rapsyspack.intl_util', '@translator' ] + arguments: [ '@service_container', '@rapsyspack.image_util', '@rapsyspack.intl_util', '@router', '@rapsyspack.slugger_util', '@translator' ] public: true # Register image util service rapsyspack.image_util: diff --git a/config/routes/rapsyspack.yaml b/config/routes/rapsyspack.yaml index f23660b..e471166 100644 --- a/config/routes/rapsyspack.yaml +++ b/config/routes/rapsyspack.yaml @@ -8,6 +8,11 @@ rapsyspack_css: path: '/bundles/rapsyspack/pack/css/{file<[a-zA-Z0-9]+>}.{!_format?css}' methods: GET +rapsyspack_download: + path: '/download/{hash<[a-zA-Z0-9=_-]+>}/{path<[a-zA-Z0-9=_-]+>}' + controller: Rapsys\PackBundle\Controller::download + methods: GET + rapsyspack_facebook: path: '/facebook/{hash<[a-zA-Z0-9=_-]+>}/{width<\d+>}/{height<\d+>}/{path<[a-zA-Z0-9=_-]+>}.{!_format<(jpeg|png|webp)>}' controller: Rapsys\PackBundle\Controller::facebook