Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
n/a
0 / 0
n/a
0 / 0
CRAP
n/a
0 / 0
TransExtractor
n/a
0 / 0
n/a
0 / 0
53
n/a
0 / 0
 extract
n/a
0 / 0
3
n/a
0 / 0
 setPrefix
n/a
0 / 0
1
n/a
0 / 0
 normalizeToken
n/a
0 / 0
3
n/a
0 / 0
 seekToNextRelevantToken
n/a
0 / 0
3
n/a
0 / 0
 skipMethodArgument
n/a
0 / 0
10
n/a
0 / 0
 canBeExtracted
n/a
0 / 0
3
n/a
0 / 0
 extractFromDirectory
n/a
0 / 0
1
n/a
0 / 0
 getValue
n/a
0 / 0
11
n/a
0 / 0
 parseTokens
n/a
0 / 0
12
n/a
0 / 0
 store
n/a
0 / 0
2
n/a
0 / 0
 storeDynamic
n/a
0 / 0
4
n/a
0 / 0
1<?php
2
3// {{{ License
4// This file is part of GNU social - https://www.gnu.org/software/social
5//
6// GNU social is free software: you can redistribute it and/or modify
7// it under the terms of the GNU Affero General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10//
11// GNU social is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU Affero General Public License for more details.
15//
16// You should have received a copy of the GNU Affero General Public License
17// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
18// }}}
19
20/**
21 * Extracts translation messages from PHP code.
22 *
23 * @package   GNUsocial
24 * @category  I18n
25 *
26 * @author    Symfony project
27 * @author    Michel Salib <michelsalib@hotmail.com>
28 * @author    Fabien Potencier <fabien@symfony.com>
29 * @copyright 2011-2019 Symfony project
30 * @author    Hugo Sales <hugo@hsal.es>
31 * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
32 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
33 */
34
35namespace App\Core\I18n;
36
37use App\Util\Formatting;
38use ArrayIterator;
39use function count;
40use InvalidArgumentException;
41use Iterator;
42use Symfony\Component\Finder\Finder;
43use Symfony\Component\Translation\Extractor\AbstractFileExtractor;
44use Symfony\Component\Translation\Extractor\ExtractorInterface;
45use Symfony\Component\Translation\Extractor\PhpStringTokenParser;
46use Symfony\Component\Translation\MessageCatalogue;
47
48/**
49 * Since this happens outside the normal request life-cycle (through a
50 * command, usually), it unfeasible to test this
51 *
52 * @codeCoverageIgnore
53 */
54class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
55{
56    /**
57     * The sequence that captures translation messages.
58     *
59     * @todo add support for all the cases we use
60     *
61     * @var array
62     */
63    protected $sequences = [
64        // [
65        //     '_m',
66        //     '(',
67        //     self::MESSAGE_TOKEN,
68        //     ',',
69        //     self::METHOD_ARGUMENTS_TOKEN,
70        //     ',',
71        //     self::DOMAIN_TOKEN,
72        // ],
73        [
74            '_m',
75            '(',
76            self::MESSAGE_TOKEN,
77        ],
78        [
79            // Special case: when we have calls to _m with a dynamic
80            // value, we need to handle them seperately
81            'function',
82            '_m_dynamic',
83            self::M_DYNAMIC,
84        ],
85    ];
86
87    // TODO probably shouldn't be done this way
88    // {{{Code from PhpExtractor
89    // See vendor/symfony/translation/Extractor/PhpExtractor.php
90    //
91    const MESSAGE_TOKEN          = 300;
92    const METHOD_ARGUMENTS_TOKEN = 1000;
93    const DOMAIN_TOKEN           = 1001;
94    const M_DYNAMIC              = 1002;
95
96    /**
97     * Prefix for new found message.
98     *
99     * @var string
100     */
101    private $prefix = '';
102
103    /**
104     * {@inheritdoc}
105     */
106    public function extract($resource, MessageCatalogue $catalog)
107    {
108        if (($dir = strstr($resource, '/Core/GNUsocial.php', true)) === false) {
109            return;
110        }
111
112        $files = $this->extractFiles($dir);
113        foreach ($files as $file) {
114            $this->parseTokens(token_get_all(file_get_contents($file)), $catalog, $file);
115
116            gc_mem_caches();
117        }
118    }
119
120    /**
121     * {@inheritdoc}
122     */
123    public function setPrefix(string $prefix)
124    {
125        $this->prefix = $prefix;
126    }
127
128    /**
129     * Normalizes a token.
130     *
131     * @param mixed $token
132     *
133     * @return null|string
134     */
135    protected function normalizeToken($token)
136    {
137        if (isset($token[1]) && 'b"' !== $token) {
138            return $token[1];
139        }
140
141        return $token;
142    }
143
144    /**
145     * Seeks to a non-whitespace token.
146     */
147    private function seekToNextRelevantToken(Iterator $tokenIterator)
148    {
149        for (; $tokenIterator->valid(); $tokenIterator->next()) {
150            $t = $tokenIterator->current();
151            if (T_WHITESPACE !== $t[0]) {
152                break;
153            }
154        }
155    }
156
157    /**
158     * {@inheritdoc}
159     */
160    private function skipMethodArgument(Iterator $tokenIterator)
161    {
162        $openBraces = 0;
163
164        for (; $tokenIterator->valid(); $tokenIterator->next()) {
165            $t = $tokenIterator->current();
166
167            if ('[' === $t[0] || '(' === $t[0]) {
168                ++$openBraces;
169            }
170
171            if (']' === $t[0] || ')' === $t[0]) {
172                --$openBraces;
173            }
174
175            if ((0 === $openBraces && ',' === $t[0]) || (-1 === $openBraces && ')' === $t[0])) {
176                break;
177            }
178        }
179    }
180
181    /**
182     * @throws InvalidArgumentException
183     *
184     * @return bool
185     *
186     *
187     */
188    protected function canBeExtracted(string $file)
189    {
190        return $this->isFile($file)
191            && 'php' === pathinfo($file, PATHINFO_EXTENSION)
192            && strstr($file, '/src/') !== false;
193    }
194
195    /**
196     * {@inheritdoc}
197     */
198    protected function extractFromDirectory($directory)
199    {
200        $finder = new Finder();
201        return $finder->files()->name('*.php')->in($directory);
202    }
203
204    /**
205     * Extracts the message from the iterator while the tokens
206     * match allowed message tokens.
207     */
208    private function getValue(Iterator $tokenIterator)
209    {
210        $message  = '';
211        $docToken = '';
212        $docPart  = '';
213
214        for (; $tokenIterator->valid(); $tokenIterator->next()) {
215            $t = $tokenIterator->current();
216            if ('.' === $t) {
217                // Concatenate with next token
218                continue;
219            }
220            if (!isset($t[1])) {
221                break;
222            }
223
224            switch ($t[0]) {
225                case T_START_HEREDOC:
226                    $docToken = $t[1];
227                    break;
228                case T_ENCAPSED_AND_WHITESPACE:
229                case T_CONSTANT_ENCAPSED_STRING:
230                    if ('' === $docToken) {
231                        $message .= PhpStringTokenParser::parse($t[1]);
232                    } else {
233                        $docPart = $t[1];
234                    }
235                    break;
236                case T_END_HEREDOC:
237                    $message .= PhpStringTokenParser::parseDocString($docToken, $docPart);
238                    $docToken = '';
239                    $docPart  = '';
240                    break;
241                case T_WHITESPACE:
242                    break;
243                default:
244                    break 2;
245            }
246        }
247
248        return $message;
249    }
250
251    // }}}
252
253    /**
254     * Extracts trans message from PHP tokens.
255     */
256    protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename)
257    {
258        $tokenIterator = new ArrayIterator($tokens);
259
260        for ($key = 0; $key < $tokenIterator->count(); ++$key) {
261            foreach ($this->sequences as $sequence) {
262                $message = '';
263                $domain  = I18n::_mdomain($filename);
264                $tokenIterator->seek($key);
265
266                foreach ($sequence as $sequenceKey => $item) {
267                    $this->seekToNextRelevantToken($tokenIterator);
268
269                    if ($this->normalizeToken($tokenIterator->current()) === $item) {
270                        $tokenIterator->next();
271                        continue;
272                    } elseif (self::MESSAGE_TOKEN === $item) {
273                        $message = $this->getValue($tokenIterator);
274
275                        if (count($sequence) === ($sequenceKey + 1)) {
276                            break;
277                        }
278                    } elseif (self::METHOD_ARGUMENTS_TOKEN === $item) {
279                        $this->skipMethodArgument($tokenIterator);
280                    } elseif (self::DOMAIN_TOKEN === $item) {
281                        $domainToken = $this->getValue($tokenIterator);
282                        if ('' !== $domainToken) {
283                            $domain = $domainToken;
284                        }
285                        break;
286                    } elseif (self::M_DYNAMIC === $item) {
287                        // Special case
288                        self::storeDynamic($catalog, $filename);
289                    } else {
290                        break;
291                    }
292                }
293
294                if ($message) {
295                    self::store($catalog, $message, $domain, $filename, $tokens[$key][2]); // Line no.
296                    break;
297                }
298            }
299        }
300    }
301
302    /**
303     * Store the $message in the message catalogue $mc
304     */
305    private function store(MessageCatalogue $mc, string $message,
306                           string $domain, string $filename, ?int $line_no = null)
307    {
308        $mc->set($message, $this->prefix . $message, $domain);
309        $metadata              = $mc->getMetadata($message, $domain) ?? [];
310        $metadata['sources'][] = Formatting::normalizePath($filename) . (!empty($line_no) ? ":{$line_no}" : '');
311        $mc->setMetadata($message, $metadata, $domain);
312    }
313
314    /**
315     * Calls `::_m_dynamic` from the class defined in $filename and
316     * stores the results in the catalogue. For cases when the
317     * translation can't be done in a static (non-PHP) file
318     */
319    private function storeDynamic(MessageCatalogue $mc, string $filename)
320    {
321        require_once $filename;
322        $class   = preg_replace('/.*\/([A-Za-z]*)\.php/', '\1', $filename);
323        $classes = get_declared_classes();
324
325        // Find FQCN of $class
326        foreach ($classes as $c) {
327            if (strstr($c, $class) !== false) {
328                $class = $c;
329                break;
330            }
331        }
332
333        $messages = $class::_m_dynamic();
334        $domain   = $messages['domain'];
335        unset($messages['domain']);
336        foreach ($messages as $m) {
337            self::store($mc, $m, $domain, $filename);
338        }
339    }
340}