1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Core\DataHandling\Model;
19
20use TYPO3\CMS\Core\Utility\GeneralUtility;
21
22/**
23 * CorrelationId representation
24 *
25 * @todo Check internal state during v10 development
26 * @internal
27 */
28class CorrelationId implements \JsonSerializable
29{
30    protected const DEFAULT_VERSION = 1;
31    protected const PATTERN_V1 = '#^(?P<flags>[[:xdigit:]]{4})\$(?:(?P<scope>[[:alnum:]]+):)?(?P<subject>[[:alnum:]]+)(?P<aspects>(?:\/[[:alnum:]._-]+)*)$#';
32
33    /**
34     * @var int
35     */
36    protected $version = self::DEFAULT_VERSION;
37
38    /**
39     * @var string
40     */
41    protected $scope;
42
43    /**
44     * @var int
45     */
46    protected $capabilities = 0;
47
48    /**
49     * @var string
50     */
51    protected $subject;
52
53    /**
54     * @var string[]
55     */
56    protected $aspects = [];
57
58    /**
59     * @param string $scope
60     * @return static
61     */
62    public static function forScope(string $scope): self
63    {
64        $target = static::create();
65        $target->scope = $scope;
66        return $target;
67    }
68
69    public static function forSubject(string $subject, string ...$aspects): self
70    {
71        return static::create()
72            ->withSubject($subject)
73            ->withAspects(...$aspects);
74    }
75
76    /**
77     * @param string $correlationId
78     * @return static
79     */
80    public static function fromString(string $correlationId): self
81    {
82        if (!preg_match(self::PATTERN_V1, $correlationId, $matches, PREG_UNMATCHED_AS_NULL)) {
83            throw new \InvalidArgumentException('Unknown format', 1569620858);
84        }
85
86        $flags = hexdec($matches['flags'] ?? 0);
87        $aspects = !empty($matches['aspects']) ? explode('/', ltrim($matches['aspects'] ?? '', '/')) : [];
88        $target = static::create()
89            ->withSubject($matches['subject'])
90            ->withAspects(...$aspects);
91        $target->scope = $matches['scope'] ?? null;
92        $target->version = $flags >> 10;
93        $target->capabilities = $flags & ((1 << 10) - 1);
94        return  $target;
95    }
96
97    /**
98     * @return static
99     */
100    protected static function create(): self
101    {
102        return GeneralUtility::makeInstance(static::class);
103    }
104
105    public function __toString(): string
106    {
107        if ($this->subject === null) {
108            throw new \LogicException('Cannot serialize for empty subject', 1569668681);
109        }
110        return $this->serialize();
111    }
112
113    public function jsonSerialize(): string
114    {
115        return (string)$this;
116    }
117
118    public function withSubject(string $subject): self
119    {
120        if ($this->subject === $subject) {
121            return $this;
122        }
123        $target = clone $this;
124        $target->subject = $subject;
125        return $target;
126    }
127
128    public function withAspects(string ...$aspects): self
129    {
130        if ($this->aspects === $aspects) {
131            return $this;
132        }
133        $target = clone $this;
134        $target->aspects = $aspects;
135        return $target;
136    }
137
138    /**
139     * @return string|null
140     */
141    public function getScope(): ?string
142    {
143        return $this->scope;
144    }
145
146    /**
147     * @return string|null
148     */
149    public function getSubject(): ?string
150    {
151        return $this->subject;
152    }
153
154    /**
155     * @return string[]
156     */
157    public function getAspects(): array
158    {
159        return $this->aspects;
160    }
161
162    /**
163     * v1 specs (eBNF)
164     * + FLAGS "$" [ SCOPE ":" ] SUBJECT { "/" ASPECT }
165     *   + FLAGS   ::= XDIGIT (* 16-bit integer big-endian)
166     *   + SCOPE   ::= ALNUM { ALNUM }
167     *   + SUBJECT ::= ALNUM { ALNUM }
168     *   + ASPECT  ::= ( ALNUM | '.' | '_' | '-' ) { ( ALNUM | '.' | '_' | '-' ) }
169     */
170    protected function serialize(): string
171    {
172        // 6-bit version 10-bit capabilities
173        $flags = $this->version << 10 + $this->capabilities;
174        return sprintf(
175            '%s$%s%s%s',
176            bin2hex(pack('n', $flags)),
177            $this->scope ? $this->scope . ':' : '',
178            $this->subject,
179            $this->aspects ? '/' . implode('/', $this->aspects) : ''
180        );
181    }
182}
183