1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8/**
9 * @package   Tiki
10 * @subpackage    Language
11 */
12
13class Language_GetStrings
14{
15	/**
16	 * Array of file types objects.
17	 *
18	 * @var array
19	 */
20	protected $fileTypes = [];
21
22	/**
23	 * Array of valid extensions. Extracted
24	 * from $this->fileTypes.
25	 *
26	 * @var array
27	 */
28	protected $extensions = [];
29
30	/**
31	 * List of languages whose language.php
32	 * files will be updated. If empty all
33	 * language.php files are updated.
34	 *
35	 * @var array
36	 */
37	protected $languages = [];
38
39	/**
40	 * Name of the file that contain the
41	 * translations.
42	 * @var string
43	 */
44	protected $fileName = 'language.php';
45
46	/**
47	 * @var Language_CollectFiles
48	 */
49	public $collectFiles;
50
51	/**
52	 * @var Language_WriteFile_Factory
53	 */
54	public $writeFileFactory;
55
56	/**
57	 * Whether file paths where the string was found
58	 * is included or not in langauge.php files. Default
59	 * is false.
60	 *
61	 * @var bool
62	 */
63	protected $outputFiles = false;
64
65	/**
66	 * Directory used as base to search for strings
67	 * and to construct paths to language.php files.
68	 * @var string
69	 */
70	protected $baseDir;
71
72	/**
73	 * Class construct.
74	 *
75	 * The following are valid $options:
76	 *   - 'outputFiles' => true: will write to language.php file the path
77	 *     to the files where the string was found. Default is false.
78	 *   - 'lang' => 'langCode' or 'lang' => array(list of lang codes):
79	 *     language code or list of language codes whose language.php will be
80	 *     updated. If empty, all language.php files are updated.
81	 *
82	 * @param Language_CollectFiles $collectFiles
83	 * @param Language_WriteFile_Factory $writeFileFactory factory to create Language_WriteFile objects
84	 * @param array $options list of options to control object behavior (see above)
85	 * @return null
86	 */
87	public function __construct(Language_CollectFiles $collectFiles, Language_WriteFile_Factory $writeFileFactory, array $options = null)
88	{
89		$this->collectFiles = $collectFiles;
90		$this->writeFileFactory = $writeFileFactory;
91
92		if (isset($options['outputFiles'])) {
93			$this->outputFiles = true;
94		}
95
96		if (isset($options['baseDir'])) {
97			if (! is_dir($options['baseDir'])) {
98				throw new Language_Exception("Invalid directory {$options['baseDir']}.");
99			}
100
101			$this->baseDir = $options['baseDir'];
102		} else {
103			$this->baseDir = getcwd();
104		}
105
106		if (isset($options['fileName'])) {
107			$this->fileName = $options['fileName'];
108		}
109
110		if (isset($options['lang'])) {
111			$this->setLanguages($options['lang']);
112		} else {
113			$this->setLanguages();
114		}
115	}
116
117	/**
118	 * Getter for $this->extensions
119	 * @return array
120	 */
121	public function getExtensions()
122	{
123		return $this->extensions;
124	}
125
126	/**
127	 * Getter for $this->fileTypes
128	 * @return array
129	 */
130	public function getFileTypes()
131	{
132		return $this->fileTypes;
133	}
134
135	/**
136	 * Add a file type object to $this->fileTypes
137	 * and update $this->extensions.
138	 *
139	 * @param $fileType Language_FileType
140	 * @return null
141	 * @throws Language_Exception if type being added already exists
142	 */
143	public function addFileType(Language_FileType $fileType)
144	{
145		if (in_array($fileType, $this->fileTypes)) {
146			$className = get_class($fileType);
147			throw new Language_Exception("Type $className already added.");
148		}
149
150		$this->fileTypes[] = $fileType;
151		$this->extensions = array_merge($this->extensions, $fileType->getExtensions());
152	}
153
154	/**
155	 * Setter method $this->languages
156	 * property.
157	 *
158	 * @param array|string $languages
159	 * @return null
160	 */
161	public function setLanguages($languages = null)
162	{
163		if (is_null($languages)) {
164			$languages = $this->getAllLanguages();
165		} else {
166			if (is_string($languages)) {
167				$languages = [$languages];
168			}
169
170			foreach ($languages as $lang) {
171				if (! file_exists($this->baseDir . '/lang/' . $lang)) {
172					throw new Language_Exception('Invalid language code.');
173				}
174			}
175		}
176
177		$this->languages = $languages;
178	}
179
180	/**
181	 * Getter method for $this->languages.
182	 *
183	 * @return array
184	 */
185	public function getLanguages()
186	{
187		return $this->languages;
188	}
189
190	/**
191	 * Get English strings from a given file.
192	 *
193	 * @param string $filePath path to file
194	 * @return array collected strings
195	 */
196	public function collectStrings($filePath)
197	{
198		if (empty($this->fileTypes)) {
199			throw new Language_Exception('No Language_FileType found.');
200		}
201
202		$strings = [];
203		$fileExtension = strrchr($filePath, '.');
204
205		if (! $fileExtension || $fileExtension == '.') {
206			throw new Language_Exception('Could not determine file extension.');
207		}
208
209		foreach ($this->fileTypes as $fileType) {
210			if (in_array($fileExtension, $fileType->getExtensions())) {
211				$file = file_get_contents($filePath);
212
213				foreach ($fileType->getCleanupRegexes() as $regex => $replacement) {
214					$file = preg_replace($regex, $replacement, $file);
215				}
216
217				foreach ($fileType->getRegexes() as $postProcess => $regex) {
218					$matches = [];
219					preg_match_all($regex, $file, $matches);
220					$newStrings = $matches[1];
221
222					// $postProcess can be used to call a file type specific method for each regular expression
223					// used for PHP file type to perform different clean up for single quoted and double quoted strings
224					if (method_exists($fileType, $postProcess)) {
225						$newStrings = $fileType->$postProcess($newStrings);
226					}
227
228					$strings = array_merge($strings, $newStrings);
229				}
230
231				break;
232			}
233		}
234
235		return array_values(array_unique($strings));
236	}
237
238	/**
239	 * Loop through a list of files and
240	 * calls $this->collectStrings() for each
241	 * file. Return a list of translatable strings
242	 * found.
243	 *
244	 * @param array $files
245	 * @return array $strings translatable strings found in scanned files
246	 */
247	public function scanFiles($files)
248	{
249		$strings = [];
250
251		// strings collected per file
252		$filesStrings = [];
253
254		if (! empty($files)) {
255			foreach ($files as $file) {
256				$filesStrings[$file] = $this->collectStrings($file);
257			}
258		}
259
260		// join strings collected per file into a single array
261		// and remove duplicated strings
262		foreach ($filesStrings as $file => $fileStrings) {
263			foreach ($fileStrings as $str) {
264				if (! isset($strings[$str])) {
265					$string = ['name' => $str];
266
267					if ($this->outputFiles) {
268						// $string['files'] is an array with all the files where the string was found
269						$string['files'] = [$file];
270					}
271
272					$strings[$str] = $string;
273				} else {
274					if ($this->outputFiles) {
275						$strings[$str]['files'][] = $file;
276					}
277				}
278			}
279		}
280
281		return $strings;
282	}
283
284	public function writeToFiles($strings)
285	{
286		foreach ($this->languages as $lang) {
287			$filePath = $this->baseDir . '/lang/' . $lang . '/' . $this->fileName;
288			$writeFile = $this->writeFileFactory->factory($filePath);
289			$writeFile->writeStringsToFile($strings, $this->outputFiles);
290		}
291	}
292
293	/**
294	 * Return all available languages (check for the
295	 * existence of a language file).
296	 * @return array all language codes
297	 */
298	protected function getAllLanguages()
299	{
300		$dirs = dir($this->baseDir . '/lang');
301
302		while (false !== ($entry = $dirs->read())) {
303			if ($entry == '.' || $entry == '..') {
304				continue;
305			}
306
307			$path = $dirs->path . '/' . $entry;
308			if (is_dir($path) && file_exists($path . '/' . $this->fileName)) {
309				$languages[] = $entry;
310			}
311		}
312
313		return $languages;
314	}
315
316	public function run()
317	{
318		if (empty($this->fileTypes)) {
319			throw new Language_Exception('No Language_FileType found.');
320		}
321
322		$this->collectFiles->setExtensions($this->extensions);
323		$files = $this->collectFiles->run($this->baseDir);
324		$strings = $this->scanFiles($files);
325		$this->writeToFiles($strings);
326	}
327}
328