|
1 <?php |
|
2 /** |
|
3 * I18N: WP_Translation_File_MO class. |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage I18N |
|
7 * @since 6.5.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Class WP_Translation_File_MO. |
|
12 * |
|
13 * @since 6.5.0 |
|
14 */ |
|
15 class WP_Translation_File_MO extends WP_Translation_File { |
|
16 /** |
|
17 * Endian value. |
|
18 * |
|
19 * V for little endian, N for big endian, or false. |
|
20 * |
|
21 * Used for unpack(). |
|
22 * |
|
23 * @since 6.5.0 |
|
24 * @var false|'V'|'N' |
|
25 */ |
|
26 protected $uint32 = false; |
|
27 |
|
28 /** |
|
29 * The magic number of the GNU message catalog format. |
|
30 * |
|
31 * @since 6.5.0 |
|
32 * @var int |
|
33 */ |
|
34 const MAGIC_MARKER = 0x950412de; |
|
35 |
|
36 /** |
|
37 * Detects endian and validates file. |
|
38 * |
|
39 * @since 6.5.0 |
|
40 * |
|
41 * @param string $header File contents. |
|
42 * @return false|'V'|'N' V for little endian, N for big endian, or false on failure. |
|
43 */ |
|
44 protected function detect_endian_and_validate_file( string $header ) { |
|
45 $big = unpack( 'N', $header ); |
|
46 |
|
47 if ( false === $big ) { |
|
48 return false; |
|
49 } |
|
50 |
|
51 $big = reset( $big ); |
|
52 |
|
53 if ( false === $big ) { |
|
54 return false; |
|
55 } |
|
56 |
|
57 $little = unpack( 'V', $header ); |
|
58 |
|
59 if ( false === $little ) { |
|
60 return false; |
|
61 } |
|
62 |
|
63 $little = reset( $little ); |
|
64 |
|
65 if ( false === $little ) { |
|
66 return false; |
|
67 } |
|
68 |
|
69 // Force cast to an integer as it can be a float on x86 systems. See https://core.trac.wordpress.org/ticket/60678. |
|
70 if ( (int) self::MAGIC_MARKER === $big ) { |
|
71 return 'N'; |
|
72 } |
|
73 |
|
74 // Force cast to an integer as it can be a float on x86 systems. See https://core.trac.wordpress.org/ticket/60678. |
|
75 if ( (int) self::MAGIC_MARKER === $little ) { |
|
76 return 'V'; |
|
77 } |
|
78 |
|
79 $this->error = 'Magic marker does not exist'; |
|
80 return false; |
|
81 } |
|
82 |
|
83 /** |
|
84 * Parses the file. |
|
85 * |
|
86 * @since 6.5.0 |
|
87 * |
|
88 * @return bool True on success, false otherwise. |
|
89 */ |
|
90 protected function parse_file(): bool { |
|
91 $this->parsed = true; |
|
92 |
|
93 $file_contents = file_get_contents( $this->file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents |
|
94 |
|
95 if ( false === $file_contents ) { |
|
96 return false; |
|
97 } |
|
98 |
|
99 $file_length = strlen( $file_contents ); |
|
100 |
|
101 if ( $file_length < 24 ) { |
|
102 $this->error = 'Invalid data'; |
|
103 return false; |
|
104 } |
|
105 |
|
106 $this->uint32 = $this->detect_endian_and_validate_file( substr( $file_contents, 0, 4 ) ); |
|
107 |
|
108 if ( false === $this->uint32 ) { |
|
109 return false; |
|
110 } |
|
111 |
|
112 $offsets = substr( $file_contents, 4, 24 ); |
|
113 |
|
114 if ( false === $offsets ) { |
|
115 return false; |
|
116 } |
|
117 |
|
118 $offsets = unpack( "{$this->uint32}rev/{$this->uint32}total/{$this->uint32}originals_addr/{$this->uint32}translations_addr/{$this->uint32}hash_length/{$this->uint32}hash_addr", $offsets ); |
|
119 |
|
120 if ( false === $offsets ) { |
|
121 return false; |
|
122 } |
|
123 |
|
124 $offsets['originals_length'] = $offsets['translations_addr'] - $offsets['originals_addr']; |
|
125 $offsets['translations_length'] = $offsets['hash_addr'] - $offsets['translations_addr']; |
|
126 |
|
127 if ( $offsets['rev'] > 0 ) { |
|
128 $this->error = 'Unsupported revision'; |
|
129 return false; |
|
130 } |
|
131 |
|
132 if ( $offsets['translations_addr'] > $file_length || $offsets['originals_addr'] > $file_length ) { |
|
133 $this->error = 'Invalid data'; |
|
134 return false; |
|
135 } |
|
136 |
|
137 // Load the Originals. |
|
138 $original_data = str_split( substr( $file_contents, $offsets['originals_addr'], $offsets['originals_length'] ), 8 ); |
|
139 $translations_data = str_split( substr( $file_contents, $offsets['translations_addr'], $offsets['translations_length'] ), 8 ); |
|
140 |
|
141 foreach ( array_keys( $original_data ) as $i ) { |
|
142 $o = unpack( "{$this->uint32}length/{$this->uint32}pos", $original_data[ $i ] ); |
|
143 $t = unpack( "{$this->uint32}length/{$this->uint32}pos", $translations_data[ $i ] ); |
|
144 |
|
145 if ( false === $o || false === $t ) { |
|
146 continue; |
|
147 } |
|
148 |
|
149 $original = substr( $file_contents, $o['pos'], $o['length'] ); |
|
150 $translation = substr( $file_contents, $t['pos'], $t['length'] ); |
|
151 // GlotPress bug. |
|
152 $translation = rtrim( $translation, "\0" ); |
|
153 |
|
154 // Metadata about the MO file is stored in the first translation entry. |
|
155 if ( '' === $original ) { |
|
156 foreach ( explode( "\n", $translation ) as $meta_line ) { |
|
157 if ( '' === $meta_line ) { |
|
158 continue; |
|
159 } |
|
160 |
|
161 list( $name, $value ) = array_map( 'trim', explode( ':', $meta_line, 2 ) ); |
|
162 |
|
163 $this->headers[ strtolower( $name ) ] = $value; |
|
164 } |
|
165 } else { |
|
166 /* |
|
167 * In MO files, the key normally contains both singular and plural versions. |
|
168 * However, this just adds the singular string for lookup, |
|
169 * which caters for cases where both __( 'Product' ) and _n( 'Product', 'Products' ) |
|
170 * are used and the translation is expected to be the same for both. |
|
171 */ |
|
172 $parts = explode( "\0", (string) $original ); |
|
173 |
|
174 $this->entries[ $parts[0] ] = $translation; |
|
175 } |
|
176 } |
|
177 |
|
178 return true; |
|
179 } |
|
180 |
|
181 /** |
|
182 * Exports translation contents as a string. |
|
183 * |
|
184 * @since 6.5.0 |
|
185 * |
|
186 * @return string Translation file contents. |
|
187 */ |
|
188 public function export(): string { |
|
189 // Prefix the headers as the first key. |
|
190 $headers_string = ''; |
|
191 foreach ( $this->headers as $header => $value ) { |
|
192 $headers_string .= "{$header}: $value\n"; |
|
193 } |
|
194 $entries = array_merge( array( '' => $headers_string ), $this->entries ); |
|
195 $entry_count = count( $entries ); |
|
196 |
|
197 if ( false === $this->uint32 ) { |
|
198 $this->uint32 = 'V'; |
|
199 } |
|
200 |
|
201 $bytes_for_entries = $entry_count * 4 * 2; |
|
202 // Pair of 32bit ints per entry. |
|
203 $originals_addr = 28; /* header */ |
|
204 $translations_addr = $originals_addr + $bytes_for_entries; |
|
205 $hash_addr = $translations_addr + $bytes_for_entries; |
|
206 $entry_offsets = $hash_addr; |
|
207 |
|
208 $file_header = pack( |
|
209 $this->uint32 . '*', |
|
210 // Force cast to an integer as it can be a float on x86 systems. See https://core.trac.wordpress.org/ticket/60678. |
|
211 (int) self::MAGIC_MARKER, |
|
212 0, /* rev */ |
|
213 $entry_count, |
|
214 $originals_addr, |
|
215 $translations_addr, |
|
216 0, /* hash_length */ |
|
217 $hash_addr |
|
218 ); |
|
219 |
|
220 $o_entries = ''; |
|
221 $t_entries = ''; |
|
222 $o_addr = ''; |
|
223 $t_addr = ''; |
|
224 |
|
225 foreach ( array_keys( $entries ) as $original ) { |
|
226 $o_addr .= pack( $this->uint32 . '*', strlen( $original ), $entry_offsets ); |
|
227 $entry_offsets += strlen( $original ) + 1; |
|
228 $o_entries .= $original . "\0"; |
|
229 } |
|
230 |
|
231 foreach ( $entries as $translations ) { |
|
232 $t_addr .= pack( $this->uint32 . '*', strlen( $translations ), $entry_offsets ); |
|
233 $entry_offsets += strlen( $translations ) + 1; |
|
234 $t_entries .= $translations . "\0"; |
|
235 } |
|
236 |
|
237 return $file_header . $o_addr . $t_addr . $o_entries . $t_entries; |
|
238 } |
|
239 } |