<?php

namespace Amp\Http\Server\StaticContent;

use Amp\ByteStream\InMemoryStream;
use Amp\ByteStream\InputStream;
use Amp\ByteStream\IteratorStream;
use Amp\CallableMaker;
use Amp\Coroutine;
use Amp\File;
use Amp\Http\Server\ErrorHandler;
use Amp\Http\Server\Request;
use Amp\Http\Server\RequestHandler;
use Amp\Http\Server\Response;
use Amp\Http\Server\Server;
use Amp\Http\Server\ServerObserver;
use Amp\Http\Status;
use Amp\Producer;
use Amp\Promise;
use Amp\Success;

final class DocumentRoot implements RequestHandler, ServerObserver
{
    use CallableMaker;

    /** @var string Default mime file path. */
    const DEFAULT_MIME_TYPE_FILE = __DIR__ . "/../resources/mime";

    /** @internal */
    const READ_CHUNK_SIZE = 8192;

    /** @internal */
    const PRECONDITION_NOT_MODIFIED = 1;

    /** @internal */
    const PRECONDITION_FAILED = 2;

    /** @internal */
    const PRECONDITION_IF_RANGE_OK = 3;

    /** @internal */
    const PRECONDITION_IF_RANGE_FAILED = 4;

    /** @internal */
    const PRECONDITION_OK = 5;

    /** @var bool */
    private $running = false;

    /** @var ErrorHandler */
    private $errorHandler;

    /** @var RequestHandler|null */
    private $fallback;

    private $root;
    private $debug;
    private $filesystem;
    private $multipartBoundary;
    private $cache = [];
    private $cacheTimeouts = [];
    private $now;

    private $mimeTypes = [];
    private $mimeFileTypes = [];
    private $indexes = ["index.html", "index.htm"];
    private $useEtagInode = true;
    private $expiresPeriod = 86400 * 7;
    private $defaultMimeType = "text/plain";
    private $defaultCharset = "utf-8";
    private $useAggressiveCacheHeaders = false;
    private $aggressiveCacheMultiplier = 0.9;
    private $cacheEntryTtl = 10;
    private $cacheEntryCount = 0;
    private $cacheEntryLimit = 2048;
    private $bufferedFileCount = 0;
    private $bufferedFileLimit = 50;
    private $bufferedFileSizeLimit = 524288;

    /**
     * @param string      $root Document root
     * @param File\Driver $filesystem Optional filesystem driver
     *
     * @throws \Error On invalid root path
     */
    public function __construct(string $root, File\Driver $filesystem = null)
    {
        $root = \str_replace("\\", "/", $root);
        if (!(\is_readable($root) && \is_dir($root))) {
            throw new \Error(
                "Document root requires a readable directory"
            );
        }

        if (\strncmp($root, "phar://", 7) !== 0) {
            $root = \realpath($root);
        }

        $this->root = \rtrim($root, "/");
        $this->filesystem = $filesystem ?: File\filesystem();
        $this->multipartBoundary = \strtr(\base64_encode(\random_bytes(16)), '+/', '-_');
    }

    /**
     * Removes expired file information from the cache and updates the current 'now' value.
     *
     * @param int $now
     */
    private function clearExpiredCacheEntries(int $now)
    {
        $this->now = $now;

        foreach ($this->cacheTimeouts as $path => $timeout) {
            if ($now <= $timeout) {
                break;
            }

            $fileInfo = $this->cache[$path];

            unset(
                $this->cache[$path],
                $this->cacheTimeouts[$path]
            );

            $this->bufferedFileCount -= isset($fileInfo->buffer);
            $this->cacheEntryCount--;
        }
    }

    /**
     * Specifies an instance of RequestHandler that is used if no file exists for the requested path.
     * If no fallback is given, a 404 response is returned from respond() when the file does not exist.
     *
     * @param RequestHandler $requestHandler
     *
     * @throws \Error If the server has started.
     */
    public function setFallback(RequestHandler $requestHandler)
    {
        if ($this->running) {
            throw new \Error("Cannot add fallback request handler after the server has started");
        }

        $this->fallback = $requestHandler;
    }

    /**
     * Respond to HTTP requests for filesystem resources.
     *
     * @param Request $request Request to handle.
     *
     * @return Promise
     */
    public function handleRequest(Request $request): Promise
    {
        $path = removeDotPathSegments($request->getUri()->getPath());

        return new Coroutine(
            ($fileInfo = $this->fetchCachedStat($path, $request))
                ? $this->respondFromFileInfo($fileInfo, $request)
                : $this->respondWithLookup($this->root . $path, $path, $request)
        );
    }

    private function fetchCachedStat(string $reqPath, Request $request)
    {
        // We specifically allow users to bypass cached representations by using their browser's "force refresh"
        // functionality. This lets us avoid the annoyance of stale file representations being served for a few seconds
        // after changes have been written to disk.
        if ($this->debug) {
            return null;
        }

        foreach ($request->getHeaderArray("Cache-Control") as $value) {
            if (strcasecmp($value, "no-cache") === 0) {
                return null;
            }
        }

        foreach ($request->getHeaderArray("Pragma") as $value) {
            if (strcasecmp($value, "no-cache") === 0) {
                return null;
            }
        }

        return $this->cache[$reqPath] ?? null;
    }

    private function shouldBufferContent(Internal\FileInformation $fileInfo): bool
    {
        if ($fileInfo->size > $this->bufferedFileSizeLimit) {
            return false;
        }

        if ($this->bufferedFileCount >= $this->bufferedFileLimit) {
            return false;
        }

        if ($this->cacheEntryCount >= $this->cacheEntryLimit) {
            return false;
        }

        return true;
    }

    private function respondWithLookup(string $realPath, string $reqPath, Request $request): \Generator
    {
        // We don't catch any potential exceptions from this yield because they represent
        // a legitimate error from some sort of disk failure. Just let them bubble up to
        // the server where they'll turn into a 500 response.
        $fileInfo = yield from $this->lookup($realPath);

        // Specifically use the request path to reference this file in the
        // cache because the file entry path may differ if it's reflecting
        // a directory index file.
        if ($this->cacheEntryCount < $this->cacheEntryLimit) {
            $this->cacheEntryCount++;
            $this->cache[$reqPath] = $fileInfo;
            $this->cacheTimeouts[$reqPath] = $this->now + $this->cacheEntryTtl;
        }

        return yield from $this->respondFromFileInfo($fileInfo, $request);
    }

    private function lookup(string $path): \Generator
    {
        $fileInfo = new Internal\FileInformation;

        $fileInfo->exists = false;
        $fileInfo->path = $path;

        File\StatCache::clear($path);
        if (!$stat = yield $this->filesystem->stat($path)) {
            return $fileInfo;
        }

        if (yield $this->filesystem->isdir($path)) {
            if ($indexPathArr = yield from $this->coalesceIndexPath($path)) {
                list($fileInfo->path, $stat) = $indexPathArr;
            } else {
                return $fileInfo;
            }
        }

        $fileInfo->exists = true;
        $fileInfo->size = (int) $stat["size"];
        $fileInfo->mtime = $stat["mtime"] ?? 0;
        $fileInfo->inode = $stat["ino"] ?? 0;
        $inode = $this->useEtagInode ? $fileInfo->inode : "";
        $fileInfo->etag = \md5("{$fileInfo->path}{$fileInfo->mtime}{$fileInfo->size}{$inode}");

        if ($this->shouldBufferContent($fileInfo)) {
            $fileInfo->buffer = yield $this->filesystem->get($fileInfo->path);
            $fileInfo->size = \strlen($fileInfo->buffer); // there's a slight chance for the size to change, be safe
            $this->bufferedFileCount++;
        }

        return $fileInfo;
    }

    private function coalesceIndexPath(string $dirPath): \Generator
    {
        $dirPath = \rtrim($dirPath, "/") . "/";
        foreach ($this->indexes as $indexFile) {
            $coalescedPath = $dirPath . $indexFile;
            if (yield $this->filesystem->isfile($coalescedPath)) {
                $stat = yield $this->filesystem->stat($coalescedPath);
                return [$coalescedPath, $stat];
            }
        }
    }

    private function respondFromFileInfo(Internal\FileInformation $fileInfo, Request $request): \Generator
    {
        if (!$fileInfo->exists) {
            if ($this->fallback !== null) {
                return $this->fallback->handleRequest($request);
            }

            return yield $this->errorHandler->handleError(Status::NOT_FOUND, null, $request);
        }

        switch ($request->getMethod()) {
            case "GET":
            case "HEAD":
                break;

            case "OPTIONS":
                return new Response(Status::NO_CONTENT, [
                    "Allow" => "GET, HEAD, OPTIONS",
                    "Accept-Ranges" => "bytes",
                ]);

            default:
                /** @var \Amp\Http\Server\Response $response */
                $response = yield $this->errorHandler->handleError(Status::METHOD_NOT_ALLOWED, null, $request);
                $response->setHeader("Allow", "GET, HEAD, OPTIONS");
                return $response;
        }

        $precondition = $this->checkPreconditions($request, $fileInfo->mtime, $fileInfo->etag);

        switch ($precondition) {
            case self::PRECONDITION_NOT_MODIFIED:
                $lastModifiedHttpDate = \gmdate('D, d M Y H:i:s', $fileInfo->mtime) . " GMT";
                $response = new Response(Status::NOT_MODIFIED, ["Last-Modified" => $lastModifiedHttpDate]);
                if ($fileInfo->etag) {
                    $response->setHeader("Etag", $fileInfo->etag);
                }
                return $response;

            case self::PRECONDITION_FAILED:
                return yield $this->errorHandler->handleError(Status::PRECONDITION_FAILED, null, $request);

            case self::PRECONDITION_IF_RANGE_FAILED:
                // Return this so the resulting generator will be auto-resolved
                return yield from $this->doNonRangeResponse($fileInfo);
        }

        if (!$rangeHeader = $request->getHeader("Range")) {
            // Return this so the resulting generator will be auto-resolved
            return yield from $this->doNonRangeResponse($fileInfo);
        }

        if ($range = $this->normalizeByteRanges($fileInfo->size, $rangeHeader)) {
            // Return this so the resulting generator will be auto-resolved
            return yield from $this->doRangeResponse($range, $fileInfo);
        }

        // If we're still here this is the only remaining response we can send
        /** @var \Amp\Http\Server\Response $response */
        $response = yield $this->errorHandler->handleError(Status::RANGE_NOT_SATISFIABLE, null, $request);
        $response->setHeader("Content-Range", "*/{$fileInfo->size}");
        return $response;
    }

    private function checkPreconditions(Request $request, int $mtime, string $etag): int
    {
        $ifMatch = $request->getHeader("If-Match");
        if ($ifMatch && \stripos($ifMatch, $etag) === false) {
            return self::PRECONDITION_FAILED;
        }

        $ifNoneMatch = $request->getHeader("If-None-Match");
        if ($ifNoneMatch && \stripos($ifNoneMatch, $etag) !== false) {
            return self::PRECONDITION_NOT_MODIFIED;
        }

        $ifModifiedSince = $request->getHeader("If-Modified-Since");
        $ifModifiedSince = $ifModifiedSince ? @\strtotime($ifModifiedSince) : 0;
        if ($ifModifiedSince && $mtime > $ifModifiedSince) {
            return self::PRECONDITION_NOT_MODIFIED;
        }

        $ifUnmodifiedSince = $request->getHeader("If-Unmodified-Since");
        $ifUnmodifiedSince = $ifUnmodifiedSince ? @\strtotime($ifUnmodifiedSince) : 0;
        if ($ifUnmodifiedSince && $mtime > $ifUnmodifiedSince) {
            return self::PRECONDITION_FAILED;
        }

        $ifRange = $request->getHeader("If-Range");
        if ($ifRange === null || !$request->getHeader("Range")) {
            return self::PRECONDITION_OK;
        }

        /**
         * This is a really stupid feature of HTTP but ...
         * If-Range headers may be either an HTTP timestamp or an Etag:.
         *
         *     If-Range = "If-Range" ":" ( entity-tag | HTTP-date )
         *
         * @link https://tools.ietf.org/html/rfc7233#section-3.2
         */
        if ($httpDate = @\strtotime($ifRange)) {
            return ($httpDate > $mtime) ? self::PRECONDITION_IF_RANGE_OK : self::PRECONDITION_IF_RANGE_FAILED;
        }

        // If the If-Range header was not an HTTP date we assume it's an Etag
        return ($etag === $ifRange) ? self::PRECONDITION_IF_RANGE_OK : self::PRECONDITION_IF_RANGE_FAILED;
    }

    private function doNonRangeResponse(Internal\FileInformation $fileInfo): \Generator
    {
        $headers = $this->makeCommonHeaders($fileInfo);
        $headers["Content-Type"] = $this->selectMimeTypeFromPath($fileInfo->path);

        if ($fileInfo->buffer !== null) {
            $headers["Content-Length"] = (string) $fileInfo->size;

            return new Response(Status::OK, $headers, new InMemoryStream($fileInfo->buffer));
        }

        // Don't use cached size if we don't have buffered file contents,
        // otherwise we get truncated files during development.
        $headers["Content-Length"] = (string) yield $this->filesystem->size($fileInfo->path);

        $handle = yield $this->filesystem->open($fileInfo->path, "r");

        $response = new Response(Status::OK, $headers, $handle);
        $response->onDispose([$handle, "close"]);
        return $response;
    }

    private function makeCommonHeaders($fileInfo): array
    {
        $headers = [
            "Accept-Ranges" => "bytes",
            "Cache-Control" => "public",
            "Etag" => $fileInfo->etag,
            "Last-Modified" => \gmdate('D, d M Y H:i:s', $fileInfo->mtime) . " GMT",
        ];

        $canCache = ($this->expiresPeriod > 0);
        if ($canCache && $this->useAggressiveCacheHeaders) {
            $postCheck = (int) ($this->expiresPeriod * $this->aggressiveCacheMultiplier);
            $preCheck = $this->expiresPeriod - $postCheck;
            $value = ", post-check={$postCheck}, pre-check={$preCheck}, max-age={$this->expiresPeriod}";
            $headers["Cache-Control"] .= $value;
        } else if ($canCache) {
            $expiry = $this->now + $this->expiresPeriod;
            $headers["Cache-Control"] .= ", max-age={$this->expiresPeriod}";
            $headers["Expires"] = \gmdate('D, d M Y H:i:s', $expiry) . " GMT";
        } else {
            $headers["Expires"] = "0";
        }

        return $headers;
    }

    private function selectMimeTypeFromPath(string $path): string
    {
        $ext = \pathinfo($path, PATHINFO_EXTENSION);
        if (empty($ext)) {
            $mimeType = $this->defaultMimeType;
        } else {
            $ext = \strtolower($ext);
            if (isset($this->mimeTypes[$ext])) {
                $mimeType = $this->mimeTypes[$ext];
            } elseif (isset($this->mimeFileTypes[$ext])) {
                $mimeType = $this->mimeFileTypes[$ext];
            } else {
                $mimeType = $this->defaultMimeType;
            }
        }

        if (\stripos($mimeType, "text/") === 0 && \stripos($mimeType, "charset=") === false) {
            $mimeType .= "; charset={$this->defaultCharset}";
        }

        return $mimeType;
    }

    /**
     * @link https://tools.ietf.org/html/rfc7233#section-2.1
     *
     * @param int    $size Total size of the file in bytes.
     * @param string $rawRanges Ranges as provided by the client.
     *
     * @return Internal\ByteRange|null
     */
    private function normalizeByteRanges(int $size, string $rawRanges)
    {
        $rawRanges = \str_ireplace([' ', 'bytes='], '', $rawRanges);

        $ranges = [];

        foreach (\explode(',', $rawRanges) as $range) {
            // If a range is missing the dash separator it's malformed; pull out here.
            if (false === strpos($range, '-')) {
                return null;
            }

            list($startPos, $endPos) = explode('-', rtrim($range), 2);

            if ($startPos === '' && $endPos === '') {
                return null;
            }

            if ($startPos === '' && $endPos !== '') {
                // The -1 is necessary and not a hack because byte ranges are inclusive and start
                // at 0. DO NOT REMOVE THE -1.
                $startPos = $size - $endPos - 1;
                $endPos = $size - 1;
            } elseif ($endPos === '' && $startPos !== '') {
                $startPos = (int) $startPos;
                // The -1 is necessary and not a hack because byte ranges are inclusive and start
                // at 0. DO NOT REMOVE THE -1.
                $endPos = $size - 1;
            } else {
                $startPos = (int) $startPos;
                $endPos = (int) $endPos;
            }

            // If the requested range(s) can't be satisfied we're finished
            if ($startPos >= $size || $endPos < $startPos || $endPos < 0) {
                return null;
            }

            $ranges[] = [$startPos, $endPos];
        }

        $range = new Internal\ByteRange;
        $range->boundary = $this->multipartBoundary;
        $range->ranges = $ranges;

        return $range;
    }

    private function doRangeResponse(Internal\ByteRange $range, Internal\FileInformation $fileInfo): \Generator
    {
        $headers = $this->makeCommonHeaders($fileInfo);
        $range->contentType = $mime = $this->selectMimeTypeFromPath($fileInfo->path);

        if (isset($range->ranges[1])) {
            $headers["Content-Type"] = "multipart/byteranges; boundary={$range->boundary}";
        } else {
            list($startPos, $endPos) = $range->ranges[0];
            $headers["Content-Length"] = (string) ($endPos - $startPos + 1);
            $headers["Content-Range"] = "bytes {$startPos}-{$endPos}/{$fileInfo->size}";
            $headers["Content-Type"] = $mime;
        }

        $handle = yield $this->filesystem->open($fileInfo->path, "r");

        if (empty($range->ranges[1])) {
            list($startPos, $endPos) = $range->ranges[0];
            $stream = $this->sendSingleRange($handle, $startPos, $endPos);
        } else {
            $stream = $this->sendMultiRange($handle, $fileInfo, $range);
        }

        $response = new Response(Status::PARTIAL_CONTENT, $headers, $stream);
        $response->onDispose([$handle, "close"]);
        return $response;
    }

    private function sendSingleRange(File\Handle $handle, int $startPos, int $endPos): InputStream
    {
        $iterator = new Producer(function (callable $emit) use ($handle, $startPos, $endPos) {
            return $this->readRangeFromHandle($handle, $emit, $startPos, $endPos);
        });

        return new IteratorStream($iterator);
    }

    private function sendMultiRange($handle, Internal\FileInformation $fileInfo, Internal\ByteRange $range): InputStream
    {
        $iterator = new Producer(function (callable $emit) use ($handle, $range, $fileInfo) {
            foreach ($range->ranges as list($startPos, $endPos)) {
                yield $emit(sprintf(
                    "--%s\r\nContent-Type: %s\r\nContent-Range: bytes %d-%d/%d\r\n\r\n",
                    $range->boundary,
                    $range->contentType,
                    $startPos,
                    $endPos,
                    $fileInfo->size
                ));
                yield from $this->readRangeFromHandle($handle, $emit, $startPos, $endPos);
                yield $emit("\r\n");
            }
            yield $emit("--{$range->boundary}--");
        });

        return new IteratorStream($iterator);
    }

    private function readRangeFromHandle(File\Handle $handle, callable $emit, int $startPos, int $endPos): \Generator
    {
        $bytesRemaining = $endPos - $startPos + 1;
        yield $handle->seek($startPos);

        while ($bytesRemaining) {
            $toBuffer = $bytesRemaining > self::READ_CHUNK_SIZE ? self::READ_CHUNK_SIZE : $bytesRemaining;
            $chunk = yield $handle->read($toBuffer);
            $bytesRemaining -= \strlen($chunk);
            yield $emit($chunk);
        }
    }

    public function setIndexes(array $indexes)
    {
        foreach ($indexes as $index) {
            if (!\is_string($index)) {
                throw new \TypeError(sprintf(
                    "Array of string index filenames required: %s provided",
                    \gettype($index)
                ));
            }
        }

        $this->indexes = \array_filter($indexes);
    }

    public function setUseEtagInode(bool $useInode)
    {
        $this->useEtagInode = $useInode;
    }

    public function setExpiresPeriod(int $seconds)
    {
        $this->expiresPeriod = ($seconds < 0) ? 0 : $seconds;
    }

    public function loadMimeFileTypes(string $mimeFile)
    {
        $mimeFile = str_replace('\\', '/', $mimeFile);
        $mimeStr = @file_get_contents($mimeFile);
        if ($mimeStr === false) {
            throw new \Exception(
                "Failed loading mime associations from file {$mimeFile}"
            );
        }

        /** @var array[] $matches */
        if (!preg_match_all('#\s*([a-z0-9]+)\s+([a-z0-9\-]+/[a-z0-9\-]+(?:\+[a-z0-9\-]+)?)#i', $mimeStr, $matches)) {
            throw new \Exception(
                "No mime associations found in file: {$mimeFile}"
            );
        }

        $mimeTypes = [];

        foreach ($matches[1] as $key => $value) {
            $mimeTypes[strtolower($value)] = $matches[2][$key];
        }

        $this->mimeFileTypes = $mimeTypes;
    }

    public function setMimeTypes(array $mimeTypes)
    {
        foreach ($mimeTypes as $ext => $type) {
            $ext = strtolower(ltrim($ext, '.'));
            $this->mimeTypes[$ext] = $type;
        }
    }

    public function setDefaultMimeType(string $mimeType)
    {
        if (empty($mimeType)) {
            throw new \Error(
                'Default mime type expects a non-empty string'
            );
        }

        $this->defaultMimeType = $mimeType;
    }

    public function setDefaultTextCharset(string $charset)
    {
        if (empty($charset)) {
            throw new \Error(
                'Default charset expects a non-empty string'
            );
        }

        $this->defaultCharset = $charset;
    }

    public function setUseAggressiveCacheHeaders(bool $bool)
    {
        $this->useAggressiveCacheHeaders = $bool;
    }

    public function setAggressiveCacheMultiplier(float $multiplier)
    {
        if ($multiplier > 0.00 && $multiplier < 1.0) {
            $this->aggressiveCacheMultiplier = $multiplier;
        } else {
            throw new \Error(
                "Aggressive cache multiplier expects a float < 1; {$multiplier} specified"
            );
        }
    }

    public function setCacheEntryTtl(int $seconds)
    {
        if ($seconds < 1) {
            $seconds = 10;
        }
        $this->cacheEntryTtl = $seconds;
    }

    public function setCacheEntryLimit(int $count)
    {
        if ($count < 1) {
            $count = 0;
        }
        $this->cacheEntryLimit = $count;
    }

    public function setBufferedFileLimit(int $count)
    {
        if ($count < 1) {
            $count = 0;
        }
        $this->bufferedFileLimit = $count;
    }

    public function setBufferedFileSizeLimit(int $bytes)
    {
        if ($bytes < 1) {
            $bytes = 524288;
        }
        $this->bufferedFileSizeLimit = $bytes;
    }

    public function onStart(Server $server): Promise
    {
        $this->running = true;

        if (empty($this->mimeFileTypes)) {
            $this->loadMimeFileTypes(self::DEFAULT_MIME_TYPE_FILE);
        }

        $this->errorHandler = $server->getErrorHandler();

        $this->debug = $server->getOptions()->isInDebugMode();

        $server->getTimeReference()->onTimeUpdate($this->callableFromInstanceMethod("clearExpiredCacheEntries"));

        if ($this->fallback instanceof ServerObserver) {
            return $this->fallback->onStart($server);
        }

        return new Success;
    }

    public function onStop(Server $server): Promise
    {
        $this->cache = [];
        $this->cacheTimeouts = [];
        $this->cacheEntryCount = 0;
        $this->bufferedFileCount = 0;
        $this->running = false;

        if ($this->fallback instanceof ServerObserver) {
            return $this->fallback->onStop($server);
        }

        return new Success;
    }
}


<?php

namespace Jb\Bundle\PhumborBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;

/**
 * PhumborBundle configuration structure.
 *
 * @author Jonathan Bouzekri <jonathan.bouzekri@gmail.com>
 */
class Configuration implements ConfigurationInterface
{
    /**
     * Generates the configuration tree builder.
     *
     * @return TreeBuilder The tree builder
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('jb_phumbor');

        $rootNode
            ->children()
                ->arrayNode('server')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->scalarNode('url')
                            ->defaultValue('http://localhost:8888')
                        ->end()
                        ->scalarNode('secret')
                            ->defaultValue('')
                        ->end()
                    ->end()
                ->end()
            ->end();

        $this->addTransformationSection($rootNode);

        return $treeBuilder;
    }

    /**
     * Add the transformation section configuration structure
     *
     * @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $rootNode
     */
    private function addTransformationSection(ArrayNodeDefinition $rootNode)
    {
        // Resize validate function. Resize value are int >= 0 or the string "orig"
        $validateResize = function ($v) {
            return $v !== "orig" && !is_int($v);
        };

        $rootNode
            ->children()
                ->arrayNode('transformations')
                    ->prototype('array')
                        // Filter key is a prototype array. Remove it if empty array
                        ->validate()
                            ->always(function ($val) {
                                if (empty($val['filters'])) {
                                    unset($val['filters']);
                                }

                                return $val;
                            })
                        ->end()
                        ->children()
                            ->scalarNode('trim')
                            ->end()
                            ->arrayNode('crop')
                                ->children()
                                    ->integerNode('top_left_x')->isRequired()->end()
                                    ->integerNode('top_left_y')->isRequired()->end()
                                    ->integerNode('bottom_right_x')->isRequired()->end()
                                    ->integerNode('bottom_right_y')->isRequired()->end()
                                ->end()
                            ->end()
                            ->arrayNode('fit_in')
                                ->children()
                                    ->integerNode('width')->isRequired()->end()
                                    ->integerNode('height')->isRequired()->end()
                                ->end()
                            ->end()
                            ->arrayNode('full_fit_in')
                                ->children()
                                    ->integerNode('width')->isRequired()->end()
                                    ->integerNode('height')->isRequired()->end()
                                ->end()
                            ->end()
                            ->arrayNode('resize')
                                ->children()
                                    ->scalarNode('width')
                                        ->isRequired()
                                        ->validate()
                                            ->ifTrue($validateResize)
                                            ->thenInvalid(
                                                'Invalid transformation.resize.width value %s. '
                                                . 'It must be an integer or the string "orig"'
                                            )
                                        ->end()
                                    ->end()
                                    ->scalarNode('height')
                                        ->isRequired()
                                        ->validate()
                                            ->ifTrue($validateResize)
                                            ->thenInvalid(
                                                'Invalid transformation.resize.height value %s. '
                                                . 'It must be an integer or the string "orig"'
                                            )
                                        ->end()
                                    ->end()
                                ->end()
                            ->end()
                            ->scalarNode('halign')
                                ->validate()
                                    ->ifNotInArray(array('left', 'center', 'right'))
                                    ->thenInvalid(
                                        'Invalid transformation.halign value %s. '
                                        . 'It must be one of the following : left, center, right.'
                                    )
                                ->end()
                            ->end()
                            ->scalarNode('valign')
                                ->validate()
                                    ->ifNotInArray(array('top', 'middle', 'bottom'))
                                    ->thenInvalid(
                                        'Invalid transformation.valign value %s. '
                                        . 'It must be one of the following : top, middle, bottom.'
                                    )
                                ->end()
                            ->end()
                            ->booleanNode('smart_crop')->end()
                            ->booleanNode('metadata_only')->end()
                            ->arrayNode('filters')
                                ->prototype('array')
                                    ->children()
                                        ->scalarNode('name')->isRequired()->end()
                                        ->arrayNode('arguments')
                                            ->prototype('scalar')->end()
                                        ->end()
                                    ->end()
                                ->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end();
    }
}


<?php

namespace Jb\Bundle\PhumborBundle\Twig;

use Jb\Bundle\PhumborBundle\Transformer\BaseTransformer;

/**
 * Description of PhumborExtension
 *
 * @author jobou
 */
class PhumborExtension extends \Twig_Extension
{
    /**
     * @var \Jb\Bundle\PhumborBundle\Transformer\BaseTransformer
     */
    protected $transformer;

    /**
     * Constructor
     *
     * @param \Jb\Bundle\PhumborBundle\Transformer\BaseTransformer $transformer
     */
    public function __construct(BaseTransformer $transformer)
    {
        $this->transformer = $transformer;
    }

    /**
     * {@inheritDoc}
     */
    public function getFilters()
    {
        return array(
            new \Twig_SimpleFilter('thumbor', array($this, 'transform')),
        );
    }

    /**
     * {@inheritDoc}
     */
    public function getFunctions()
    {
        return array(
            new \Twig_SimpleFunction('thumbor', array($this, 'transform')),
        );
    }

    /**
     * Twig thumbor filter
     *
     * @param string $orig
     * @param string $transformation
     * @param array $overrides
     *
     * @return string
     */
    public function transform($orig, $transformation = null, $overrides = array())
    {
        return $this->transformer->transform($orig, $transformation, $overrides);
    }

    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return 'phumbor_extension';
    }
}


<?php

namespace Jb\Bundle\PhumborBundle\Transformer;

use Thumbor\Url\BuilderFactory;
use Thumbor\Url\Builder;

/**
 * Description of BaseTransformer
 *
 * @author jobou
 */
class BaseTransformer
{
    /**
     * Method name for each operation
     *
     * @var array
     */
    protected static $filterMethod = array(
        'trim' => 'trim',
        'crop' => 'crop',
        'fit_in' => 'fitIn',
        'full_fit_in' => 'fullFitIn',
        'resize' => 'resize',
        'halign' => 'halign',
        'valign' => 'valign',
        'smart_crop' => 'smartCrop',
        'metadata_only' => 'metadataOnly',
        'filters' => 'filters'
    );

    /**
     * Phumbor Builder Factory
     *
     * @var \Thumbor\Url\BuilderFactory
     */
    protected $factory;

    /**
     * The configured transformations
     *
     * @var array
     */
    protected $transformations;

    /**
     * Constructor
     *
     * @param \Thumbor\Url\BuilderFactory $factory
     * @param array $transformations
     */
    public function __construct(BuilderFactory $factory, array $transformations)
    {
        $this->factory = $factory;
        $this->transformations = $transformations;
    }

    /**
     * Transform an url or path for thumbor
     *
     * @param string $orig the original url or path
     * @param string $transformation the name of the transformation to apply to the original image
     * @param array $overrides an array of additionnal filters to override the ones from the transformation
     *
     * @return \Thumbor\Url\Builder
     *
     * @throws \Jb\Bundle\PhumborBundle\Transformer\Exception\UnknownTransformationException
     */
    public function transform($orig, $transformation = null, $overrides = array())
    {
        $url = $this->factory->url($orig);
        if (is_null($transformation) && count($overrides) == 0) {
            return $url;
        }

        // Check if a transformation is given without overrides
        if (!isset($this->transformations[$transformation]) && count($overrides) == 0) {
            throw new Exception\UnknownTransformationException(
                "Unknown transformation $transformation. Use on of "
                . "the following ".implode(', ', array_keys($this->transformations))
            );
        }

        // Override transformation configuration with custom values
        $configuration = array();
        if (isset($this->transformations[$transformation])) {
            $configuration = $this->transformations[$transformation];
        }
        $configuration = array_merge($configuration, $overrides);

        // Build url from transformation configuration
        foreach ($configuration as $filter => $arguments) {
            $method = self::$filterMethod[$filter];
            $this->$method($url, $arguments);
        }

        return $url;
    }

    /**
     * Apply trim filter
     *
     * @param \Thumbor\Url\Builder $url
     * @param bool|string $args
     *
     * @return void
     */
    protected function trim(Builder $url, $args)
    {
        $args = (is_string($args)) ? $args : null;
        $url->trim($args);
    }

    /**
     * Apply resize filter
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function resize(Builder $url, $args)
    {
        $url->resize($args['width'], $args['height']);
    }

    /**
     * Apply crop filter
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function crop(Builder $url, $args)
    {
        $url->crop($args['top_left_x'], $args['top_left_y'], $args['bottom_right_x'], $args['bottom_right_y']);
    }

    /**
     * Apply fitIn filter
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function fitIn(Builder $url, $args)
    {
        $url->fitIn($args['width'], $args['height']);
    }

    /**
     * Apply fullFitIn filter
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function fullFitIn(Builder $url, $args)
    {
        $url->fullFitIn($args['width'], $args['height']);
    }

    /**
     * Apply halign filter
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function halign(Builder $url, $args)
    {
        $url->halign($args);
    }

    /**
     * Apply valign filter
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function valign(Builder $url, $args)
    {
        $url->valign($args);
    }

    /**
     * Apply smartCrop filter
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function smartCrop(Builder $url, $args)
    {
        $url->smartCrop($args);
    }

    /**
     * Request metadata endpoint
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function metadataOnly(Builder $url, $args)
    {
        $url->metadataOnly($args);
    }

    /**
     * Apply filters
     *
     * @param \Thumbor\Url\Builder $url
     * @param array $args
     *
     * @return void
     */
    protected function filters(Builder $url, $args)
    {
        foreach ($args as $arg) {
            $arguments = (is_array($arg['arguments'])) ? $arg['arguments'] : array($arg['arguments']);
            array_unshift($arguments, $arg['name']);
            call_user_func_array(array($url, 'addFilter'), $arguments);
        }
    }

    /**
    * Setter allowing for factory override
    *
    * @param \Thumbor\Url\BuilderFactory $factory
    */
    public function setFactory(BuilderFactory $factory)
    {
        $this->factory = $factory;
    }
}


<?php

/*
 * This file is part of the SamlBundle.
 *
 * (c) Paulo Dias <dias.paulo@gmail.com>
 *
 */
namespace PDias\SamlBundle\Security\EntryPoint;

use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface,
    Symfony\Component\HttpFoundation\ParameterBag,
    Symfony\Component\HttpFoundation\Request,
    Symfony\Component\HttpFoundation\Response,
    Symfony\Component\Security\Core\Exception\AuthenticationException,
    Symfony\Component\Security\Http\HttpUtils,
    PDias\SamlBundle\Saml\SamlAuth;

/**
 * @author: Paulo Dias <dias.paulo@gmail.com>
 */
class SamlAuthenticationEntryPoint implements AuthenticationEntryPointInterface
{
    protected $options;
    protected $samlAuth;
    protected $httpUtils;

    /**
     * Constructor
     */
    public function __construct(SamlAuth $samlAuth, HttpUtils $httpUtils, array $options = array())
    {
        $this->samlAuth = $samlAuth;
        $this->httpUtils = $httpUtils;
        $this->options = new ParameterBag($options);
    }

    /**
     * {@inheritdoc}
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        $this->samlAuth->setLoginReturn($this->httpUtils->generateUri($request, $this->options->get('login_return')));
        $this->samlAuth->requireAuth();
    }
}


<?php
/*
 * This file is part of the SamlBundle.
 *
 * (c) Paulo Dias <dias.paulo@gmail.com>
 *
 */
namespace PDias\SamlBundle\DependencyInjection\Security\Factory;

use Symfony\Component\DependencyInjection\ContainerBuilder,
    Symfony\Component\DependencyInjection\Reference,
    Symfony\Component\DependencyInjection\DefinitionDecorator,
    Symfony\Component\Config\Definition\Builder\NodeDefinition,
    Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory;

/**
 * @author: Paulo Dias <dias.paulo@gmail.com>
 */
class SamlFactory extends AbstractFactory
{

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'saml';
    }

    public function addConfiguration(NodeDefinition $node)
    {
        parent::addConfiguration($node);

        $builder = $node->children();
        $builder
            ->booleanNode('direct_entry')->defaultTrue()->end()
        ;
    }

    protected function getListenerId()
    {
        return 'saml.security.authentication.listener';
    }

    protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
    {
        $authProviderId = 'security.authentication.provider.saml.'.$id;
        $container
            ->setDefinition($authProviderId, new DefinitionDecorator('saml.security.authentication.provider'))
            ->replaceArgument(0, new Reference($userProviderId));

        return $authProviderId;
    }

    protected function createListener($container, $id, $config, $userProvider)
    {
        $listenerId = $this->getListenerId();
        $listener = new DefinitionDecorator($listenerId);
        $listener->replaceArgument(8, $config);
        $listenerId .= '.'.$id;
        $container->setDefinition($listenerId, $listener);

        //Logout listener/handler
        $this->createLogoutHandler($container, $id, $config);

        return $listenerId;
    }

    /*protected function createEntryPoint($container, $id, $config, $defaultEntryPointId)
    {
        $entryPointId = 'saml.security.authentication.entry_point.'.$id;
        $container
            ->setDefinition($entryPointId, new DefinitionDecorator('saml.security.authentication.entry_point'))
            ->replaceArgument(2, $config)
        ;

        // set options to container for use by other classes
        $container->setParameter('saml.options.'.$id, $config);

        return $entryPointId;
    }*/

    protected function createLogoutHandler($container, $id, $config)
    {
        //Logout listener
        if($container->hasDefinition('security.logout_listener.'.$id)) {
            $logoutListener = $container->getDefinition('security.logout_listener.'.$id);
            $samlListenerId = 'security.logout.handler.saml';

            //Add logout handler
            $container
                ->setDefinition($samlListenerId, new DefinitionDecorator('saml.security.http.logout'))
                ->replaceArgument(2, array_intersect_key($config, $this->options));
            $logoutListener->addMethodCall('addHandler', array(new Reference($samlListenerId)));
        }
    }
}


<?php
/*
 * This file is part of the SamlBundle.
 *
 * (c) Paulo Dias <dias.paulo@gmail.com>
 *
 */
namespace PDias\SamlBundle\Security\User;

use Symfony\Component\Security\Core\Role\RoleInterface,
    Symfony\Component\Security\Core\Role\Role,
    Symfony\Component\Security\Core\User\UserInterface,
    Symfony\Component\Security\Core\User\EquatableInterface;

/**
 * @author: Paulo Dias <dias.paulo@gmail.com>
 */
class SamlUser implements UserInterface, EquatableInterface
{
    //private $id = null;
    private $username;
    private $roles;
    private $attributes;

    public function __construct($username, array $roles = array(), array $attributes = array())
    {
        $this->username = $username;

        $this->attributes = array();
        foreach($attributes as $key => $attribute){
            if(count($attribute)==1) {
                $this->setAttribute($key, $attribute[0]);
            } else {
                $this->setAttribute($key, $attribute);
            }
        }

        $this->roles = array();
        foreach ($roles as $role) {
            if (is_string($role)) {
                $role = new Role($role);
            } elseif (!$role instanceof RoleInterface) {
                throw new \InvalidArgumentException(sprintf('Roles must be an array of strings, or RoleInterface instances, but got %s.', gettype($role)));
            }

            $this->roles[] = $role;
        }
    }

    public function getPassword(){}

    public function getSalt(){}

    public function eraseCredentials(){}

    /**
     * Returns user name.
     *
     * @return string User name
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * Returns user roles.
     *
     * @return array User roles
     */
    public function getRoles()
    {
        return $this->roles;
    }

    /**
     * Add user role.
     *
     */
    public function addRole($role)
    {
        if (is_string($role)) {
            $role = new Role($role);
        } elseif (!$role instanceof RoleInterface) {
            throw new \InvalidArgumentException(sprintf('Role must be a string or RoleInterface instance, but got %s.', gettype($role)));
        }

        if(!\in_array($role, $this->roles)) {
            $this->roles[] = $role;
        }
    }

    /**
     * @param string $role
     *
     * @return boolean
     */
    public function hasRole($role)
    {
        foreach ($this->roles as $k => $r) {
            $roles[$k] = $r instanceof RoleInterface ? $r->getRole() : (string) $r;
        }
        $roles = array_flip(array_flip($roles));

        return in_array($role, $roles);
    }

    /**
     * Returns the token attributes.
     *
     * @return array The token attributes
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * Sets the token attributes.
     *
     * @param array $attributes The token attributes
     */
    public function setAttributes(array $attributes)
    {
        $this->attributes = $attributes;
        return $this;
    }

    /**
     * Returns true if the attribute exists.
     *
     * @param  string  $name  The attribute name
     * @return Boolean true if the attribute exists, false otherwise
     */
    public function hasAttribute($name)
    {
        return array_key_exists($name, $this->attributes);
    }

    /**
     * Returns a attribute value.
     *
     * @param string $name The attribute name
     * @return mixed The attribute value
     * @throws \InvalidArgumentException When attribute doesn't exist for this token
     */
    public function getAttribute($name)
    {
        if (!array_key_exists($name, $this->attributes)) {
            throw new \InvalidArgumentException(sprintf('This token has no "%s" attribute.', $name));
        }

        return $this->attributes[$name];
    }

    /**
     * Sets a attribute.
     *
     * @param string $name  The attribute name
     * @param mixed  $value The attribute value
     */
    public function setAttribute($name, $value)
    {
        $this->attributes[$name] = $value;
        return $this;
    }

    /**
     * Verify if user is equalt to $user.
     *
     * @param UserInterface $user  User interface
     */
    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof SamlUser) {
            return false;
        }

        if ($this->getSalt() !== $user->getSalt()) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
    }

    //For compatiblity with other user bundles

    /**
     * Sets id.
     *
     * @param integer $id  The id
     */
    /*public function setId($id)
    {
        $this->id = $id;
        return $this;
    }*/

    /**
     * Returns the id.
     */
    /*public function getId()
    {
        return $this->id;
    }*/
}


<?php
/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with TYPO3 source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace Causal\ImageAutoresize\Task;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;

/**
 * Additional BE fields for batch resize task.
 *
 * Creates a text area where directories may be excluded from batch processing.
 *
 * @category    Task
 * @package     TYPO3
 * @subpackage  tx_imageautoresize
 * @author      Xavier Perseguers <xavier@causal.ch>
 * @license     http://www.gnu.org/copyleft/gpl.html
 */
class BatchResizeAdditionalFieldProvider implements \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface
{

    /**
     * Default directories (to be processed)
     *
     * @var string
     */
    protected $defaultDirectories = '';

    /**
     * Default exclude directories
     *
     * @var string
     */
    protected $defaultExcludeDirectories = '';

    /**
     * Adds text area input fields for choosing directories to be processed and excluding directories.
     *
     * @param array $taskInfo Reference to the array containing the info used in the add/edit form
     * @param object $task When editing, reference to the current task object. Null when adding.
     * @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $parentObject Reference to the calling object (Scheduler's BE module)
     * @return array Array containing all the information pertaining to the additional fields
     */
    public function getAdditionalFields(array &$taskInfo, $task, \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $parentObject)
    {
        $editCommand = version_compare(TYPO3_branch, '9.5', '>=')
            ? $parentObject->getCurrentAction() === Action::EDIT
            : $parentObject->CMD === 'edit';

        // Initialize selected fields
        if (!isset($taskInfo['scheduler_batchResize_directories'])) {
            $taskInfo['scheduler_batchResize_directories'] = $this->defaultDirectories;
            if ($editCommand) {
                /** @var $task \Causal\ImageAutoresize\Task\BatchResizeTask */
                $taskInfo['scheduler_batchResize_directories'] = $task->directories;
            }
        }
        if (!isset($taskInfo['scheduler_batchResize_excludeDirectories'])) {
            $taskInfo['scheduler_batchResize_excludeDirectories'] = $this->defaultExcludeDirectories;
            if ($editCommand) {
                /** @var $task \Causal\ImageAutoresize\Task\BatchResizeTask */
                $taskInfo['scheduler_batchResize_excludeDirectories'] = $task->excludeDirectories;
            }
        }

        // Create HTML form fields
        $additionalFields = [];

        // Directories to be processed
        $fieldName = 'tx_scheduler[scheduler_batchResize_directories]';
        $fieldId = 'scheduler_batchResize_directories';
        $fieldValue = trim($taskInfo['scheduler_batchResize_directories']);
        $fieldHtml = '<textarea class="form-control" rows="4" name="' . $fieldName . '" id="' . $fieldId . '">' . htmlspecialchars($fieldValue) . '</textarea>';
        $additionalFields[$fieldId] = [
            'code' => $fieldHtml,
            'label' => 'LLL:EXT:image_autoresize/Resources/Private/Language/locallang_mod.xlf:label.batchResize.directories',
        ];

        // Directories to be excluded
        $fieldName = 'tx_scheduler[scheduler_batchResize_excludeDirectories]';
        $fieldId = 'scheduler_batchResize_excludeDirectories';
        $fieldValue = trim($taskInfo['scheduler_batchResize_excludeDirectories']);
        $fieldHtml = '<textarea class="form-control" rows="4" name="' . $fieldName . '" id="' . $fieldId . '">' . htmlspecialchars($fieldValue) . '</textarea>';
        $additionalFields[$fieldId] = [
            'code' => $fieldHtml,
            'label' => 'LLL:EXT:image_autoresize/Resources/Private/Language/locallang_mod.xlf:label.batchResize.excludeDirectories',
        ];

        return $additionalFields;
    }

    /**
     * Checks if the given value is a string
     *
     * @param array $submittedData Reference to the array containing the data submitted by the user
     * @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $parentObject Reference to the calling object (Scheduler's BE module)
     * @return boolean true if validation was ok (or selected class is not relevant), false otherwise
     */
    public function validateAdditionalFields(array &$submittedData, \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $parentObject)
    {
        $result = true;

        // Check for valid directories
        $directories = GeneralUtility::trimExplode(LF, $submittedData['scheduler_batchResize_directories'], true);
        foreach ($directories as $directory) {
            $absoluteDirectory = GeneralUtility::getFileAbsFileName($directory);
            if (!@is_dir($absoluteDirectory)) {
                $result = false;
                $parentObject->addMessage(
                    sprintf(
                        $GLOBALS['LANG']->sL('LLL:EXT:image_autoresize/Resources/Private/Language/locallang_mod.xlf:msg.invalidDirectories'),
                        $directory
                    ),
                    \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR
                );
            }
        }

        $directories = GeneralUtility::trimExplode(LF, $submittedData['scheduler_batchResize_excludeDirectories'], true);
        foreach ($directories as $directory) {
            $absoluteDirectory = GeneralUtility::getFileAbsFileName($directory);
            if (!@is_dir($absoluteDirectory)) {
                $result = false;
                $parentObject->addMessage(
                    sprintf(
                        $GLOBALS['LANG']->sL('LLL:EXT:image_autoresize/Resources/Private/Language/locallang_mod.xlf:msg.invalidExcludeDirectories'),
                        $directory
                    ),
                    \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR);
            }
        }

        return $result;
    }

    /**
     * Saves given string value in task object
     *
     * @param array $submittedData Contains data submitted by the user
     * @param \Causal\ImageAutoresize\Task\BatchResizeTask $task Reference to the current task object
     * @return void
     */
    public function saveAdditionalFields(array $submittedData, \TYPO3\CMS\Scheduler\Task\AbstractTask $task)
    {
        /** @var $task \Causal\ImageAutoresize\Task\BatchResizeTask */
        $task->directories = trim($submittedData['scheduler_batchResize_directories']);
        $task->excludeDirectories = trim($submittedData['scheduler_batchResize_excludeDirectories']);
    }

}


<?php
/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with TYPO3 source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace Causal\ImageAutoresize\Task;

use Causal\ImageAutoresize\Controller\ConfigurationController;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Causal\ImageAutoresize\Service\ImageResizer;

/**
 * Scheduler task to batch resize pictures.
 *
 * @category    Task
 * @package     TYPO3
 * @subpackage  tx_imageautoresize
 * @author      Xavier Perseguers <xavier@causal.ch>
 * @license     http://www.gnu.org/copyleft/gpl.html
 */
class BatchResizeTask extends \TYPO3\CMS\Scheduler\Task\AbstractTask
{

    /**
     * @var string
     * @additionalField
     */
    public $directories = '';

    /**
     * @var string
     * @additionalField
     */
    public $excludeDirectories = '';

    /**
     * @var ImageResizer
     */
    protected $imageResizer;

    /**
     * Batch resize pictures, called by scheduler.
     *
     * @return boolean true if task run was successful
     */
    public function execute()
    {
        $configuration = ConfigurationController::readConfiguration();

        $this->imageResizer = GeneralUtility::makeInstance(\Causal\ImageAutoresize\Service\ImageResizer::class);
        $this->imageResizer->initializeRulesets($configuration);

        if (empty($this->directories)) {
            // Process watched directories
            $directories = $this->imageResizer->getAllDirectories();
        } else {
            $directories = GeneralUtility::trimExplode(LF, $this->directories, true);
        }
        $processedDirectories = [];

        // Expand watched directories if they contain wildcard characters
        $expandedDirectories = [];
        foreach ($directories as $directory) {
            if (($pos = strpos($directory, '/*')) !== false) {
                $pattern = $this->imageResizer->getDirectoryPattern($directory);
                $basePath = substr($directory, 0, $pos + 1);

                $objects = new \RecursiveIteratorIterator(
                    new \RecursiveDirectoryIterator(PATH_site . $basePath),
                    \RecursiveIteratorIterator::SELF_FIRST
                );
                foreach ($objects as $name => $object) {
                    $relativePath = substr($name, strlen(PATH_site));
                    if (substr($relativePath, -2) === DIRECTORY_SEPARATOR . '.') {
                        if (preg_match($pattern, $relativePath)) {
                            $expandedDirectories[] = substr($relativePath, 0, -1);
                        }
                    }
                }
            } else {
                $expandedDirectories[] = $directory;
            }
        }
        $directories = $expandedDirectories;

        $success = true;
        foreach ($directories as $directory) {
            $skip = false;
            foreach ($processedDirectories as $processedDirectory) {
                if (GeneralUtility::isFirstPartOfStr($directory, $processedDirectory)) {
                    continue 2;
                }
            }

            // Execute bach resize
            $success |= $this->batchResizePictures($directory);
            $processedDirectories[] = $directory;
        }

        return $success;
    }

    /**
     * Batch resizes pictures in a given parent directory (including all subdirectories
     * recursively).
     *
     * @param string $directory
     * @return boolean true if run was successful
     * @throws \RuntimeException
     */
    protected function batchResizePictures($directory)
    {
        $directory = GeneralUtility::getFileAbsFileName($directory);
        // Check if given directory exists
        if (!@is_dir($directory)) {
            throw new \RuntimeException('Given directory "' . $directory . '" does not exist', 1384102984);
        }

        $allFileTypes = $this->imageResizer->getAllFileTypes();

        // We do not want to pass any backend user, even if manually running the task as administrator from
        // the Backend as images may be resized based on usergroup rule sets and this should only happen when
        // actually resizing the image while uploading, not during a batch processing (it's simply "too late").
        $backendUser = null;

        if ($GLOBALS['BE_USER']->isAdmin()) {
            // As the scheduler user should never be an administrator, if current user is an administrator
            // the task is most probably run manually from the Scheduler module, so just show notifications
            $callbackNotification = [$this, 'notify'];
        } else {
            $callbackNotification = [$this, 'syslog'];
        }

        $excludeDirectories = GeneralUtility::trimExplode(LF, $this->excludeDirectories, true);

        $directoryContent = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory));
        foreach ($directoryContent as $fileName => $file) {
            $filePath = $file->getPath();
            $name = substr($fileName, strlen($filePath) + 1);

            // Skip files in recycler directory or whose type should not be processed
            $skip = $name{0} === '.' || substr($filePath, -10) === '_recycler_';
            // Skip exclude directories
            foreach ($excludeDirectories as $excludeDirectory) {
                $excludeDirectory = GeneralUtility::getFileAbsFileName($excludeDirectory);
                if (GeneralUtility::isFirstPartOfStr($filePath, $excludeDirectory) ||
                    rtrim($excludeDirectory, '/') === $filePath
                ) {
                    $skip = true;
                    continue;
                }
            }

            if (!$skip) {
                if (($dotPosition = strrpos($name, '.')) !== false) {
                    $fileExtension = strtolower(substr($name, $dotPosition + 1));
                    if (in_array($fileExtension, $allFileTypes)) {
                        $this->imageResizer->processFile(
                            $fileName,
                            '',    // target file name
                            '',    // target directory
                            null,
                            $backendUser,
                            $callbackNotification
                        );
                    }
                }
            }
        }

        return true;
    }

    /**
     * Notifies the user using a Flash message.
     *
     * @param string $message The message
     * @param integer $severity Optional severity, must be either of \TYPO3\CMS\Core\Messaging\FlashMessage::INFO,
     *                          \TYPO3\CMS\Core\Messaging\FlashMessage::OK, \TYPO3\CMS\Core\Messaging\FlashMessage::WARNING
     *                          or \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR.
     *                          Default is \TYPO3\CMS\Core\Messaging\FlashMessage::OK.
     * @return void
     * @internal This method is public only to be callable from a callback
     */
    public function notify($message, $severity = \TYPO3\CMS\Core\Messaging\FlashMessage::OK)
    {
        static $numberOfValidNotifications = 0;

        if ($severity <= \TYPO3\CMS\Core\Messaging\FlashMessage::OK || \TYPO3\CMS\Core\Messaging\FlashMessage::OK) {
            $numberOfValidNotifications++;
            if ($numberOfValidNotifications > 20) {
                // Do not show more "ok" messages
                return;
            }
        }

        $flashMessage = GeneralUtility::makeInstance(
            \TYPO3\CMS\Core\Messaging\FlashMessage::class,
            $message,
            '',
            $severity,
            true
        );
        /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */
        $flashMessageService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessageService::class);
        /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
        $defaultFlashMessageQueue->enqueue($flashMessage);
    }

    /**
     * Creates an entry in syslog.
     *
     * @param string $message
     * @param integer $severity
     * @return void
     */
    public function syslog($message, $severity = \TYPO3\CMS\Core\Messaging\FlashMessage::OK)
    {
        switch ($severity) {
            case \TYPO3\CMS\Core\Messaging\FlashMessage::NOTICE:
                $severity = GeneralUtility::SYSLOG_SEVERITY_NOTICE;
                break;
            case \TYPO3\CMS\Core\Messaging\FlashMessage::INFO:
                $severity = GeneralUtility::SYSLOG_SEVERITY_INFO;
                break;
            case \TYPO3\CMS\Core\Messaging\FlashMessage::OK:
                $severity = GeneralUtility::SYSLOG_SEVERITY_INFO;
                break;
            case \TYPO3\CMS\Core\Messaging\FlashMessage::WARNING:
                $severity = GeneralUtility::SYSLOG_SEVERITY_WARNING;
                break;
            case \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR:
                $severity = GeneralUtility::SYSLOG_SEVERITY_ERROR;
                break;
        }

        GeneralUtility::sysLog($message, 'image_autoresize', $severity);
    }

}


<?php
/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with TYPO3 source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace Causal\ImageAutoresize\Slots;

use Causal\ImageAutoresize\Controller\ConfigurationController;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Resource\Driver\AbstractDriver;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Service\FileProcessingService;
use Causal\ImageAutoresize\Service\ImageResizer;

/**
 * Slot implementation when a file is uploaded but before it is processed
 * by \TYPO3\CMS\Core\Resource\ResourceStorage to automatically resize
 * huge pictures.
 *
 * @category    Slots
 * @package     TYPO3
 * @subpackage  tx_imageautoresize
 * @author      Xavier Perseguers <xavier@causal.ch>
 * @license     http://www.gnu.org/copyleft/gpl.html
 */
class FileUpload
{

    const SIGNAL_SanitizeFileName = 'sanitizeFileName';
    const SIGNAL_PostFileReplace = 'postFileReplace';
    const SIGNAL_PreFileAdd = 'preFileAdd';
    const SIGNAL_PopulateMetadata = 'populateMetadata';

    /**
     * @var ImageResizer
     */
    protected static $imageResizer;

    /**
     * @var array|null
     */
    protected static $metadata;

    /**
     * @var string|null
     */
    protected static $originalFileName;

    /**
     * Default constructor.
     */
    public function __construct()
    {
        if (static::$imageResizer === null) {
            static::$imageResizer = GeneralUtility::makeInstance(ImageResizer::class);

            $configuration = ConfigurationController::readConfiguration();
            static::$imageResizer->initializeRulesets($configuration);
        }
    }

    /**
     * Sanitizes the file name.
     *
     * @param string $fileName
     * @param \TYPO3\CMS\Core\Resource\Folder $folder
     * @return void|array
     */
    public function sanitizeFileName($fileName, \TYPO3\CMS\Core\Resource\Folder $folder)
    {
        $slotArguments = func_get_args();
        // Last parameter is the signal name itself and is not actually part of the arguments
        array_pop($slotArguments);

        $storageConfiguration = $folder->getStorage()->getConfiguration();
        $storageRecord = $folder->getStorage()->getStorageRecord();
        if ($storageRecord['driver'] !== 'Local') {
            // Unfortunately unsupported yet
            return;
        }

        $targetDirectory = $storageConfiguration['pathType'] === 'relative' ? PATH_site : '';
        $targetDirectory .= rtrim(rtrim($storageConfiguration['basePath'], '/') . $folder->getIdentifier(), '/');

        $processedFileName = static::$imageResizer->getProcessedFileName(
            $targetDirectory . '/' . $fileName,
            $GLOBALS['BE_USER']
        );
        if ($processedFileName !== null) {
            static::$originalFileName = $fileName;
            $slotArguments[0] = PathUtility::basename($processedFileName);

            return $slotArguments;
        }
    }

    /**
     * A file is about to be added as a *replacement* of an existing
     * one.
     *
     * @param File $file
     * @param string $uploadedFileName
     * @return void
     */
    public function postFileReplace(File $file, $uploadedFileName)
    {
        $folder = $file->getParentFolder();
        $storageConfiguration = $folder->getStorage()->getConfiguration();
        $storageRecord = $folder->getStorage()->getStorageRecord();
        if ($storageRecord['driver'] !== 'Local') {
            // Unfortunately unsupported yet
            return;
        }

        $targetDirectory = $storageConfiguration['pathType'] === 'relative' ? PATH_site : '';
        $targetDirectory .= rtrim(rtrim($storageConfiguration['basePath'], '/') . $folder->getIdentifier(), '/');
        $targetFileName = $targetDirectory . '/' . $file->getName();

        $this->processFile($targetFileName, PathUtility::basename($targetFileName), $targetDirectory, $file);
        $this->populateMetadata($file, $folder);
    }

    /**
     * Auto-resizes a given source file (possibly converting it as well).
     *
     * @param string $targetFileName
     * @param \TYPO3\CMS\Core\Resource\Folder $folder
     * @param string $sourceFile
     * @return void
     */
    public function preFileAdd(&$targetFileName, \TYPO3\CMS\Core\Resource\Folder $folder, $sourceFile)
    {
        $storageConfiguration = $folder->getStorage()->getConfiguration();
        $storageRecord = $folder->getStorage()->getStorageRecord();
        if ($storageRecord['driver'] !== 'Local') {
            // Unfortunately unsupported yet
            return;
        }

        if (static::$originalFileName) {
            // Temporarily change back the file name to ensure original format is used
            // when converting from one format to another with IM/GM
            $targetFileName = static::$originalFileName;
            static::$originalFileName = null;
        }

        $targetDirectory = $storageConfiguration['pathType'] === 'relative' ? PATH_site : '';
        $targetDirectory .= rtrim(rtrim($storageConfiguration['basePath'], '/') . $folder->getIdentifier(), '/');

        $extension = strtolower(substr($targetFileName, strrpos($targetFileName, '.') + 1));

        // Various operation (including IM/GM) relies on a file WITH an extension
        $originalSourceFile = $sourceFile;
        $sourceFile .= '.' . $extension;

        if (rename($originalSourceFile, $sourceFile)) {
            $newSourceFile = $this->processFile($sourceFile, $targetFileName, $targetDirectory);
            $newExtension = strtolower(substr($newSourceFile, strrpos($newSourceFile, '.') + 1));

            // We must go back to original (temporary) file name
            rename($newSourceFile, $originalSourceFile);

            if ($newExtension !== $extension) {
                $targetFileName = substr($targetFileName, 0, -strlen($extension)) . $newExtension;
            }
        }
    }

    /**
     * @param string $fileName Full path to the file to be processed
     * @param string $targetFileName Target file name if not converted, no path included
     * @param string $targetDirectory
     * @param File $file
     * @return string
     */
    protected function processFile($fileName, &$targetFileName, $targetDirectory, File $file = null)
    {
        $newFileName = static::$imageResizer->processFile(
            $fileName,
            $targetFileName,
            $targetDirectory,
            $file,
            $GLOBALS['BE_USER'],
            [$this, 'notify']
        );

        static::$metadata = static::$imageResizer->getLastMetadata();

        return $newFileName;
    }

    /**
     * Populates the FAL metadata of the resized image.
     *
     * @param \TYPO3\CMS\Core\Resource\FileInterface $file
     * @param \TYPO3\CMS\Core\Resource\Folder $folder
     * @return void
     */
    public function populateMetadata(\TYPO3\CMS\Core\Resource\FileInterface $file, \TYPO3\CMS\Core\Resource\Folder $folder)
    {
        if (is_array(static::$metadata) && count(static::$metadata)) {
            \Causal\ImageAutoresize\Utility\FAL::indexFile(
                $file,
                '', '',
                static::$metadata['COMPUTED']['Width'],
                static::$metadata['COMPUTED']['Height'],
                static::$metadata
            );
        }
    }

    /**
     * Notifies the user using a Flash message.
     *
     * @param string $message The message
     * @param integer $severity Optional severity, must be either of \TYPO3\CMS\Core\Messaging\FlashMessage::INFO,
     *                          \TYPO3\CMS\Core\Messaging\FlashMessage::OK, \TYPO3\CMS\Core\Messaging\FlashMessage::WARNING
     *                          or \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR.
     *                          Default is \TYPO3\CMS\Core\Messaging\FlashMessage::OK.
     * @return void
     * @internal This method is public only to be callable from a callback
     */
    public function notify($message, $severity = \TYPO3\CMS\Core\Messaging\FlashMessage::OK)
    {
        if (TYPO3_MODE !== 'BE') {
            return;
        }
        $flashMessage = GeneralUtility::makeInstance(
            \TYPO3\CMS\Core\Messaging\FlashMessage::class,
            $message,
            '',
            $severity,
            true
        );
        /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */
        $flashMessageService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessageService::class);
        /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
        $defaultFlashMessageQueue->enqueue($flashMessage);
    }

}


<?php
/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with TYPO3 source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace Causal\ImageAutoresize\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\ArrayUtility;

/**
 * Configuration controller.
 *
 * @package     TYPO3
 * @subpackage  tx_imageautoresize
 * @author      Xavier Perseguers <xavier@causal.ch>
 * @license     http://www.gnu.org/copyleft/gpl.html
 */
class ConfigurationController
{

    const virtualTable = 'tx_imageautoresize';
    const virtualRecordId = 1;

    /**
     * @var string
     */
    protected $extKey = 'image_autoresize';

    /**
     * @var array
     */
    protected $expertKey = 'image_autoresize_ff';

    /**
     * @var \TYPO3\CMS\Lang\LanguageService
     */
    protected $languageService;

    /**
     * @var \TYPO3\CMS\Backend\Form\FormEngine
     */
    protected $tceforms;

    /**
     * @var \TYPO3\CMS\Backend\Form\FormResultCompiler $formResultCompiler
     */
    protected $formResultCompiler;

    /**
     * @var \TYPO3\CMS\Backend\Template\ModuleTemplate
     */
    protected $moduleTemplate;

    /**
     * @var array
     */
    protected $config;

    /**
     * Generally used for accumulating the output content of backend modules
     *
     * @var string
     */
    public $content = '';

    /**
     * Default constructor
     */
    public function __construct()
    {
        $this->moduleTemplate = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Template\ModuleTemplate::class);
        $this->languageService = $GLOBALS['LANG'];

        if (!empty($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$this->expertKey])) {
            // Automatically migrate configuration from v1.8
            $config = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$this->expertKey];
            $config = unserialize($config);
            if (is_array($config) && !empty($config) && $this->persistConfiguration($config)) {
                // Drop legacy configuration
                $this->writeToLocalconf($this->expertKey, []);
            }
        }
        $this->config = static::readConfiguration();
        $this->config['conversion_mapping'] = implode(LF, explode(',', $this->config['conversion_mapping']));
    }

    /**
     * Injects the request object for the current request or subrequest
     * As this controller goes only through the main() method, it is rather simple for now
     *
     * @param ServerRequestInterface $request the current request
     * @return ResponseInterface the response with the content
     */
    public function mainAction(ServerRequestInterface $request) : ResponseInterface
    {
        /** @var ResponseInterface $response */
        $response = func_num_args() === 2 ? func_get_arg(1) : null;

        $this->languageService->includeLLFile('EXT:image_autoresize/Resources/Private/Language/locallang_mod.xlf');
        $this->processData();

        $formTag = '<form action="" method="post" name="editform" id="EditDocumentController">';

        $this->moduleTemplate->setForm($formTag);

        $this->content .= sprintf('
%s
', htmlspecialchars($this->languageService->getLL('title')));
        $this->addStatisticsAndSocialLink();

        // Generate the content
        $this->moduleContent($this->config);

        // Compile document
        $this->addToolbarButtons();
        $this->moduleTemplate->setContent($this->content);
        $content = $this->moduleTemplate->renderContent();

        if ($response !== null) {
            $response->getBody()->write($content);
        } else {
            // Behaviour in TYPO3 v9
            $response = new HtmlResponse($content);
        }

        return $response;
    }

    /**
     * Generates the module content.
     *
     * @param array $row
     * @return void
     */
    protected function moduleContent(array $row)
    {
        $this->formResultCompiler = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormResultCompiler::class);

        $wizard = $this->formResultCompiler->addCssFiles();
        $wizard .= $this->buildForm($row);
        $wizard .= $this->formResultCompiler->printNeededJSFunctions();

        $this->content .= $wizard;
    }

    /**
     * Builds the expert configuration form.
     *
     * @param array $row
     * @return string
     */
    protected function buildForm(array $row)
    {
        $record = [
            'uid' => static::virtualRecordId,
            'pid' => 0,
        ];
        $record = array_merge($record, $row);

        // Trick to use a virtual record
        $dataProviders =& $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord'];

        $dataProviders[\Causal\ImageAutoresize\Backend\Form\FormDataProvider\VirtualDatabaseEditRow::class] = [
            'before' => [
                \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEditRow::class,
            ]
        ];

        // Initialize record in our virtual provider
        \Causal\ImageAutoresize\Backend\Form\FormDataProvider\VirtualDatabaseEditRow::initialize($record);

        /** @var \TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord $formDataGroup */
        $formDataGroup = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord::class);
        /** @var \TYPO3\CMS\Backend\Form\FormDataCompiler $formDataCompiler */
        $formDataCompiler = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\FormDataCompiler::class, $formDataGroup);
        /** @var \TYPO3\CMS\Backend\Form\NodeFactory $nodeFactory */
        $nodeFactory = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Form\NodeFactory::class);

        $formDataCompilerInput = [
            'tableName' => static::virtualTable,
            'vanillaUid' => $record['uid'],
            'command' => 'edit',
            'returnUrl' => '',
        ];

        // Load the configuration of virtual table 'tx_imageautoresize'
        $this->loadVirtualTca();

        $formData = $formDataCompiler->compile($formDataCompilerInput);
        $formData['renderType'] = 'outerWrapContainer';
        $formResult = $nodeFactory->create($formData)->render();

        // Remove header and footer
        $html = preg_replace('/
.*<\/h1>/', '', $formResult['html']);

        $startFooter = strrpos($html, '
');
        $endTag = '
';

        if ($startFooter !== false) {
            $endFooter = strpos($html, $endTag, $startFooter);
            $html = substr($html, 0, $startFooter) . substr($html, $endFooter + strlen($endTag));
        }

        $formResult['html'] = '';
        $formResult['doSaveFieldName'] = 'doSave';

        // @todo: Put all the stuff into FormEngine as final "compiler" class
        // @todo: This is done here for now to not rewrite JStop()
        // @todo: and printNeededJSFunctions() now
        $this->formResultCompiler->mergeResult($formResult);

        // Combine it all
        $formContent = '

			' . $html . '

			<input type="hidden" name="returnUrl" value="' . htmlspecialchars($this->retUrl) . '" />
			<input type="hidden" name="closeDoc" value="0" />
			<input type="hidden" name="doSave" value="0" />
			<input type="hidden" name="_serialNumber" value="' . md5(microtime()) . '" />
			<input type="hidden" name="_scrollPosition" value="" />';

        $overriddenAjaxUrl = GeneralUtility::quoteJSvalue(BackendUtility::getModuleUrl('TxImageAutoresize::record_flex_container_add'));
        $formContent .= <<moduleTemplate->getDocHeaderComponent()->getButtonBar();
        $saveSplitButton = $buttonBar->makeSplitButton();

        // SAVE button:
        $saveButton = $buttonBar->makeInputButton()
            ->setTitle(htmlspecialchars($this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveDoc')))
            ->setName('_savedok')
            ->setValue('1')
            ->setForm('EditDocumentController')
            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
                'actions-document-save',
                \TYPO3\CMS\Core\Imaging\Icon::SIZE_SMALL
            ));
        $saveSplitButton->addItem($saveButton, true);

        // SAVE & CLOSE button:
        $saveAndCloseButton = $buttonBar->makeInputButton()
            ->setTitle(htmlspecialchars($this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.saveCloseDoc')))
            ->setName('_saveandclosedok')
            ->setValue('1')
            ->setForm('EditDocumentController')
            ->setClasses('t3js-editform-submitButton')
            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
                'actions-document-save-close',
                \TYPO3\CMS\Core\Imaging\Icon::SIZE_SMALL
            ));
        $saveSplitButton->addItem($saveAndCloseButton);

        $buttonBar->addButton($saveSplitButton, \TYPO3\CMS\Backend\Template\Components\ButtonBar::BUTTON_POSITION_LEFT, 2);

        // CLOSE button:
        $closeButton = $buttonBar->makeLinkButton()
            ->setTitle(htmlspecialchars($this->languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc')))
            ->setHref('#')
            ->setClasses('t3js-editform-close')
            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
                'actions-view-go-back',
                \TYPO3\CMS\Core\Imaging\Icon::SIZE_SMALL
            ));
        $buttonBar->addButton($closeButton);
    }

    /**
     * Prints out the module HTML.
     *
     * @return string HTML output
     */
    public function printContent()
    {
        echo $this->content;
    }

    /**
     * Returns the default configuration.
     *
     * @return array
     */
    protected static function getDefaultConfiguration()
    {
        return [
            'directories' => 'fileadmin/,uploads/',
            'file_types' => 'jpg,jpeg,png',
            'threshold' => '400K',
            'max_width' => '1024',
            'max_height' => '768',
            'max_size' => '100M',
            'auto_orient' => true,
            'conversion_mapping' => implode(',', [
                'ai => jpg',
                'bmp => jpg',
                'pcx => jpg',
                'tga => jpg',
                'tif => jpg',
                'tiff => jpg',
            ]),
        ];
    }

    /**
     * Processes submitted data and stores it to localconf.php.
     *
     * @return void
     */
    protected function processData()
    {
        $close = GeneralUtility::_GP('closeDoc');
        $save = GeneralUtility::_GP('_savedok');
        $saveAndClose = GeneralUtility::_GP('_saveandclosedok');

        if ($save || $saveAndClose) {
            $table = static::virtualTable;
            $id = static::virtualRecordId;
            $field = 'rulesets';

            $inputData_tmp = GeneralUtility::_GP('data');
            $data = $inputData_tmp[$table][$id];

            if (count($inputData_tmp[$table]) > 1) {
                foreach ($inputData_tmp[$table] as $key => $values) {
                    if ($key === $id) continue;
                    ArrayUtility::mergeRecursiveWithOverrule($data, $values);
                }
            }

            $newConfig = $this->config;
            ArrayUtility::mergeRecursiveWithOverrule($newConfig, $data);

            // Action commands (sorting order and removals of FlexForm elements)
            $ffValue = &$data[$field];
            if ($ffValue) {
                $actionCMDs = GeneralUtility::_GP('_ACTION_FLEX_FORMdata');
                if (is_array($actionCMDs[$table][$id][$field]['data'])) {
                    $dataHandler = new CustomDataHandler();
                    $dataHandler->_ACTION_FLEX_FORMdata($ffValue['data'], $actionCMDs[$table][$id][$field]['data']);
                }
                // Renumber all FlexForm temporary ids
                $this->persistFlexForm($ffValue['data']);

                // Keep order of FlexForm elements
                $newConfig[$field] = $ffValue;
            }

            // Persist configuration
            $localconfConfig = $newConfig;
            $localconfConfig['conversion_mapping'] = implode(',', GeneralUtility::trimExplode(LF, $localconfConfig['conversion_mapping'], true));

            if ($this->persistConfiguration($localconfConfig)) {
                $this->config = $newConfig;
            }
        }

        if ($close || $saveAndClose) {
            $closeUrl = BackendUtility::getModuleUrl('tools_ExtensionmanagerExtensionmanager');
            \TYPO3\CMS\Core\Utility\HttpUtility::redirect($closeUrl);
        }
    }

    /**
     * Writes a configuration line to AdditionalConfiguration.php.
     * We don't use the tx_install methods as they add unneeded
     * comments at the end of the file.
     *
     * @param string $key
     * @param array $config
     * @return bool
     */
    protected function writeToLocalconf($key, array $config)
    {
        /** @var $objectManager \TYPO3\CMS\Extbase\Object\ObjectManager */
        $objectManager = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
        /** @var $configurationManager \TYPO3\CMS\Core\Configuration\ConfigurationManager */
        $configurationManager = $objectManager->get(\TYPO3\CMS\Core\Configuration\ConfigurationManager::class);
        return $configurationManager->setLocalConfigurationValueByPath('EXT/extConf/' . $key, serialize($config));
    }

    /**
     * @return array
     */
    public static function readConfiguration() : array
    {
        $configurationFileName = PATH_site . 'typo3conf/image_autoresize.config.php';

        $config = file_exists($configurationFileName) ? include($configurationFileName) : [];
        if (!is_array($config) || empty($config)) {
            $config = static::getDefaultConfiguration();
        }

        return $config;
    }

    /**
     * Writes configuration to typo3conf/image_autoresize.config.php.
     *
     * @param array $config
     * @return bool
     */
    protected function persistConfiguration(array $config) : bool
    {
        $configurationFileName = PATH_site . 'typo3conf/image_autoresize.config.php';

        $exportConfig = var_export($config, true);
        $exportConfig = str_replace('array (', '[', $exportConfig);
        if (substr($exportConfig, -1) === ')') {
            $exportConfig = substr($exportConfig, 0, strlen($exportConfig) - 1) . ']';
        }
        $exportConfig = preg_replace('/=>\\s*[[]/s', '=> [', $exportConfig);
        $lines = explode(LF, $exportConfig);
        foreach ($lines as $i => $line) {
            if (preg_match('/^(\\s+)(.+)$/', $line, $matches)) {
                if ($matches[2] === '),') {
                    // Convert ending of former array declaration to new syntax
                    $matches[2] = '],';
                }
                $lines[$i] = str_repeat(' ', 2 * strlen($matches[1])) . $matches[2];
            }
        }
        $exportConfig = implode(LF, $lines);

        $content = '<?' . 'php' . LF . 'return ' . $exportConfig . ';' . LF;
        $success = GeneralUtility::writeFile($configurationFileName, $content);
        return true;
    }

    /**
     * Loads the configuration of the virtual table 'tx_imageautoresize'.
     *
     * @return void
     */
    protected function loadVirtualTca()
    {
        $GLOBALS['TCA'][static::virtualTable] = include(ExtensionManagementUtility::extPath($this->extKey) . 'Configuration/TCA/Module/Options.php');
        ExtensionManagementUtility::addLLrefForTCAdescr(static::virtualTable, 'EXT:' . $this->extKey . '/Resource/Private/Language/locallang_csh_' . static::virtualTable . '.xlf');
    }

    /**
     * Persists FlexForm items by removing 'ID-' in front of new
     * items.
     *
     * @param array &$valueArray : by reference
     * @return void
     */
    protected function persistFlexForm(array &$valueArray)
    {
        foreach ($valueArray as $key => $value) {
            if ($key === 'el') {
                foreach ($value as $idx => $v) {
                    if ($v && substr($idx, 0, 3) === 'ID-') {
                        $valueArray[$key][substr($idx, 3)] = $v;
                        unset($valueArray[$key][$idx]);
                    }
                }
            } elseif (isset($valueArray[$key])) {
                $this->persistFlexForm($valueArray[$key]);
            }
        }
    }

    /**
     * Returns some statistics and a social link to Twitter.
     *
     * @return void
     */
    protected function addStatisticsAndSocialLink()
    {
        $fileName = PATH_site . 'typo3temp/.tx_imageautoresize';

        if (!is_file($fileName)) {
            return;
        }

        $data = json_decode(file_get_contents($fileName), true);
        if (!is_array($data) || !(isset($data['images']) && isset($data['bytes']))) {
            return;
        }

        $resourcesPath = '../' . ExtensionManagementUtility::siteRelPath($this->extKey) . 'Resources/Public/';
        $pageRenderer = $this->moduleTemplate->getPageRenderer();
        $pageRenderer->addCssFile($resourcesPath . 'Css/twitter.css');
        $pageRenderer->addJsFile($resourcesPath . 'JavaScript/popup.js');

        $totalSpaceClaimed = GeneralUtility::formatSize((int)$data['bytes']);
        $messagePattern = $this->languageService->getLL('storage.claimed');
        $message = sprintf($messagePattern, $totalSpaceClaimed, (int)$data['images']);

        $flashMessage = htmlspecialchars($message);

        $twitterMessagePattern = $this->languageService->getLL('social.twitter');
        $message = sprintf($twitterMessagePattern, $totalSpaceClaimed);
        $url = 'https://extensions.typo3.org/extension/image_autoresize/';

        $twitterLink = 'https://twitter.com/intent/tweet?text=' . urlencode($message) . '&url=' . urlencode($url);
        $twitterLink = GeneralUtility::quoteJSvalue($twitterLink);
        $flashMessage .= '

                languageService->getLL('social.share')) . '">

                    Tweet

';

        $this->content .= '

                        ' . $flashMessage . '

        ';
    }
}

// ReflectionMethod does not work properly with arguments passed as reference thus
// using a trick here
class CustomDataHandler extends \TYPO3\CMS\Core\DataHandling\DataHandler
{

    /**
     * Actions for flex form element (move, delete)
     * allows to remove and move flexform sections
     *
     * @param array &$valueArray by reference
     * @param array $actionCMDs
     * @return void
     */
    public function _ACTION_FLEX_FORMdata(&$valueArray, $actionCMDs)
    {
        parent::_ACTION_FLEX_FORMdata($valueArray, $actionCMDs);
    }

}