Passed
Push — master ( 0e0687...5606fe )
by Dante
01:10
created

File::fixString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 8
rs 10
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2023 Atlas Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
16
namespace BEdita\I18n\Filesystem;
17
18
use RecursiveDirectoryIterator;
19
use RecursiveIteratorIterator;
20
use RegexIterator;
21
22
/**
23
 * File utilities.
24
 */
25
class File
26
{
27
    /**
28
     * Parse a directory, look for files and rips gettext strings (@see parseFile).
29
     * Files with extensions .php, .ctp, .thtml, .inc, .tpl, .twig are parsed.
30
     * Returns true if all files are parsed correctly, false otherwise.
31
     *
32
     * @param string $dir The directory
33
     * @param string $defaultDomain The default domain
34
     * @param array $translations The translations array
35
     * @return bool
36
     */
37
    public static function parseDir(string $dir, string $defaultDomain, array &$translations): bool
38
    {
39
        $result = true;
40
        if (!is_dir($dir)) {
41
            return false;
42
        }
43
        $files = new RegexIterator(
44
            new RecursiveIteratorIterator(
45
                new RecursiveDirectoryIterator(
46
                    $dir,
47
                    RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_PATHNAME
48
                )
49
            ),
50
            '/.*\.(php|ctp|thtml|inc|tpl|twig)/i',
51
        );
52
        foreach ($files as $file) {
53
            $parseResult = self::parseFile($file, $defaultDomain, $translations);
54
            $result = $result && $parseResult;
55
        }
56
57
        return $result;
58
    }
59
60
    /**
61
     * Parse file and rips gettext strings.
62
     * Returns true if file is parsed correctly, false otherwise.
63
     *
64
     * @param string $file The file name
65
     * @param string $defaultDomain The default domain
66
     * @param array $translations The translations array
67
     * @return bool
68
     */
69
    public static function parseFile(string $file, string $defaultDomain, array &$translations): bool
70
    {
71
        if (!file_exists($file)) {
72
            return false;
73
        }
74
        $content = file_get_contents($file);
75
        if (empty($content)) {
76
            return false;
77
        }
78
79
        $functions = [
80
            '__' => 0, // __( string $singular , ... $args )
81
            '__n' => 0, // __n( string $singular , string $plural , integer $count , ... $args )
82
            '__d' => 1, // __d( string $domain , string $msg , ... $args )
83
            '__dn' => 1, // __dn( string $domain , string $singular , string $plural , integer $count , ... $args )
84
            '__x' => 1, // __x( string $context , string $singular , ... $args )
85
            '__xn' => 1, // __xn( string $context , string $singular , string $plural , integer $count , ... $args )
86
            '__dx' => 2, // __dx( string $domain , string $context , string $msg , ... $args )
87
            '__dxn' => 2, // __dxn( string $domain , string $context , string $singular , string $plural , integer $count , ... $args )
88
        ];
89
90
        // temporarily replace "\'" with "|||||", fixString will replace "|||||" with "\'"
91
        // this fixes wrongly matched data in the following regexp
92
        $content = str_replace("\'", '|||||', $content);
93
94
        $options = [
95
            'open_parenthesis' => preg_quote('('),
96
            'quote' => preg_quote("'"),
97
            'double_quote' => preg_quote('"'),
98
        ];
99
100
        foreach ($functions as $fname => $singularPosition) {
101
            $capturePath = "'[^']*'";
102
            $doubleQuoteCapture = str_replace("'", $options['double_quote'], $capturePath);
103
            $quoteCapture = str_replace("'", $options['quote'], $capturePath);
104
105
            // phpcs:disable
106
            $rgxp = '/' . $fname . '\s*' . $options['open_parenthesis'] . str_repeat('((?:' . $doubleQuoteCapture . ')|(?:' . $quoteCapture . '))\s*[,)]\s*', $singularPosition + 1) . '/';
107
            // phpcs:enable
108
109
            $matches = [];
110
            preg_match_all($rgxp, $content, $matches);
111
112
            $limit = count($matches[0]);
113
            for ($i = 0; $i < $limit; $i++) {
114
                $domain = $defaultDomain;
115
                $ctx = '';
116
                $str = self::unquoteString($matches[1][$i]);
117
118
                if (strpos($fname, '__d') === 0) {
119
                    $domain = self::unquoteString($matches[1][$i]);
120
121
                    if (strpos($fname, '__dx') === 0) {
122
                        $ctx = self::unquoteString($matches[2][$i]);
123
                        $str = self::unquoteString($matches[3][$i]);
124
                    } else {
125
                        $str = self::unquoteString($matches[2][$i]);
126
                    }
127
                } elseif (strpos($fname, '__x') === 0) {
128
                    $ctx = self::unquoteString($matches[1][$i]);
129
                    $str = self::unquoteString($matches[2][$i]);
130
                }
131
                $str = self::fixString($str);
132
133
                if (!array_key_exists($domain, $translations)) {
134
                    $translations[$domain] = [];
135
                }
136
137
                if (!array_key_exists($str, $translations[$domain])) {
138
                    $translations[$domain][$str] = [''];
139
                }
140
141
                if (!in_array($ctx, $translations[$domain][$str])) {
142
                    $translations[$domain][$str][] = $ctx;
143
                }
144
            }
145
        }
146
147
        return true;
148
    }
149
150
    /**
151
     * Remove leading and trailing quotes from string.
152
     *
153
     * @param string $str The string
154
     * @return string The new string
155
     */
156
    public static function unquoteString($str): string
157
    {
158
        return substr($str, 1, -1);
159
    }
160
161
    /**
162
     * "fix" string - strip slashes, escape and convert new lines to \n.
163
     *
164
     * @param string $str The string
165
     * @return string The new string
166
     */
167
    public static function fixString($str): string
168
    {
169
        $str = stripslashes($str);
170
        $str = str_replace('"', '\"', $str);
171
        $str = str_replace("\n", '\n', $str);
172
        $str = str_replace('|||||', "'", $str); // special sequence used in parseContent to temporarily replace "\'"
173
174
        return $str;
175
    }
176
}
177