<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Sender\Builder;

use Apple\ApnPush\Encoder\PayloadEncoder;
use Apple\ApnPush\Encoder\PayloadEncoderInterface;
use Apple\ApnPush\Protocol\Http\Authenticator\AuthenticatorInterface;
use Apple\ApnPush\Protocol\Http\ExceptionFactory\ExceptionFactory;
use Apple\ApnPush\Protocol\Http\ExceptionFactory\ExceptionFactoryInterface;
use Apple\ApnPush\Protocol\Http\Sender\CurlHttpSender;
use Apple\ApnPush\Protocol\Http\Sender\HttpSenderInterface;
use Apple\ApnPush\Protocol\Http\UriFactory\UriFactory;
use Apple\ApnPush\Protocol\Http\UriFactory\UriFactoryInterface;
use Apple\ApnPush\Protocol\Http\Visitor\AddApnIdHeaderVisitor;
use Apple\ApnPush\Protocol\Http\Visitor\AddCollapseIdHeaderVisitor;
use Apple\ApnPush\Protocol\Http\Visitor\AddExpirationHeaderVisitor;
use Apple\ApnPush\Protocol\Http\Visitor\AddPriorityHeaderVisitor;
use Apple\ApnPush\Protocol\Http\Visitor\HttpProtocolChainVisitor;
use Apple\ApnPush\Protocol\Http\Visitor\HttpProtocolVisitorInterface;
use Apple\ApnPush\Protocol\HttpProtocol;
use Apple\ApnPush\Protocol\ProtocolInterface;
use Apple\ApnPush\Sender\Sender;
use Apple\ApnPush\Sender\SenderInterface;

/**
 * Builder for create sender with HTTP/2 protocol
 */
class Http20Builder implements BuilderInterface
{
    /**
     * @var \SplPriorityQueue|HttpProtocolVisitorInterface[]
     */
    private $visitors;

    /**
     * @var UriFactoryInterface
     */
    private $uriFactory;

    /**
     * @var PayloadEncoderInterface
     */
    private $payloadEncoder;

    /**
     * @var AuthenticatorInterface
     */
    private $authenticator;

    /**
     * @var HttpSenderInterface
     */
    private $httpSender;

    /**
     * @var ExceptionFactoryInterface
     */
    private $exceptionFactory;

    /**
     * @var bool
     */
    private $addedDefaultVisitors;

    /**
     * Constructor.
     *
     * @param AuthenticatorInterface $authenticator
     */
    public function __construct(AuthenticatorInterface $authenticator)
    {
        $this->authenticator = $authenticator;
        $this->visitors = new \SplPriorityQueue();
        $this->uriFactory = new UriFactory();
        $this->payloadEncoder = new PayloadEncoder();
        $this->httpSender = new CurlHttpSender();
        $this->exceptionFactory = new ExceptionFactory();

        $this->addDefaultVisitors();
    }

    /**
     * Set authenticator
     *
     * @param AuthenticatorInterface $authenticator
     *
     * @return Http20Builder
     */
    public function setAuthenticator(AuthenticatorInterface $authenticator): Http20Builder
    {
        $this->authenticator = $authenticator;

        return $this;
    }

    /**
     * Add visitor
     *
     * @param HttpProtocolVisitorInterface $visitor
     * @param int                          $priority
     *
     * @return Http20Builder
     */
    public function addVisitor(HttpProtocolVisitorInterface $visitor, int $priority = 0): Http20Builder
    {
        $this->visitors->insert($visitor, $priority);

        return $this;
    }

    /**
     * Add default visitors
     *
     * @return Http20Builder
     *
     * @deprecated This method is deprecated and will be a move to private scope.
     *             Please do not use this method in your code.
     *             This method will be called from the constructor of this builder.
     */
    public function addDefaultVisitors(): Http20Builder
    {
        if ($this->addedDefaultVisitors) {
            return $this;
        }

        $this->addedDefaultVisitors = true;

        $this->addVisitor(new AddExpirationHeaderVisitor());
        $this->addVisitor(new AddPriorityHeaderVisitor());
        $this->addVisitor(new AddApnIdHeaderVisitor());
        $this->addVisitor(new AddCollapseIdHeaderVisitor());

        return $this;
    }

    /**
     * Set URI factory
     *
     * @param UriFactoryInterface $uriFactory
     *
     * @return Http20Builder
     */
    public function setUriFactory(UriFactoryInterface $uriFactory): Http20Builder
    {
        $this->uriFactory = $uriFactory;

        return $this;
    }

    /**
     * Set notification encoder
     *
     * @param PayloadEncoderInterface $payloadEncoder
     *
     * @return Http20Builder
     */
    public function setPayloadEncoder(PayloadEncoderInterface $payloadEncoder): Http20Builder
    {
        $this->payloadEncoder = $payloadEncoder;

        return $this;
    }

    /**
     * Set http sender
     *
     * @param HttpSenderInterface $httpSender
     *
     * @return Http20Builder
     */
    public function setHttpSender(HttpSenderInterface $httpSender): Http20Builder
    {
        $this->httpSender = $httpSender;

        return $this;
    }

    /**
     * Set exception factory
     *
     * @param ExceptionFactoryInterface $exceptionFactory
     *
     * @return Http20Builder
     */
    public function setExceptionFactory(ExceptionFactoryInterface $exceptionFactory): Http20Builder
    {
        $this->exceptionFactory = $exceptionFactory;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function buildProtocol(): ProtocolInterface
    {
        $chainVisitor = $this->createChainVisitor();

        return new HttpProtocol(
            $this->authenticator,
            $this->httpSender,
            $this->payloadEncoder,
            $this->uriFactory,
            $chainVisitor,
            $this->exceptionFactory
        );
    }

    /**
     * {@inheritdoc}
     */
    public function build(): SenderInterface
    {
        $protocol = $this->buildProtocol();

        return new Sender($protocol);
    }

    /**
     * Create chain visitor
     *
     * @return HttpProtocolChainVisitor
     */
    private function createChainVisitor(): HttpProtocolChainVisitor
    {
        $chainVisitors = new HttpProtocolChainVisitor();
        $visitors = clone $this->visitors;
        $priority = 0;

        foreach ($visitors as $visitor) {
            $chainVisitors->add($visitor, ++$priority);
        }

        return $chainVisitors;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\UriFactory;

use Apple\ApnPush\Model\DeviceToken;

/**
 * Default URI factory
 */
class UriFactory implements UriFactoryInterface
{
    /**
     * Create URI for device
     *
     * @param DeviceToken $deviceToken
     * @param bool        $sandbox
     *
     * @return string
     */
    public function create(DeviceToken $deviceToken, bool $sandbox): string
    {
        $uri = 'https://api.push.apple.com/3/device/%s';

        if ($sandbox) {
            $uri = 'https://api.development.push.apple.com/3/device/%s';
        }

        return sprintf($uri, $deviceToken);
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Jwt\SignatureGenerator;

use Apple\ApnPush\Jwt\JwtInterface;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\Converter\JsonConverter;
use Jose\Component\Core\Converter\StandardConverter;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Jose\Component\Signature\Serializer\JWSSerializer;

/**
 * The JWT signature generator worked with "web-token/jwt-*" libraries.
 *
 * Next libraries must be installed:
 *      - web-token/jwt-key-mgmt
 *      - web-token/jwt-core
 *      - web-token/jwt-signature
 */
class WebTokenJwtSignatureGenerator implements SignatureGeneratorInterface
{
    /**
     * @var JsonConverter
     */
    private $jsonConverter;

    /**
     * @var JWSBuilder
     */
    private $jwsBuilder;

    /**
     * @var JWSSerializer
     */
    private $serializer;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->jsonConverter = new StandardConverter();
        $this->jwsBuilder = new JWSBuilder($this->jsonConverter, AlgorithmManager::create([new ES256()]));
        $this->serializer = new CompactSerializer($this->jsonConverter);
    }

    /**
     * {@inheritdoc}
     */
    public function generate(JwtInterface $jwt): string
    {
        $jwk = JWKFactory::createFromKeyFile($jwt->getPath(), '', [
            'kid' => $jwt->getKey(),
        ]);

        $claims = [
            'iss' => $jwt->getTeamId(),
            'iat' => time(),
        ];

        $header = [
            'alg' => 'ES256',
            'kid' => $jwk->get('kid'),
        ];

        $payload = $this->jsonConverter->encode($claims);

        $jws = $this->jwsBuilder
            ->create()
            ->withPayload($payload)
            ->addSignature($jwk, $header)
            ->build();

        return $this->serializer->serialize($jws);
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Model;

/**
 * Send this notification to device.
 */
class Notification
{
    /**
     * @var Payload
     */
    private $payload;

    /**
     * @var ApnId
     */
    private $apnId;

    /**
     * @var Priority
     */
    private $priority;

    /**
     * @var Expiration
     */
    private $expiration;

    /**
     * @var CollapseId
     */
    private $collapseId;

    /**
     * Constructor.
     *
     * @param Payload         $payload
     * @param ApnId|null      $apnId
     * @param Priority|null   $priority
     * @param Expiration|null $expiration
     * @param CollapseId|null $collapseId
     */
    public function __construct(Payload $payload, ApnId $apnId = null, Priority $priority = null, Expiration $expiration = null, CollapseId $collapseId = null)
    {
        $this->payload = $payload;
        $this->priority = $priority;
        $this->apnId = $apnId;
        $this->expiration = $expiration;
        $this->collapseId = $collapseId;
    }

    /**
     * Create new notification with body only
     *
     * @param string $body
     *
     * @return Notification
     */
    public static function createWithBody(string $body): Notification
    {
        return new self(Payload::createWithBody($body));
    }

    /**
     * Set payload
     *
     * @param Payload $payload
     *
     * @return Notification
     */
    public function withPayload(Payload $payload): Notification
    {
        $cloned = clone $this;

        $cloned->payload = $payload;

        return $cloned;
    }

    /**
     * Get payload
     *
     * @return Payload
     */
    public function getPayload(): Payload
    {
        return $this->payload;
    }

    /**
     * Set apn identifier
     *
     * @param ApnId $apnId
     *
     * @return Notification
     */
    public function withApnId(ApnId $apnId = null): Notification
    {
        $cloned = clone $this;

        $cloned->apnId = $apnId;

        return $cloned;
    }

    /**
     * Get identifier of notification
     *
     * @return ApnId
     */
    public function getApnId(): ?ApnId
    {
        return $this->apnId;
    }

    /**
     * Set priority
     *
     * @param Priority $priority
     *
     * @return Notification
     */
    public function withPriority(Priority $priority = null): Notification
    {
        $cloned = clone $this;

        $cloned->priority = $priority;

        return $cloned;
    }

    /**
     * Get priority
     *
     * @return Priority
     */
    public function getPriority(): ?Priority
    {
        return $this->priority;
    }

    /**
     * Set expiration
     *
     * @param Expiration $expiration
     *
     * @return Notification
     */
    public function withExpiration(Expiration $expiration = null): Notification
    {
        $cloned = clone $this;

        $cloned->expiration = $expiration;

        return $cloned;
    }

    /**
     * Get expiration
     *
     * @return Expiration
     */
    public function getExpiration(): ?Expiration
    {
        return $this->expiration;
    }

    /**
     * Set the collapse identifier
     *
     * @param CollapseId|null $collapseId
     *
     * @return Notification
     */
    public function withCollapseId(CollapseId $collapseId = null): Notification
    {
        $cloned = clone $this;

        $cloned->collapseId = $collapseId;

        return $cloned;
    }

    /**
     * Get the collapse identifier
     *
     * @return CollapseId|null
     */
    public function getCollapseId(): ?CollapseId
    {
        return $this->collapseId;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Jwt\SignatureGenerator;

use Apple\ApnPush\Jwt\JwtInterface;
use Jose\Factory\JWKFactory;
use Jose\Factory\JWSFactory;

/**
 * The signature generator with use "spomky-labs/jose" library.
 */
class SpomkyLabsJoseSignatureGenerator implements SignatureGeneratorInterface
{
    /**
     * {@inheritdoc}
     */
    public function generate(JwtInterface $jwt): string
    {
        $jwk = JWKFactory::createFromKeyFile($jwt->getPath(), '', [
            'kid' => $jwt->getKey(),
            'alg' => 'ES256',
            'use' => 'sig',
        ]);

        $payload = [
            'iss' => $jwt->getTeamId(),
            'iat' => time(),
        ];

        $header = [
            'alg' => 'ES256',
            'kid' => $jwk->get('kid'),
        ];

        return JWSFactory::createJWSToCompactJSON(
            $payload,
            $jwk,
            $header
        );
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\Sender;

use Apple\ApnPush\Protocol\Http\Request;
use Apple\ApnPush\Protocol\Http\Response;
use Apple\ApnPush\Protocol\Http\Sender\Exception\HttpSenderException;

/**
 * Send HTTP request via cURL
 */
class CurlHttpSender implements HttpSenderInterface
{
    /**
     * @var resource
     */
    private $resource;

    /**
     * {@inheritdoc}
     *
     * @throws HttpSenderException
     */
    public function send(Request $request): Response
    {
        $this->initializeCurlResource();
        $this->prepareCurlResourceByRequest($request);

        $content = curl_exec($this->resource);

        if (false === $content) {
            throw new HttpSenderException(sprintf(
                'cURL Error [%d]: %s',
                (int) curl_errno($this->resource),
                (string) curl_error($this->resource)
            ));
        }

        $statusCode = (int) curl_getinfo($this->resource, CURLINFO_HTTP_CODE);

        return new Response($statusCode, (string) $content);
    }

    /**
     * {@inheritdoc}
     */
    public function close(): void
    {
        curl_close($this->resource);
        $this->resource = null;
    }

    /**
     * Initialize cURL resource
     */
    private function initializeCurlResource(): void
    {
        if (!$this->resource) {
            $this->resource = curl_init();

            curl_setopt($this->resource, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($this->resource, CURLOPT_POST, 1);
            curl_setopt($this->resource, CURLOPT_HTTP_VERSION, 3);
        }
    }

    /**
     * Prepare cURL resource by request
     *
     * @param Request $request
     */
    private function prepareCurlResourceByRequest(Request $request): void
    {
        curl_setopt($this->resource, CURLOPT_URL, $request->getUrl());
        curl_setopt($this->resource, CURLOPT_POSTFIELDS, $request->getContent());

        if ($request->getCertificate()) {
            curl_setopt($this->resource, CURLOPT_SSLCERT, $request->getCertificate());
            curl_setopt($this->resource, CURLOPT_SSLCERTPASSWD, $request->getCertificatePassPhrase());
        }

        $inlineHeaders = [];

        foreach ($request->getHeaders() as $name => $value) {
            $inlineHeaders[] = sprintf('%s: %s', $name, $value);
        }

        curl_setopt($this->resource, CURLOPT_HTTPHEADER, $inlineHeaders);
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\Visitor;

use Apple\ApnPush\Model\Notification;
use Apple\ApnPush\Protocol\Http\Request;

/**
 * Visitor for add the apns-collapse-id header.
 */
class AddCollapseIdHeaderVisitor implements HttpProtocolVisitorInterface
{
    /**
     * {@inheritdoc}
     */
    public function visit(Notification $notification, Request $request): Request
    {
        $collapseId = $notification->getCollapseId();

        if ($collapseId) {
            $request = $request->withHeader('apns-collapse-id', $collapseId->getValue());
        }

        return $request;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\Authenticator;

use Apple\ApnPush\Jwt\JwtInterface;
use Apple\ApnPush\Jwt\SignatureGenerator\SignatureGeneratorFactory;
use Apple\ApnPush\Jwt\SignatureGenerator\SignatureGeneratorInterface;
use Apple\ApnPush\Protocol\Http\Request;

/**
 * Authenticate request via Json Web Token
 */
class JwtAuthenticator implements AuthenticatorInterface
{
    /**
     * @var JwtInterface
     */
    private $jwt;

    /**
     * @var SignatureGeneratorInterface
     */
    private $signatureGenerator;

    /**
     * @var string
     */
    private $jws;

    /**
     * @var \DateInterval
     */
    private $jwsLifetime;

    /**
     * @var \DateTime
     */
    private $jwsValidTo;

    /**
     * Constructor.
     *
     * @param JwtInterface                $jwt
     * @param \DateInterval               $jwsLifetime
     * @param SignatureGeneratorInterface $signatureGenerator
     *
     * @throws \InvalidArgumentException
     * @throws \LogicException
     */
    public function __construct(JwtInterface $jwt, \DateInterval $jwsLifetime = null, SignatureGeneratorInterface $signatureGenerator = null)
    {
        $this->jwt = $jwt;

        if ($jwsLifetime && $jwsLifetime->invert) {
            throw new \InvalidArgumentException('JWS lifetime must not be inverted');
        }

        $this->jwsLifetime = $jwsLifetime;

        $this->signatureGenerator = $signatureGenerator ?: SignatureGeneratorFactory::resolve();
    }

    /**
     * {@inheritdoc}
     */
    public function authenticate(Request $request): Request
    {
        $now = new \DateTimeImmutable();

        if (!$this->jws || $this->jwsValidTo < $now) {
            $this->jws = $this->signatureGenerator->generate($this->jwt);
            $this->jwsValidTo = $this->jwsLifetime ? ($now)->add($this->jwsLifetime) : $this->jwsValidTo;
        }

        $request = $request->withHeader('authorization', sprintf('bearer %s', $this->jws));

        return $request;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\Visitor;

use Apple\ApnPush\Model\Notification;
use Apple\ApnPush\Protocol\Http\Request;

/**
 * Visitor for add priority header to request
 */
class AddPriorityHeaderVisitor implements HttpProtocolVisitorInterface
{
    /**
     * {@inheritdoc}
     */
    public function visit(Notification $notification, Request $request): Request
    {
        $priority = $notification->getPriority();

        if ($priority) {
            $request = $request->withHeader('apns-priority', (string) $priority->getValue());
        }

        return $request;
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Protocol\Http\ExceptionFactory;

use Apple\ApnPush\Exception\SendNotification\BadCertificateEnvironmentException;
use Apple\ApnPush\Exception\SendNotification\BadCertificateException;
use Apple\ApnPush\Exception\SendNotification\BadCollapseIdException;
use Apple\ApnPush\Exception\SendNotification\BadDeviceTokenException;
use Apple\ApnPush\Exception\SendNotification\BadExpirationDateException;
use Apple\ApnPush\Exception\SendNotification\BadMessageIdException;
use Apple\ApnPush\Exception\SendNotification\BadPathException;
use Apple\ApnPush\Exception\SendNotification\BadPriorityException;
use Apple\ApnPush\Exception\SendNotification\BadTopicException;
use Apple\ApnPush\Exception\SendNotification\DeviceTokenNotForTopicException;
use Apple\ApnPush\Exception\SendNotification\DuplicateHeadersException;
use Apple\ApnPush\Exception\SendNotification\ExpiredProviderTokenException;
use Apple\ApnPush\Exception\SendNotification\ForbiddenException;
use Apple\ApnPush\Exception\SendNotification\IdleTimeoutException;
use Apple\ApnPush\Exception\SendNotification\InternalServerErrorException;
use Apple\ApnPush\Exception\SendNotification\InvalidProviderTokenException;
use Apple\ApnPush\Exception\SendNotification\InvalidResponseException;
use Apple\ApnPush\Exception\SendNotification\MethodNotAllowedException;
use Apple\ApnPush\Exception\SendNotification\MissingContentInResponseException;
use Apple\ApnPush\Exception\SendNotification\MissingDeviceTokenException;
use Apple\ApnPush\Exception\SendNotification\MissingErrorReasonInResponseException;
use Apple\ApnPush\Exception\SendNotification\MissingProviderTokenException;
use Apple\ApnPush\Exception\SendNotification\MissingTopicException;
use Apple\ApnPush\Exception\SendNotification\PayloadEmptyException;
use Apple\ApnPush\Exception\SendNotification\PayloadTooLargeException;
use Apple\ApnPush\Exception\SendNotification\SendNotificationException;
use Apple\ApnPush\Exception\SendNotification\ServiceUnavailableException;
use Apple\ApnPush\Exception\SendNotification\ShutdownException;
use Apple\ApnPush\Exception\SendNotification\TooManyProviderTokenUpdatesException;
use Apple\ApnPush\Exception\SendNotification\TooManyRequestsException;
use Apple\ApnPush\Exception\SendNotification\TopicDisallowedException;
use Apple\ApnPush\Exception\SendNotification\UndefinedErrorException;
use Apple\ApnPush\Exception\SendNotification\UnregisteredException;
use Apple\ApnPush\Protocol\Http\Response;

/**
 * Default exception factory for create exception via response.
 * For all error codes, please see: https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW17
 */
class ExceptionFactory implements ExceptionFactoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function create(Response $response): SendNotificationException
    {
        $content = $response->getContent();

        if (!$content) {
            return new MissingContentInResponseException();
        }

        $json = json_decode($content, true);

        if (null === $json) {
            return new InvalidResponseException(sprintf(
                'Can not parse JSON in response. Error: %d - %s',
                json_last_error(),
                json_last_error_msg()
            ));
        }

        if (!array_key_exists('reason', $json)) {
            return new MissingErrorReasonInResponseException();
        }

        $reason = $json['reason'];

        return $this->createByReason($reason, $json);
    }

    /**
     * Create exception by reason
     *
     * @param string $reason
     * @param array  $json
     *
     * @return SendNotificationException
     */
    private function createByReason(string $reason, array $json): SendNotificationException
    {
        $reason = strtolower($reason);

        switch ($reason) {
            case 'badcollapseid':
                return new BadCollapseIdException();

            case 'baddevicetoken':
                return new BadDeviceTokenException();

            case 'badexpirationdate':
                return new BadExpirationDateException();

            case 'badmessageid':
                return new BadMessageIdException();

            case 'badpriority':
                return new BadPriorityException();

            case 'badtopic':
                return new BadTopicException();

            case 'devicetokennotfortopic':
                return new DeviceTokenNotForTopicException();

            case 'duplicateheaders':
                return new DuplicateHeadersException();

            case 'idletimeout':
                return new IdleTimeoutException();

            case 'missingdevicetoken':
                return new MissingDeviceTokenException();

            case 'missingtopic':
                return new MissingTopicException();

            case 'payloadempty':
                return new PayloadEmptyException();

            case 'topicdisallowed':
                return new TopicDisallowedException();

            case 'badcertificate':
                return new BadCertificateException();

            case 'badcertificateenvironment':
                return new BadCertificateEnvironmentException();

            case 'expiredprovidertoken':
                return new ExpiredProviderTokenException();

            case 'forbidden':
                return new ForbiddenException();

            case 'invalidprovidertoken':
                return new InvalidProviderTokenException();

            case 'missingprovidertoken':
                return new MissingProviderTokenException();

            case 'badpath':
                return new BadPathException();

            case 'methodnotallowed':
                return new MethodNotAllowedException();

            case 'unregistered':
                $timestamp = array_key_exists('timestamp', $json) ? $json['timestamp'] : 0;
                $lastConfirmed = new \DateTime('now', new \DateTimeZone('UTC'));
                $lastConfirmed->setTimestamp($timestamp);

                return new UnregisteredException($lastConfirmed);

            case 'payloadtoolarge':
                return new PayloadTooLargeException();

            case 'toomanyprovidertokenupdates':
                return new TooManyProviderTokenUpdatesException();

            case 'toomanyrequests':
                return new TooManyRequestsException();

            case 'internalservererror':
                return new InternalServerErrorException();

            case 'serviceunavailable':
                return new ServiceUnavailableException();

            case 'shutdown':
                return new ShutdownException();

            default:
                return new UndefinedErrorException();
        }
    }
}


<?php

declare(strict_types = 1);

/*
 * This file is part of the AppleApnPush package
 *
 * (c) Vitaliy Zhuk <zhuk2205@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code
 */

namespace Apple\ApnPush\Encoder;

use Apple\ApnPush\Model\Alert;
use Apple\ApnPush\Model\Aps;
use Apple\ApnPush\Model\Payload;

/**
 * The encoder for encode notification payload to string for next send to Apple Push Notification Service
 */
class PayloadEncoder implements PayloadEncoderInterface
{
    /**
     * {@inheritdoc}
     */
    public function encode(Payload $payload): string
    {
        $data = [
            'aps' => $this->convertApsToArray($payload->getAps()),
        ];

        $data = array_merge($payload->getCustomData(), $data);

        return json_encode($data);
    }

    /**
     * Convert APS data to array
     *
     * @param Aps $aps
     *
     * @return array
     */
    private function convertApsToArray(Aps $aps): array
    {
        $data = [
            'alert' => $this->convertAlertToArray($aps->getAlert()),
        ];

        if ($aps->getSound()) {
            $data['sound'] = $aps->getSound();
        }

        if ($aps->getBadge() !== null) {
            $data['badge'] = $aps->getBadge();
        }

        if ($aps->getCategory()) {
            $data['category'] = $aps->getCategory();
        }

        if ($aps->isContentAvailable()) {
            $data['content-available'] = 1;
        }

        if ($aps->isMutableContent()) {
            $data['mutable-content'] = 1;
        }

        if ($aps->getThreadId()) {
            $data['thread-id'] = $aps->getThreadId();
        }

        return $data;
    }

    /**
     * Convert alert object to array
     *
     * @param Alert $alert
     *
     * @return array
     */
    private function convertAlertToArray(Alert $alert): array
    {
        $data = [];

        if ($alert->getBodyLocalized()->getKey()) {
            $data['loc-key'] = $alert->getBodyLocalized()->getKey();
            $data['loc-args'] = $alert->getBodyLocalized()->getArgs();
        } else {
            $data['body'] = $alert->getBody();
        }

        if ($alert->getTitleLocalized()->getKey()) {
            $data['title-loc-key'] = $alert->getTitleLocalized()->getKey();
            $data['title-loc-args'] = $alert->getTitleLocalized()->getArgs();
        } elseif ($alert->getTitle()) {
            $data['title'] = $alert->getTitle();
        }

        if ($alert->getActionLocalized()->getKey()) {
            $data['action-loc-key'] = $alert->getActionLocalized()->getKey();
        }

        if ($alert->getLaunchImage()) {
            $data['launch-image'] = $alert->getLaunchImage();
        }

        return $data;
    }
}


<?php

namespace Spatie\MailableTest;

use Exception;
use Faker\Generator;
use Illuminate\Database\Eloquent\Model;
use Spatie\MailableTest\Exceptions\CouldNotDetermineValue;

class FakerArgumentValueProvider implements ArgumentValueProvider
{
    /** @var \Faker\Generator */
    protected $faker;

    public function __construct(Generator $faker)
    {
        $this->faker = $faker;
    }

    /**
     * @param string $mailableClass
     * @param string $argumentName
     * @param string $argumentType
     * @param string|null $defaultValue
     *
     * @return mixed
     */
    public function getValue(string $mailableClass, string $argumentName, string $argumentType = '', $defaultValue = null)
    {
        if ($argumentType === 'int') {
            return $defaultValue ?? $this->faker->numberBetween(1, 100);
        }

        if ($argumentType === 'string') {
            return $defaultValue ?? $this->faker->sentence();
        }

        if ($argumentType === 'bool') {
            $defaultValue = ($defaultValue == 'false' ? false : $defaultValue);

            return $defaultValue ?? $this->faker->boolean(50);
        }

        try {
            $argumentValue = app($argumentType);
        } catch (Exception $exception) {
            throw CouldNotDetermineValue::create($argumentType, $argumentName, $exception);
        }

        if ($argumentValue instanceof Model) {
            return $this->getModelInstance($mailableClass, $argumentName, $argumentValue, $id = $defaultValue);
        }

        return $argumentValue;
    }

    /**
     * @param string $mailableClass
     * @param string $argumentName
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param string|int|null $id
     *
     * @return \Illuminate\Database\Eloquent\Model
     * @throws \Spatie\MailableTest\Exceptions\CouldNotDetermineValue
     */
    protected function getModelInstance(string $mailableClass, string $argumentName, Model $model, $id): Model
    {
        $modelInstance = $id ? $model->find($id) : $model->first();

        if (! $modelInstance) {
            throw CouldNotDetermineValue::noModelInstanceFound($model);
        }

        return $modelInstance;
    }
}


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class ChangeSrcReferenceColumnInTranslations extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('ltm_translations', function (Blueprint $table) {
            $table->dropColumn('source');
        });

        Schema::table('ltm_translations', function (Blueprint $table) {
            $table->text('source')->nullable();
            $table->boolean('is_auto_added')->default(0);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('ltm_translations', function (Blueprint $table) {
            $table->dropColumn('source');
            $table->dropColumn('is_auto_added');
        });

        Schema::table('ltm_translations', function (Blueprint $table) {
            $table->string('source', 256)->nullable();
        });
    }
}


<?php

namespace Vsch\TranslationManager;

use Closure;
use Illuminate\Support\Facades\App;

class RouteAfterMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Closure $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        /* @var $translationManager Manager */
        $translationManager = App::make(ManagerServiceProvider::PACKAGE);

        $response = $next($request);

        $translationManager->afterRoute($request, $response);

        return $response;
    }
}


<?php

namespace Vsch\TranslationManager;

use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Events\Dispatcher;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Gate;
use Symfony\Component\Finder\Finder;
use Vsch\TranslationManager\Classes\PathTemplateResolver;
use Vsch\TranslationManager\Classes\TranslationFileRewriter;
use Vsch\TranslationManager\Models\Translation;
use Vsch\TranslationManager\Models\UserLocales;
use Vsch\TranslationManager\Repositories\Interfaces\ITranslatorRepository;
use ZipArchive;

/**
 * Class Manager
 *
 * @package Vsch\TranslationManager
 */
class Manager
{
    const INDATABASE_PUBLISH_KEY = 'indatabase_publish';
    const DEFAULT_DB_CONNECTION_KEY = 'default_connection';
    const USER_LOCALES_ENABLED = 'user_locales_enabled';
    const PDO_FETCH_MODE_ENABLED = 'pdo_fetch_mode_enabled';
    const USER_LIST_CONNECTION_KEY = 'user_list_connection';
    const DB_CONNECTIONS_KEY = 'db_connections';
    const PERSISTENT_PREFIX_KEY = 'persistent_prefix';
    const EXCLUDE_PAGE_EDIT_GROUPS_KEY = 'exclude_page_edit_groups';
    const LOG_MISSING_KEYS_KEY = 'log_missing_keys';
    const EXCLUDE_GROUPS_KEY = 'exclude_groups';
    const MISSING_KEYS_LOTTERY_KEY = 'missing_keys_lottery';
    const LOTTERY_PERSISTENT_SUFFIX = 'lottery';
    const LOG_KEY_USAGE_INFO_KEY = 'log_key_usage_info';
    const ADDITIONAL_LOCALES_KEY = 'locales';
    const SHOW_LOCALES_KEY = 'show_locales';
    const MARKDOWN_KEY_SUFFIX = 'markdown_key_suffix';

    const ABILITY_ADMIN_TRANSLATIONS = 'ltm-admin-translations';
    const ABILITY_BYPASS_LOTTERY = 'ltm-bypass-lottery';
    const ABILITY_LIST_EDITORS = 'ltm-list-editors';
    const DISABLE_REACT_UI = 'disable-react-ui';
    const DISABLE_REACT_UI_LINK = 'disable-react-ui-link';

    /** @var \Illuminate\Foundation\Application */
    protected $app;
    /** @var \Illuminate\Filesystem\Filesystem */
    protected $files;
    /** @var \Illuminate\Events\Dispatcher */
    protected $dispathesEvents;

    protected $config;
    protected $imported;

    /* @var $translation Translation */
    protected $translation;

    protected $persistentPrefix;
    protected $cache;
    protected $cacheIsDirty;
    protected $cacheTransKey;
    protected $usageCache;
    protected $usageCacheIsDirty;
    protected $usageCacheTransKey;
    protected $errors;
    protected $indatabase_publish;
    protected $default_connection;
    protected $default_translation_connection;
    protected $default_indatabase_publish;
    protected $groupList;
    protected $augmentedGroupList;
    protected $augmentedGroupReverseList;
    protected $preloadedGroupKeys;
    protected $preloadedGroup;
    protected $preloadedGroupLocales;
    protected $ltmJsonKeys;
    protected $jsonLtmKeys;

    /**
     * @var   \ZipArchive
     */
    protected $zipExporting;
    protected $jsonKeyWordSeparator;
    protected $isWebUI;
    protected $createJsonKeysForWebUI; // not used, ignored
    protected $newJsonKeyFromPrimaryLocale;
    protected $primaryLocale;

    private $package;
    private $translatorRepository;
    private $translator;

    public const JSON_GROUP = 'JSON';

    public function __construct(Application $app, Filesystem $files, Dispatcher $dispatchesEvents, ITranslatorRepository $translatorRepository)
    {
        $this->app = $app;
        $this->translatorRepository = $translatorRepository;

        $this->package = ManagerServiceProvider::PACKAGE;
        $this->config = $this->app['config'][$this->package];

        $this->files = $files;
        $this->dispathesEvents = $dispatchesEvents;
        $this->translation = $translatorRepository->getTranslation();
        $this->default_connection = $this->translation->getConnectionName();
        $this->default_translation_connection = $this->config(self::DEFAULT_DB_CONNECTION_KEY, null);
        if (!$this->default_translation_connection) {
            $this->default_translation_connection = $this->default_connection;
        } else {
            $this->setConnectionName($this->default_translation_connection);
        }

        $this->preloadedGroupKeys = null;
        $this->preloadedGroup = null;

        $this->persistentPrefix = null;
        $this->cache = null;
        $this->usageCache = null;

        $this->indatabase_publish = $this->getConnectionInDatabasePublish($this->default_translation_connection);

        $this->groupList = null;
        $this->augmentedGroupList = null;
    }

    public function getRepository()
    {
        return $this->translatorRepository;
    }

    public function setConnectionName($connection = null)
    {
        if ($this->isDefaultTranslationConnection($connection)) {
            // resetting to default
            $connection = $this->default_translation_connection;
        }

        $this->translation->setConnection($connection);
        $this->indatabase_publish = $this->getConnectionInDatabasePublish($connection);

        $this->resetCache();
    }

    public function getNormalizedConnectionName($connection)
    {
        if ($this->isDefaultTranslationConnection($connection)) {
            $connectionName = '';
        } else {
            $connectionName = $connection;
        }

        return $connectionName;
    }

    public function getResolvedConnectionName($connection)
    {
        if ($this->isDefaultTranslationConnection($connection)) {
            $connectionName = $this->default_translation_connection ?: '';
        } else {
            $connectionName = $connection;
        }

        return $connectionName;
    }

    public function getConnectionName()
    {
        $connectionName = $this->translation->getConnectionName();
        return $connectionName;
    }

    /**
     * @param $connection
     *
     * @return bool
     */
    public function isDefaultTranslationConnection($connection)
    {
        return $connection == null || $connection == '' || $connection == $this->default_translation_connection;
    }

    /**
     * @return \Vsch\TranslationManager\Models\Translation
     */
    public function getTranslation()
    {
        return $this->translation;
    }

    public function getConnectionInDatabasePublish($connectionName)
    {
        if ($connectionName === null || $connectionName === '' || $this->isDefaultTranslationConnection($connectionName)) {
            return $this->config(self::INDATABASE_PUBLISH_KEY, 0);
        }
        return $this->getConnectionInfo($connectionName, self::INDATABASE_PUBLISH_KEY, $this->config(self::INDATABASE_PUBLISH_KEY, 0));
    }

    public function getUserListConnection($connectionName)
    {
        if ($connectionName === null || $connectionName === '' || $this->isDefaultTranslationConnection($connectionName)) {
            // use the default connection for the user list
            return '';
        }

        $userListConnection = $this->getConnectionInfo($connectionName, self::USER_LIST_CONNECTION_KEY, $connectionName);
        if (!$userListConnection) $userListConnection = $connectionName;
        return $userListConnection;
    }

    public function getUserListProvider($connection)
    {
        return function ($user, $connection_name, &$user_list) {
            return Gate::forUser($user)->allows(self::ABILITY_LIST_EDITORS, [$connection_name, &$user_list]);
        };
    }

    public function getUserLocalesTableName()
    {
        $userLocales = new UserLocales();
        $prefix = $this->translatorRepository->getConnection()->getTablePrefix();
        return $prefix . $userLocales->getTable();
    }

    public function getConnectionInfo($connectionName, $key = null, $default = null)
    {
        if ($key === null) {
            return $this->config(self::DB_CONNECTIONS_KEY);
        }

        $db_connections = $this->config(self::DB_CONNECTIONS_KEY);
        $environment = App::environment();

        $db_connection = $connectionName !== null && array_key_exists($environment, $db_connections) && array_key_exists($connectionName, $db_connections[$environment]) ? $db_connections[$environment][$connectionName] : null;

        $value = $db_connection && array_key_exists($key, $db_connection)
            ? $db_connection[$key]
            : $default;

        return $value;
    }

    /**
     * @return bool
     */
    public function inDatabasePublishing()
    {
        return $this->zipExporting ? 3 : (int)$this->indatabase_publish;
    }

    /**
     * @return bool
     */
    public function areUserLocalesEnabled()
    {
        return $this->config(self::USER_LOCALES_ENABLED, false);
    }

    function firstOrNewTranslation($attributes = null)
    {
        $checkDB = true;

        /* @var $query Builder */

        $translation = null;

        if ($this->preloadedGroupKeys && array_key_exists('group', $attributes) && $this->preloadedGroup === $attributes['group']
            && array_key_exists('locale', $attributes) && array_key_exists('key', $attributes)
            && array_key_exists($attributes['locale'], $this->preloadedGroupLocales)
        ) {
            $checkDB = false;

            if (array_key_exists($attributes['key'], $this->preloadedGroupKeys)
                && array_key_exists($attributes['locale'], $this->preloadedGroupKeys[$attributes['key']])
            ) {
                $translation = $this->preloadedGroupKeys[$attributes['key']][$attributes['locale']];
            }
        }

        if ($checkDB) {
            $query = $this->translation->on($this->getConnectionName());

            foreach ($attributes as $attribute => $value) {
                if ($attribute === 'locale' || $attribute === 'group' || $attribute === 'key') {
                    $query = $query->where($attribute, $value);
                }
            }

            $translation = $query->first();
        }

        if (!$translation) {
            $translation = new Translation();
            $translation->fill($attributes);
            $translation->setConnection($this->getConnectionName());

            // put it in the cache as empty so we don't hit the database every time
            $this->cacheTranslation('', $translation->group, $translation->key, null, $translation->locale);
        }

        return $translation;
    }

    function firstOrCreateTranslation($attributes = null)
    {
        $translation = $this->firstOrNewTranslation($attributes);
        if (!$translation->exists) {
            $translation->save();

            // put it in the cache as empty so we don't hit the database every time
            $this->cacheTranslation('', $translation->group, $translation->key, null, $translation->locale);
        }

        return $translation;
    }

    public function cacheGroupTranslations($group, $locales, $translations)
    {
        $this->preloadedGroupKeys = $translations;
        $this->preloadedGroup = $group;
        $this->preloadedGroupLocales = array_combine($locales, $locales);
    }

    public function getPackagePublicPath()
    {
        return $this->app->publicPath() . $this->getPackagePublicSuffix();
    }

    public function getPackagePublicSuffix()
    {
        return '/vendor/' . $this->package;
    }

    public function afterRoute($request, $response)
    {
        // only save cache if it is default connection
        if ($this->isDefaultTranslationConnection($this->getConnectionName())) {
            $this->saveCache();
            $this->saveUsageCache();
        }
    }

    public function getGroupList()
    {
        if ($this->groupList === null) {
            // read it in
            /* @var Builder $groups */
            $groups = $this->getTranslation()->groupBy('group');
            $excludedGroups = $this->config(Manager::EXCLUDE_GROUPS_KEY);
            if ($excludedGroups) {
                $groups = $groups->whereNotIn('group', $excludedGroups);
            }
            //            $groups = $groups->where('group','<>', self::JSON_GROUP);

            $this->groupList = ManagerServiceProvider::getLists($groups->pluck('group', 'group'));
        }
        return $this->groupList;
    }

    public function getAugmentedGroup($namespace, $group)
    {
        $augmentedGroup = $group;

        if ($namespace && $namespace != '*') {
            $augmentedGroup = "$namespace::$group";
            if (array_key_exists($augmentedGroup, $this->getGroupAugmentedList())) {
                $augmentedGroup = $this->augmentedGroupList[$augmentedGroup];
            } else {
                $augmentedGroup = $group;
            }
        }
        return $augmentedGroup;
    }

    public function getGroupAugmentedList()
    {
        if ($this->augmentedGroupList === null) {
            // compute augmented list from vnd:{vendor}.{package}::group.key
            // remove the vnd:{vendor}. and add to augmented, map it to original name
            $groupList = $this->getGroupList();
            $this->augmentedGroupList = [];
            $this->augmentedGroupReverseList = [];

            foreach ($groupList as $group) {
                if (starts_with($group, ["vnd:", "wbn:"])) {
                    // we need this one
                    $parts = explode('.', $group, 2);
                    if (count($parts) === 2) {
                        // if it is not included in the vendor resources
                        if (!array_key_exists($parts[1], $groupList)) {
                            $this->augmentedGroupList[$parts[1]] = $group;
                        }
                        $this->augmentedGroupReverseList[$group] = $parts[1];
                    }
                }
            }
        }
        return $this->augmentedGroupList;
    }

    public function getGroupAugmentedReverseList()
    {
        $this->getGroupAugmentedList();
        return $this->augmentedGroupReverseList;
    }

    public function getJsonKeyWordSeparator()
    {
        if (!isset($this->jsonKeyWordSeparator)) {
            $string = $this->config('new-json-keys-separator', '-');
            $this->jsonKeyWordSeparator = substr($string, 0, 1);
        }
        return $this->jsonKeyWordSeparator;
    }

    public function config($key = null, $default = null)
    {
        // Version 5.1
        if (!$this->config) {
            $this->config = $this->app['config'][$this->package];
        }
        // Version 4.2
        //if (!$this->config) $this->config = $this->app['config'][$this->package.'::config'];

        if ($key === null) {
            return $this->config;
        }
        if (array_key_exists($key, $this->config)) {
            return $this->config[$key];
        }
        return $default;
    }

    public function cacheEnabled()
    {
        return $this->cachePrefix() !== '';
    }

    public function cachePrefix()
    {
        if ($this->persistentPrefix === null) {
            if (array_key_exists(self::PERSISTENT_PREFIX_KEY, $this->config())) {
                $this->persistentPrefix = $this->config(self::PERSISTENT_PREFIX_KEY);
            } else {
                $this->persistentPrefix = '';
            }
        }

        if ($this->cacheTransKey == null) {
            $this->cacheTransKey = $this->persistentPrefix ? $this->persistentPrefix . 'translations' : '';
        }
        return $this->persistentPrefix;
    }

    public function usageCachePrefix()
    {
        if ($this->usageCacheTransKey == null) {
            $this->cachePrefix();
            $this->usageCacheTransKey = $this->persistentPrefix ? $this->persistentPrefix . 'usage_info' : '';
        }
        return $this->persistentPrefix;
    }

    public function cache()
    {
        if ($this->cache === null) {
            if ($this->isDefaultTranslationConnection($this->getConnectionName())) {
                $this->cache = $this->cachePrefix() !== '' && $this->indatabase_publish != 0 && Cache::has($this->cacheTransKey) ? Cache::get($this->cacheTransKey) : [];
                $this->cacheIsDirty = $this->persistentPrefix !== '' && !Cache::has($this->cacheTransKey);
            } else {
                $this->cache = [];   // only default connections use cache
            }
        }
        return $this->cache;
    }

    public function usageCache()
    {
        if ($this->usageCache === null) {
            if ($this->isDefaultTranslationConnection($this->getConnectionName())) {
                $this->usageCache = $this->usageCachePrefix() !== '' && Cache::has($this->usageCacheTransKey) ? Cache::get($this->usageCacheTransKey) : [];
                $this->usageCacheIsDirty = $this->persistentPrefix !== '' && !Cache::has($this->usageCacheTransKey);
            } else {
                $this->usageCache = [];
            }
        }
        return $this->usageCache;
    }

    public function saveCache()
    {
        if ($this->isDefaultTranslationConnection($this->getConnectionName())) {
            if ($this->persistentPrefix && $this->cacheIsDirty) {
                Cache::put($this->cacheTransKey, $this->cache, 60 * 24 * 365);
                $this->cacheIsDirty = false;
            }
        }
    }

    public function saveUsageCache()
    {
        if ($this->isDefaultTranslationConnection($this->getConnectionName())) {
            if ($this->persistentPrefix && $this->usageCacheIsDirty) {
                // we never save it in the cache, it is only in database use, otherwise every page it will save the full cache to the database
                // instead of only the accessed keys
                //Cache::put($this->usageCacheTransKey, $this->usageCache, 60 * 24 * 365);
                Cache::put($this->usageCacheTransKey, [], 60 * 24 * 365);
                $this->usageCacheIsDirty = false;

                // now update the keys in the database
                foreach ($this->usageCache as $group => $keys) {
                    $this->translatorRepository->updateUsedTranslationsForGroup($keys, $group);
                }
            }
        }
    }

    public function clearCache($groups = null)
    {
        if (!$groups || $groups === '*') {
            $this->cache = [];
            $this->cacheIsDirty = !!$this->persistentPrefix;
        } else if ($this->cache()) {
            if (!is_array($groups)) $groups = [$groups];

            foreach ($groups as $group) {
                if (array_key_exists($group, $this->cache)) {
                    unset($this->cache[$group]);
                    $this->cacheIsDirty = !!$this->persistentPrefix;
                }
            }
        }
    }

    public function resetCache()
    {
        if ($this->isDefaultTranslationConnection($this->getConnectionName())) {
            $this->cache = null; // default means we need to use the cache
        } else {
            $this->cache = [];
            $this->cacheIsDirty = false;
        }
    }

    public function clearUsageCache($clearDatabase, $groups = null)
    {
        if (!$groups || $groups === '*') {
            $this->usageCache();
            $this->usageCache = [];
            $this->usageCacheIsDirty = true;
            $this->saveUsageCache();

            if ($clearDatabase) {
                $this->translatorRepository->setNotUsedForAllTranslations();
            }
        } else if ($this->usageCache()) {
            $this->usageCache();
            if (!is_array($groups)) {
                $groups = [$groups];
            }

            foreach ($groups as $group) {
                if (array_key_exists($group, $this->usageCache)) {
                    unset($this->usageCache[$group]);
                    $this->usageCacheIsDirty = !!$this->persistentPrefix;
                }

                if ($clearDatabase) {
                    $this->translatorRepository->updateUsedTranslationsForGroup(null, $group, 0);
                }
            }

            $this->saveUsageCache();
        }
    }

    public function cacheKey($key, $locale)
    {
        return $locale . ':' . $key;
    }

    public function usageCacheKey($key, $locale)
    {
        return $key;
    }

    protected static function groupKeyList($key)
    {
        $key = explode('.', $key, 2);
        if (count($key) > 1) {
            $group = self::fixGroup($key[0]);
            $key = $key[1];
        } else {
            $group = '';
            $key = $key[0];
        }
        return [$group, $key];
    }

    public function cacheTranslation($namespace, $group, $transKey, $value, $locale)
    {
        $group = self::fixGroup($group);
        $group = $namespace && $namespace !== '*' ? $namespace . '::' . $group : $group;

        if ($group) {
            if (!array_key_exists($group, $this->cache())) {
                $this->cache[$group] = [];
            }
            $this->cache[$group][$this->cacheKey($transKey, $locale)] = $value;
            $this->cacheIsDirty = $this->persistentPrefix !== '';
        }
    }

    /**
     * @param $namespace
     * @param $group
     * @param $transKey
     * @param $locale
     *
     * @return string|null translation value
     */
    public function cachedTranslation($namespace, $group, $transKey, $locale)
    {
        $group = self::fixGroup($group);
        $group = $namespace && $namespace !== '*' ? $namespace . '::' . $group : $group;

        $cacheKey = $this->cacheKey($transKey, $locale);
        $value = $group && array_key_exists($group, $this->cache()) && array_key_exists($cacheKey, $this->cache[$group]) ? $this->cache[$group][$cacheKey] : null;
        return $value;
    }

    /**
     * @param $namespace
     * @param $group
     * @param $locale
     * @param $translations array|null  translations to override with cached translations
     *
     * @return array   of key=>translation of cached translations for the locale
     */
    public function cachedTranslations($namespace, $group, $locale, $translations = null)
    {
        $group = self::fixGroup($group);
        $group = $namespace && $namespace !== '*' ? $namespace . '::' . $group : $group;
        $translations = $translations ?: [];

        $values = $group && array_key_exists($group, $this->cache()) ? $this->cache[$group] : null;
        if ($values) {
            $localePrefix = "$locale:";
            $prefixLen = strlen($localePrefix);
            foreach ($values as $key => $translation) {
                if (str_starts_with($key, $localePrefix)) {
                    $transKey = substr($key, $prefixLen);
                    $translations[$transKey] = $translation;
                }
            }
        }

        return $translations;
    }

    public function cacheUsageInfo($namespace, $group, $transKey, $value, $locale)
    {
        $group = self::fixGroup($group);
        $group = $namespace && $namespace !== '*' ? $namespace . '::' . $group : $group;

        if ($group) {
            if (!array_key_exists($group, $this->usageCache())) {
                $this->usageCache[$group] = [];
            }
            $this->usageCache[$group][$this->usageCacheKey($transKey, $locale)] = $value;
            $this->usageCacheIsDirty = $this->persistentPrefix !== '';
        }
    }

    public function cachedUsageInfo($namespace, $group, $transKey, $locale)
    {
        $group = self::fixGroup($group);
        $group = $namespace && $namespace !== '*' ? $namespace . '::' . $group : $group;

        $cacheKey = $this->usageCacheKey($transKey, $locale);
        $value = $group && array_key_exists($group, $this->usageCache()) && array_key_exists($cacheKey, $this->usageCache[$group]) ? $this->usageCache[$group][$cacheKey] : null;
        return $value;
    }

    public function excludedPageEditGroup($group)
    {
        return
            in_array($group, $this->config(self::EXCLUDE_PAGE_EDIT_GROUPS_KEY, []))
            || in_array($group, $this->config(self::EXCLUDE_GROUPS_KEY, []));
    }

    public static function fixGroup($group)
    {
        if ($group !== null) {
            $group = str_replace('/', '.', $group);
        }
        return $group;
    }

    // this is called to tell Missing Keys that the keys being passed in are LTM keys not jSON keys
    public function setWebUI($createJsonKeys = null)
    {
        $this->isWebUI = true;
        $this->createJsonKeysForWebUI = $createJsonKeys;
    }

    /**
     * @param             $namespace string
     * @param             $group     string
     * @param             $key       string
     * @param null|string $locale
     * @param bool $useLottery
     * @param bool $findOrNew
     *
     * @return \Vsch\TranslationManager\Models\Translation|null
     */
    public function missingKey($namespace, $group, $key, $locale = null, $useLottery = true, $findOrNew = false)
    {
        if (!$useLottery || $this->config(self::LOG_MISSING_KEYS_KEY)) {
            // L5 changes
            $group = self::fixGroup($group);
            $group = $namespace && $namespace !== '*' ? $namespace . '::' . $group : $group;
            if (!in_array($group, $this->config(self::EXCLUDE_GROUPS_KEY))) {
                $lottery = 1;
                if ($useLottery && $this->config(self::MISSING_KEYS_LOTTERY_KEY) !== 1) {
                    $lottery = \Session::get($this->config(self::PERSISTENT_PREFIX_KEY) . self::LOTTERY_PERSISTENT_SUFFIX, '');
                    if ($lottery === '') {
                        $lottery = rand(1, $this->config(self::MISSING_KEYS_LOTTERY_KEY));
                        \Session::put($this->config(self::PERSISTENT_PREFIX_KEY) . self::LOTTERY_PERSISTENT_SUFFIX, $lottery);
                    }
                }

                if ($lottery == 1) {
                    // here need to map a local group to wbn: or vnd: package if the local file does not already exist so that
                    // new keys will be added to the appropriate package
                    $augmentedGroupList = $this->getGroupAugmentedList();
                    $augmentedGroup = $group;
                    if (array_key_exists($group, $augmentedGroupList)) {
                        $augmentedGroup = $augmentedGroupList[$group];
                    }

                    $locale = $locale ?: ($this->translator ?: ($this->translator = $this->app->make('translator')))->getLocale();

                    if ($findOrNew) {
                        $translation = $this->firstOrNewTranslation(array(
                            'locale' => $locale,
                            'group' => $augmentedGroup,
                            'key' => $key,
                        ));
                    } else {
                        $translation = $this->firstOrCreateTranslation(array(
                            'locale' => $locale,
                            'group' => $augmentedGroup,
                            'key' => $key,
                        ));
                    }

                    if ($group === 'JSON' && $locale !== 'json') {
                        // we create a key if one does not exist since we know what the json key's value should be

                        if (!$this->isWebUI) {
                            $this->loadLtmJsonKeys(false);
                            if (!array_key_exists($key, $this->jsonLtmKeys)) {
                                $ltmKey = $this->generateLtmKey($this->jsonLtmKeys, $key);

                                $jsonTranslation = $this->firstOrCreateTranslation(array(
                                    'locale' => 'json',
                                    'group' => $augmentedGroup,
                                    'key' => $ltmKey,
                                    'value' => $key,
                                    'saved_value' => $key,
                                    'status' => Translation::STATUS_CHANGED,
                                ));

                                $this->ltmJsonKeys[$ltmKey] = $key;
                                $this->jsonLtmKeys[$key] = $ltmKey;
                            }
                            //                        } else if ($this->createJsonKeysForWebUI) {
                            //                            $this->loadLtmJsonKeys(false);
                            //                            if (!array_key_exists($key, $this->ltmJsonKeys)) {
                            //                                $ltmKey = $key;
                            //
                            //                                $jsonTranslation = $this->firstOrCreateTranslation(array(
                            //                                    'locale' => 'json',
                            //                                    'group' => $augmentedGroup,
                            //                                    'key' => $ltmKey,
                            //                                    'value' => $key,
                            //                                    'saved_value' => $key,
                            //                                    'status' => Translation::STATUS_CHANGED,
                            //                                ));
                            //
                            //                                $this->ltmJsonKeys[$ltmKey] = $key;
                            //                                $this->jsonLtmKeys[$key] = $ltmKey;
                            //                            }
                        }
                    }

                    /* @var $translation \Vsch\TranslationManager\Models\Translation */
                    return $translation;
                }
            }
        }
        return null;
    }

    /**
     * @param             $namespace string
     * @param             $group     string
     * @param             $key       string
     * @param null|string $locale
     * @param bool $useLottery
     */
    public
    function usingKey($namespace, $group, $key, $locale, $useLottery)
    {
        if ($this->config(self::LOG_KEY_USAGE_INFO_KEY)) {
            $group = self::fixGroup($group);
            $group = $namespace && $namespace !== '*' ? $namespace . '::' . $group : $group;

            if (!in_array($group, $this->config(self::EXCLUDE_GROUPS_KEY))) {
                $lottery = 1;
                if ($useLottery && $this->config(self::MISSING_KEYS_LOTTERY_KEY) !== 1) {
                    $lottery = \Session::get($this->config(self::PERSISTENT_PREFIX_KEY) . self::LOTTERY_PERSISTENT_SUFFIX, '');
                    if ($lottery === '') {
                        $lottery = rand(1, $this->config(self::MISSING_KEYS_LOTTERY_KEY));
                        \Session::put($this->config(self::PERSISTENT_PREFIX_KEY) . self::LOTTERY_PERSISTENT_SUFFIX, $lottery);
                    }
                }

                if ($lottery == 1) {
                    $locale = $locale ?: ($this->translator ?: ($this->translator = $this->app->make('translator')))->getLocale();
                    $this->cacheUsageInfo('', $group, $key, 1, $locale);
                }
            }
        }
    }

    /**
     * @param $locale
     * @param $db_group
     * @param $translations
     * @param $replace
     */
    protected
    function importTranslationFile($locale, $db_group, $translations, $replace)
    {
        $connectionName = $this->getConnectionName();
        $dbTranslations = $this->translatorRepository->selectTranslationsByLocaleAndGroup($locale, $db_group);
        $inDatabasePublishing = $this->inDatabasePublishing();

        // we are either in-database publishing or writing the files but pretending that we are for a remote
        // connection which is really limited to do in-database publishing only
        $inDatabasePublishing = $inDatabasePublishing === 1 || $inDatabasePublishing === 2 ? $inDatabasePublishing : 0;

        // isLocal in this case means we are importing files local to the server
        // otherwise their values will not reflect what is local to the server
        $isLocal = $this->isDefaultTranslationConnection($this->getConnectionName()) && $inDatabasePublishing != 2;

        $timeStamp = 'now()';
        $dbTransMap = [];
        $dbTranslations->each(function ($trans) use (&$dbTransMap, $connectionName) {
            $dbTransMap[$trans->key] = $trans;
        });

        $values = [];
        $statusChangeOnly = [];
        foreach ($translations as $key => $value) {
            if (!$value) {
                // must have been manually modified file to empty string
                continue;
            }
            if (is_array($value)) {
                if ($value) $this->errors[] = "translation value is an array: $db_group.$key locale $locale";
                continue;
            }

            $value = (string)$value;

            if (array_key_exists($key, $dbTransMap)) {
                $translation = $dbTransMap[$key];
                unset($dbTransMap[$key]);
            } else {
                $translation = new Translation(array(
                    'locale' => $locale,
                    'group' => $db_group,
                    'key' => $key,
                ));

                $translation->setConnection($connectionName);
            }

            $translation->is_deleted = 0;
            $translation->is_auto_added = 0;

            $status = (int)$translation->status;

            // replace if current translation has no value or we want to replace existing values
            if ($replace || !$translation->value) {
                $translation->value = $value;
            }

            $previousSaved = $translation->saved_value;
            $translation->saved_value = $value;

            if ($isLocal || $locale === 'json') {
                $status = $translation->saved_value === $translation->value ? Translation::STATUS_SAVED : Translation::STATUS_CHANGED;
            } else {
                if ($translation->value === $translation->saved_value) {
                    if ($value === $previousSaved) {
                        // if it was saved then it is still reflecting the file, otherwise consider it cached.
                        if ($status !== Translation::STATUS_SAVED) $status = Translation::STATUS_SAVED_CACHED;
                    } else {
                        // the file and database are the same, but the database may no longer reflect the file
                        $status = Translation::STATUS_SAVED_CACHED;
                    }
                } else {
                    $status = Translation::STATUS_CHANGED;
                }
            }

            if ($status !== (int)$translation->status) {
                $translation->status = $status;
            }

            if (!$translation->exists) {
                $values[] = $this->translatorRepository->getInsertTranslationsElement($translation, $timeStamp);
            } else if ($translation->isDirty()) {
                if ($translation->isDirty(['value', 'source', 'saved_value', 'was_used',])) {
                    $translation->save();
                } else {
                    if (!array_key_exists($translation->status, $statusChangeOnly)) {
                        $statusChangeOnly[$translation->status] = $translation->id;
                    } else {
                        $statusChangeOnly[$translation->status] .= ',' . $translation->id;
                    }
                }
            }

            $this->imported++;
        }

        // now batch update those with status changes only
        $updated_at = new Carbon();
        foreach ($statusChangeOnly as $status => $translationIds) {
            $this->translatorRepository->updateStatusForTranslations($status, $updated_at, $translationIds);
        }

        // now process all the new translations that were not in the files
        if ($replace == 2) {
            // we delete all translations that were not in the files
            if ($dbTransMap) {
                $translationIds = '';
                foreach ($dbTransMap as $translation) {
                    $translationIds .= ',' . $translation->id;
                }

                $translationIds = trim_prefix($translationIds, ',');
                $this->translatorRepository->deleteTranslationsForIds($translationIds);
            }
        } else {
            // these are no-ops, we don't delete
        }

        if ($values) {
            try {
                $this->translatorRepository->insertTranslations($values);
            } catch (\Exception $e) {
                $tmp = 0;
            }
        }
    }

    private
    function generateLtmKey($jsonLtmKeys, $jsonKey, $maxJsonKey = null)
    {
        // replace all symbols and spaces with _, remove duplicate _ and use that if it does not already exist
        // if it exists we append _# until we get a unique key
        if (!$maxJsonKey) {
            $maxJsonKey = $this->config('json_dbkey_length', 32);
            if ($maxJsonKey > 120) $maxJsonKey = 120;
        }

        $ltmKey = mb_strtolower($jsonKey);
        $iMax = strlen($ltmKey);
        $newKey = "";
        $hadUnderscore = null;
        $jsonKeyWordSeparator = $this->getJsonKeyWordSeparator();
        for ($i = 0; $i < $iMax; $i++) {
            $c = mb_substr($ltmKey, $i, 1);
            if (preg_match("/[\\p{L}0-9]/", $c)) {
                if ($hadUnderscore) $newKey .= $this->jsonKeyWordSeparator;
                $newKey .= $c;
                $hadUnderscore = false;
            } else if (!$hadUnderscore) {
                $hadUnderscore = true;
            }
            if (strlen($newKey) >= $maxJsonKey) {
                break;
            }
        }
        // either messes up unicode if using preg_replace, or does not recognize char class if using mb_ereg_replace
        //$ltmKey = mb_eregi_replace("/[^\\p{L}0-9]/", "_", $ltmKey);
        //$ltmKey = preg_replace("/_+/", "_", $ltmKey);
        //$ltmKey = substr($ltmKey, 0, 120);
        $ltmKey = $newKey;
        $suffix = "";
        $count = 0;
        while (array_key_exists($ltmKey . $suffix, $jsonLtmKeys)) {
            $suffix = $this->jsonKeyWordSeparator . ++$count;
        }

        return $ltmKey . $suffix;
    }

    private
    function loadLtmJsonKeys($loadAllKeys = false, $ignoreDeleted = false, $loadValues = false)
    {
        if (!isset($this->ltmJsonKeys)) {
            $newKeyFromPrimary = $this->isNewJsonKeyFromPrimaryLocale();

            $q = $this->translation->query()
                ->where('group', '=', self::JSON_GROUP);

            if ($newKeyFromPrimary && $loadAllKeys) {
                $primaryLocale = $this->getPrimaryLocale();
                $q = $q
                    ->whereIn('locale', ['json', $primaryLocale]);
            } else {
                $q = $q
                    ->where('locale', '=', 'json');
            }

            if ($loadValues) {
                $q = $q
                    ->whereNotNull('value');
            } else {
                $q = $q
                    ->whereNotNull('saved_value');
            }

            if ($ignoreDeleted) {
                $q = $q
                    ->where('is_deleted', 0);
            }

            $q = $q
                ->get([
                    'key',
                    $loadValues ? 'value' : 'saved_value',
                ]);

            $this->ltmJsonKeys = [];
            $this->jsonLtmKeys = [];
            $primaryLocaleValues = [];
            $q->each(function ($tr) use ($loadValues, &$primaryLocaleValues) {
                $value = $loadValues ? $tr->value : $tr->saved_value;
                $key = $tr->key;
                if ($key === 'json') {
                    $this->ltmJsonKeys[$key] = $value;
                    $this->jsonLtmKeys[$value] = $key;
                } else {
                    $primaryLocaleValues[$key] = $value;
                }
            });

            if ($loadAllKeys) {
                $q = $this->translation->query()
                    ->where('group', '=', self::JSON_GROUP)
                    ->where('locale', '<>', 'json');

                if ($ignoreDeleted) {
                    $q = $q
                        ->where('is_deleted', 0);
                }

                if ($loadValues) {
                    $q = $q
                        ->whereNotNull('value');
                } else {
                    $q = $q
                        ->whereNotNull('saved_value');
                }

                $q = $q
                    ->groupBy(['key'])
                    ->get([
                        'key',
                    ]);

                $q->each(function ($tr) use ($primaryLocaleValues) {
                    $key = $tr->key;
                    if (!array_key_exists($key, $this->ltmJsonKeys)) {
                        $jsonKey = array_key_exists($key, $primaryLocaleValues) ? $primaryLocaleValues[$key] : $key;
                        $this->ltmJsonKeys[$key] = $jsonKey;
                        $this->jsonLtmKeys[$jsonKey] = $key;
                    }
                });
            }
        }
    }

    public
    function getTranslations($namespace, $group, $locale, $includeMissing = true, $useUnpublished = false, $mergeTranslations = null)
    {
        $group = self::fixGroup($group);
        $group = $namespace && $namespace !== '*' ? $namespace . '::' . $group : $group;

        // may need to create new jsonKeys from default locale values for any that are missing from json locale of JSON group
        $query = $this->translation->query()
            ->where('group', '=', $group)
            ->where('locale', '=', $locale);

        if (!$includeMissing) {
            if ($useUnpublished) {
                $query = $query->whereNotNull('value');
            } else {
                $query = $query->whereNotNull('saved_value');
            }
        }

        $translations = $mergeTranslations ?: [];

        if ($useUnpublished) {
            $jsonTranslations = $query->get([
                'key',
                'value',
                'is_deleted',
            ]);

            $jsonTranslations->each(function ($tr) use ($namespace, $group, $locale, &$translations, $includeMissing) {
                if ($tr->is_deleted) {
                    if ($includeMissing) {
                        $translations[$tr->key] = null;
                    } else {
                        if (array_key_exists($tr->key, $translations)) {
                            unset($translations[$tr->key]);
                        }
                    }
                } else {
                    $translations[$tr->key] = $tr->value;
                }
            });
        } else {
            $jsonTranslations = $query->get([
                'key',
                'saved_value',
            ]);

            $jsonTranslations->each(function ($tr) use ($namespace, $group, $locale, &$translations) {
                $translations[$tr->key] = $tr->saved_value;
            });
        }

        return $translations;
    }

    public
    function importTranslations($replace, $groups = null)
    {
        // this can come from the command line
        if (is_array($groups)) {
            foreach ($groups as &$group) {
                $group = self::fixGroup($group);
            }
        } else {
            $groups = self::fixGroup($groups);
        }

        // if we don't track usage information and replace == 2 we can truncate the translations for the groups or group
        if (!$this->config(self::LOG_KEY_USAGE_INFO_KEY) && $replace == 2) {
            $this->truncateTranslations($groups);
        }

        $this->imported = 0;
        $this->clearCache($groups);
        $this->clearUsageCache(false, $groups);

        $pathTemplateResolver = new PathTemplateResolver($this->files, $this->app->basePath(), $this->config('language_dirs'), '5');

        $langFiles = $pathTemplateResolver->langFileList();

        // generate JSON to LTM keys here and put them in the JSON group as json locale
        $jsonFiles = [];

        foreach ($langFiles as $langFile => $vars) {
            $locale = $vars['{locale}'];
            $db_group = $vars['{db_group}'];
            if (in_array($db_group, $this->config(self::EXCLUDE_GROUPS_KEY))) continue;
            if ($db_group === self::JSON_GROUP) {
                $json = file_get_contents($langFile);
                if ($locale !== 'json') {
                    $jsonFiles[$langFile] = json_decode($json, true);
                }
            }
        }

        $jsonTranslations = []; // locale => [ ltmKey => jsonTranslation ]
        $ltmJsonKeys = [];
        if ($jsonFiles) {
            $maxJsonKey = $this->config('json_dbkey_length', 32);
            if ($maxJsonKey > 120) $maxJsonKey = 120;

            // get only existing keys, any missing mapping may come from the imported files
            $this->loadLtmJsonKeys();
            $this->config(); // init config and sep for json keys

            foreach ($jsonFiles as $langFile => $json) {
                // create unique keys based on json keys
                $vars = $langFiles[$langFile];
                $locale = $vars['{locale}'];
                $translations = [];
                foreach ($json as $jsonKey => $translation) {
                    if (!array_key_exists($jsonKey, $this->jsonLtmKeys)) {
                        $ltmKey = $this->generateLtmKey($this->ltmJsonKeys, $jsonKey, $maxJsonKey);
                        $this->jsonLtmKeys[$jsonKey] = $ltmKey;
                        $this->ltmJsonKeys[$ltmKey] = $jsonKey;
                        $ltmJsonKeys[$ltmKey] = $jsonKey;
                    } else {
                        $ltmKey = $this->jsonLtmKeys[$jsonKey];
                        $ltmJsonKeys[$ltmKey] = $jsonKey;
                    }

                    // now store the translation for the locale with the ltmKey
                    $translations[$ltmKey] = $translation;
                }

                // save it, if we need it
                $jsonTranslations[$locale] = $translations;
            }

            // need to update database JSON group json locale with key map of ltmKey to jsonKey for exporting, use the same replace level
            $this->importTranslationFile('json', self::JSON_GROUP, $ltmJsonKeys, $replace);
        }

        if ($groups !== null) {
            // now we can filter to the list of given groups or
            $files = [];
            if (!is_array($groups)) $groups = array($groups);
            $groups = array_combine($groups, $groups);

            foreach ($langFiles as $file => $values) {
                if (array_key_exists($values['{db_group}'], $groups)) {
                    $files[$file] = $values;
                }
            }

            $langFiles = $files;
        }

        foreach ($langFiles as $langFile => $vars) {
            $locale = $vars['{locale}'];
            $db_group = $vars['{db_group}'];
            if ($locale == 'json') continue; // don't import keys. Use Database ones
            if (in_array($db_group, $this->config(self::EXCLUDE_GROUPS_KEY))) continue;
            if ($db_group === self::JSON_GROUP) {
                // just update the locale with translations, keys are already LTM keys here
                $translations = $jsonTranslations[$locale];
            } else {
                $translations = array_dot(include($langFile));
            }
            $this->importTranslationFile($locale, $db_group, $translations, $replace);
        }

        return $this->imported;
    }

    public
    function findTranslations($path = null)
    {
        // functions is a replacement variable in $pattern
        /** @noinspection PhpUnusedLocalVariableInspection */
        $functions = config('laravel-translation-manager.find.functions');
        $pattern = implode('', config('laravel-translation-manager.find.pattern'));

        // Find all PHP + Twig files in the app folder, except for storage
        $paths = $path ? [$path] : array_merge([$this->app->basePath() . '/app'], $this->app['config']['view']['paths']);
        $keys = array();
        foreach ($paths as $path) {
            $finder = new Finder();
            $finder->in($path)->name("*.{" . implode(',', config('laravel-translation-manager.find.files')) . "}")->files();

            /** @var \Symfony\Component\Finder\SplFileInfo $file */
            foreach ($finder as $file) {
                // Search the current file for the pattern
                $fileContents = $file->getContents();
                if (preg_match_all("/$pattern/siU", $fileContents, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE)) {
                    // Get all matches
                    $fileLines = null;
                    foreach ($matches[3] as $index => $key) {
                        $quote = $matches[2][$index][0];
                        $keyValue = $key[0];
                        if ($quote == '\'' && !str_contains($keyValue, ["\"", "'", "->",]) ||
                            $quote == '"' && !str_contains($keyValue, ["$", "\"", "'", "->",])
                        ) {
                            if ($fileLines == null) {
                                $fileLines = self::computeFileLines($fileContents);
                            }
                            $keys[$keyValue][$file->getPath() . '/' . $file->getFilename()][] = self::offsetLine($fileLines, $key[1]);
                        }
                    }
                }
            }
        }

        // Add the translations to the database, if not existing.
        $count = 0;
        foreach ($keys as $key => $filePathsAndLocation) {
            // Split the group and item
            list($group, $item) = explode('.', $key, 2);

            $translation = $this->missingKey('', $group, $item, null, false, true);

            // create references
            $paths = '';
            $basePath = $this->app->basePath();
            $basePathLen = strlen($basePath);
            foreach ($filePathsAndLocation as $filePath => $locations) {
                $relativePath = substr($filePath, $basePathLen);
                $paths .= $relativePath . ':' . implode(',', $locations) . "\n";
            }

            if (!$translation->exists) {
                // this one is new
                $translation->is_auto_added = true;
                $count++;
            }

            $translation->source = $paths;
            $translation->save();
        }

        // Return the number of found translations
        return $count;
    }

    public
    static function offsetLine($lines, $offset)
    {
        $iMax = count($lines);
        for ($i = 0; $i < $iMax; $i++) {
            if ($lines[$i] > $offset) {
                return $i + 1;
            }
        }

        return $iMax;
    }

    public
    static function computeFileLines($fileContents)
    {
        $lines = [];
        if (preg_match_all("/(\\n)/siU", $fileContents, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE)) {
            foreach ($matches[1] as $index => $key) {
                $lines[] = $key[1];
            }
        }
        return $lines;
    }

    public
    function makeDirPath($path)
    {
        $directories = explode("/", $path);
        array_shift($directories);

        $filename = array_pop($directories);
        $dirpath = "/";

        $full = "/" . implode('/', $directories);
        // find the first existing folder backwards
        for ($i = count($directories); $i > 0; $i--) {
            if ($this->files->exists($full)) {
                break;
            }
            $lastpart = substr(strrchr($full, '/'), 1);
            $full = substr($full, 0, -strlen($lastpart) - 1);
        }

        // Build path and create directories if needed
        for ($k = 0; $k < count($directories); $k++) {
            $dirpath .= $directories[$k] . "/";
            if ($k < $i) continue;
            if (!$this->files->exists($dirpath)) {
                try {
                    $this->files->makeDirectory($dirpath);
                } catch (Exception $e) {
                    $this->errors[] = $e->getMessage() . " for $dirpath";
                }
            }
        }
    }

    public
    function clearErrors()
    {
        $this->errors = [];
    }

    public
    function errors()
    {
        return $this->errors;
    }

    /**
     * @param $group
     * @param $translations
     */
    private
    function cacheTranslationGroup($group, $translations): void
    {
        $this->clearCache($group);
        $this->clearUsageCache(false, $group);
        $translations->each(function ($tr) {
            if ($tr->group === 'JSON' && $tr->locale === 'json') {
                // we have to cache the reverse, we need jsonKey to ltmKey for this one
                $this->cacheTranslation('', $tr->group, $tr->saved_value, $tr->key, $tr->locale);
            } else {
                $this->cacheTranslation('', $tr->group, $tr->key, $tr->saved_value, $tr->locale);
            }
        });
    }

    public
    function exportTranslations($group, $fromAllTranslations = false)
    {
        assert($group && $group !== '*', "exportTranslations only exports one group at a time, given" . $group);
        if (in_array($group, $this->config(self::EXCLUDE_GROUPS_KEY))) return;

        $inDatabasePublishing = $this->inDatabasePublishing();

        if (!$fromAllTranslations) {
            $this->clearErrors();
            $group = self::fixGroup($group);

            if ($inDatabasePublishing !== 3) {
                $this->clearCache($group);
                $this->clearUsageCache(false, $group);
                $this->translatorRepository->deleteTranslationWhereIsDeleted($group);
            }
        }

        if ($group === self::JSON_GROUP) {
            unset($this->ltmJsonKeys);
            $this->loadLtmJsonKeys(true, $inDatabasePublishing != 3, $inDatabasePublishing != 3);

            // save these mappings to the database replacing old values, these are now the standard
            if ($inDatabasePublishing != 3) {
                $this->importTranslationFile('json', self::JSON_GROUP, $this->ltmJsonKeys, 2);
            }
        }

        if (!$fromAllTranslations && ($inDatabasePublishing === 1 || $inDatabasePublishing === 2)) {
            // locked down file system or remote access to one
            $this->translatorRepository->updatePublishTranslations(Translation::STATUS_SAVED_CACHED, $group);

            $translations = $this->translation->query()
                ->where('status', '<>', Translation::STATUS_SAVED)->where('group', '=', $group)->get([
                    'group',
                    'key',
                    'locale',
                    'saved_value',
                ]);
            /* @var $translations Collection */
            $this->cacheTranslationGroup($group, $translations);
        }

        if ($inDatabasePublishing != 1) {
            // not locked down, or remote to locked down or zipping
            // set to published
            if (!$inDatabasePublishing) {
                // not cached and not zipped, so nothing has been set, if cached then already set to cached
                $this->translatorRepository->updatePublishTranslations(Translation::STATUS_SAVED, $group);
            }

            // at this point if published then no deleted translations, if zipping we ignore them
            $rawTranslations = $this->translation->query()
                ->where('group', $group)
                ->whereNotNull('value');

            $rawTranslations = $rawTranslations
                ->orderby('key')
                ->get();

            $tree = $this->makeTree($rawTranslations);
            $lostDotTranslations = $this->getLostDotTranslation($rawTranslations, $tree);
            if ($lostDotTranslations) {
                $errorText = "Incorrect use of dot convention for translation keys (value will be overwritten with an array of child values):";

                foreach ($lostDotTranslations as $group => $groupKeys) {
                    $keys = $groupKeys;
                    $errorText .= "<br>     <strong>$group::</strong>";

                    foreach ($keys as $key => $value) {
                        $errorText .= "<br>          $key";
                    }
                }
                $this->errors[] = $errorText;
            }
            $configRewriter = new TranslationFileRewriter();
            $exportOptions = array_key_exists('export_format', $this->config()) ? TranslationFileRewriter::optionFlags($this->config('export_format')) : null;

            $base_path = $this->app->basePath();
            $pathTemplateResolver = new PathTemplateResolver($this->files, $base_path, $this->config('language_dirs'), '5');
            $zipRoot = $base_path . $this->config('zip_root', mb_substr($this->app->langPath(), 0, -4));

            if (mb_substr($zipRoot, -1) === '/') {
                $zipRoot = substr($zipRoot, 0, -1);
            }

            foreach ($tree as $locale => $groups) {
                if (isset($groups[$group])) {
                    $translations = $groups[$group];

                    // use the new path mapping
                    $computedPath = $pathTemplateResolver->groupFilePath($group, $locale);
                    $path = $base_path . $computedPath;

                    if ($computedPath) {
                        if ($group === self::JSON_GROUP) {
                            if ($locale !== 'json') {
                                // we need translation mapping keys
                                $jsonTranslations = [];
                                foreach ($translations as $key => $value) {
                                    $jsonTranslations[$this->ltmJsonKeys[$key]] = $value;
                                }

                                $output = json_encode($jsonTranslations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
                            } else {
                                // save jsonKey => ltmKey in the json.json file
                                $output = json_encode($this->jsonLtmKeys, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
                            }
                        } else {
                            $configRewriter->parseSource($this->files->exists($path) && $this->files->isFile($path) ? $this->files->get($path) : '');
                            $output = $configRewriter->formatForExport($translations, $exportOptions);
                        }

                        if ($this->zipExporting) {
                            $pathPrefix = mb_substr($path, 0, mb_strlen($zipRoot));
                            $filePathName = ($pathPrefix === $zipRoot) ? mb_substr($path, mb_strlen($zipRoot)) : $path;
                            //$this->makeDirPath($filePathName);
                            $this->zipExporting->addFromString($filePathName, $output);
                        } else {
                            try {
                                $this->makeDirPath($path);
                                if (($result = $this->files->put($path, $output)) === false) {
                                    $this->errors[] = "Failed to write to $path";
                                };
                            } catch (Exception $e) {
                                $this->errors[] = $e->getMessage();
                            }
                        }
                    }
                }
            }
        }
    }

    public
    function exportAllTranslations()
    {
        $groups = $this->translatorRepository->findFilledGroups();

        $inDatabasePublishing = $this->inDatabasePublishing();

        if ($inDatabasePublishing != 3) {
            // not zipping, remove deleted items
            $this->clearCache();
            $this->clearUsageCache(false);
            $this->translatorRepository->deleteTranslationWhereIsDeleted();
        }

        if ($inDatabasePublishing === 1 || $inDatabasePublishing === 2) {
            // locked down file system or remote access to one
            $this->translatorRepository->updatePublishTranslations(Translation::STATUS_SAVED_CACHED);

            $translations = $this->translation->query()->where('status', '<>', Translation::STATUS_SAVED)->get([
                'group',
                'key',
                'locale',
                'saved_value',
            ]);

            /* @var $translations Collection */
            $this->cacheTranslationGroup(null, $translations);
        }

        foreach ($groups as $group) {
            $this->exportTranslations($group->group);
        }
    }

    public
    function zipTranslations($groups)
    {
        $zip_name = @tempnam('Translations_' . time(), 'zip'); // Zip name
        $this->zipExporting = new ZipArchive();
        $this->zipExporting->open($zip_name, ZipArchive::OVERWRITE);

        if (!is_array($groups)) {
            if (!$groups || $groups === '*') {
                $groups = $this->translatorRepository->findFilledGroups();
                foreach ($groups as $group) {
                    // Stuff with content
                    $this->exportTranslations($group->group);
                }
            } else {
                // Stuff with content
                $this->exportTranslations($groups);
            }
        } else {
            foreach ($groups as $group) {
                // Stuff with content
                $this->exportTranslations($group);
            }
        }

        $this->zipExporting->close();
        $this->zipExporting = null;

        return $zip_name;
    }

    public
    function cleanTranslations()
    {
        $this->translation->query()->whereNull('value')->delete();
    }

    public
    function truncateTranslations($group = null)
    {
        if ($group === '*' || $group === null) {
            $this->translation->query()->truncate();
        } else {
            $this->translatorRepository->deleteTranslationByGroup($group);
        }
    }

    protected
    function makeTree($translations)
    {
        $array = array();
        foreach ($translations as $translation) {
            array_set($array[$translation->locale][$translation->group], $translation->key, $translation->value);
        }
        return $array;
    }

    protected
    function getLostDotTranslation($translations, $tree)
    {
        // check if all translation values are in the array or some were lost because of invalid dot notation for keys
        $nonArrays = array();
        foreach ($translations as $translation) {
            $group = $translation->group;
            $key = $translation->key;
            if (!array_key_exists($key, $nonArrays)) {
                $value = array_get($tree[$translation->locale][$translation->group], $translation->key);

                if (is_array($value)) {
                    // this one is an array while it is a translation in the source
                    $nonArrays[$group][$key] = $translation;
                }
            }
        }

        return $nonArrays;
    }

    /**
     * @return true if new json keys take primary locale translation value
     */
    public function isNewJsonKeyFromPrimaryLocale()
    {
        if (!isset($this->newJsonKeyFromPrimaryLocale)) {
            $this->newJsonKeyFromPrimaryLocale = $this->config('new-json-keys-primary-locale', true);
        }
        return $this->newJsonKeyFromPrimaryLocale;
    }

    public function getPrimaryLocale()
    {
        if (!isset($this->primaryLocale)) {
            $this->primaryLocale = $this->config($this->package . '.primary_locale', 'en');
        }
        return $this->primaryLocale;
    }

    public function setPrimaryLocale($primaryLocale)
    {
        $this->primaryLocale = $primaryLocale;
    }
}