1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4/**
5 * Contains the Translation2_Admin_Container_gettext class
6 *
7 * PHP versions 4 and 5
8 *
9 * LICENSE: Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions are met:
11 * 1. Redistributions of source code must retain the above copyright
12 *    notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 *    notice, this list of conditions and the following disclaimer in the
15 *    documentation and/or other materials provided with the distribution.
16 * 3. The name of the author may not be used to endorse or promote products
17 *    derived from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
20 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
21 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 * IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 *
30 * @category  Internationalization
31 * @package   Translation2
32 * @author    Lorenzo Alberton <l.alberton@quipo.it>
33 * @author    Michael Wallner <mike@php.net>
34 * @copyright 2004-2007 Lorenzo Alberton, Michael Wallner
35 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
36 * @version   CVS: $Id: gettext.php 305985 2010-12-05 22:55:33Z clockwerx $
37 * @link      http://pear.php.net/package/Translation2
38 */
39
40/**
41 * require Translation2_Container_gettext class
42 */
43require_once 'Translation2/Container/gettext.php';
44
45/**
46 * Storage driver for storing/fetching data to/from a gettext file
47 *
48 * This storage driver requires the gettext extension
49 *
50 * @category  Internationalization
51 * @package   Translation2
52 * @author    Lorenzo Alberton <l.alberton@quipo.it>
53 * @author    Michael Wallner <mike@php.net>
54 * @copyright 2004-2007 Lorenzo Alberton, Michael Wallner
55 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
56 * @version   CVS: $Id: gettext.php 305985 2010-12-05 22:55:33Z clockwerx $
57 * @link      http://pear.php.net/package/Translation2
58 */
59class Translation2_Admin_Container_gettext extends Translation2_Container_gettext
60{
61    // {{{ class vars
62
63    var $_bulk   = false;
64    var $_queue  = array();
65    var $_fields = array('name', 'meta', 'error_text', 'encoding');
66
67    // }}}
68    // {{{ addLang()
69
70    /**
71     * Creates a new entry in the langs_avail .ini file.
72     *
73     * @param array  $langData language data
74     * @param string $path     path to gettext data dir
75     *
76     * @return  mixed   Returns true on success or PEAR_Error on failure.
77     */
78    function addLang($langData, $path = null)
79    {
80        if (!isset($path) || !is_string($path)) {
81            $path = $this->_domains[$this->options['default_domain']];
82        }
83
84        $path .= '/'. $langData['lang_id'] . '/LC_MESSAGES';
85
86        if (!is_dir($path)) {
87            include_once 'System.php';
88            if (!System::mkdir(array('-p', $path))) {
89                return $this->raiseError(sprintf(
90                        'Cannot create new language in path "%s"', $path
91                    ),
92                    TRANSLATION2_ERROR_CANNOT_CREATE_DIR
93                );
94            }
95        }
96
97        return true;
98    }
99
100    // }}}
101    // {{{ addLangToList()
102
103    /**
104     * Creates a new entry in the langsAvail .ini file.
105     * If the file doesn't exist yet, it is created.
106     *
107     * @param array $langData array('lang_id'    => 'en',
108     *                              'name'       => 'english',
109     *                              'meta'       => 'some meta info',
110     *                              'error_text' => 'not available'
111     *                              'encoding'   => 'iso-8859-1',
112     * );
113     *
114     * @return true|PEAR_Error on failure
115     */
116    function addLangToList($langData)
117    {
118        if (PEAR::isError($changed = $this->_updateLangData($langData))) {
119            return $changed;
120        }
121        return $changed ? $this->_writeLangsAvailFile() : true;
122    }
123
124    // }}}
125    // {{{ add()
126
127    /**
128     * Add a new entry in the strings domain.
129     *
130     * @param string $stringID string ID
131     * @param string $pageID   page/group ID
132     * @param array  $strings  Associative array with string translations.
133     *               Sample format:  array('en' => 'sample', 'it' => 'esempio')
134     *
135     * @return true|PEAR_Error on failure
136     */
137    function add($stringID, $pageID, $strings)
138    {
139        if (!isset($pageID)) {
140            $pageID = $this->options['default_domain'];
141        }
142
143        $langs = array_intersect(array_keys($strings), $this->getLangs('ids'));
144
145        if (!count($langs)) {
146            return true; // really?
147        }
148
149        if ($this->_bulk) {
150            foreach ($strings as $lang => $string) {
151                if (in_array($lang, $langs)) {
152                    $this->_queue['add'][$pageID][$lang][$stringID] = $string;
153                }
154            }
155            return true;
156        } else {
157            $add = array();
158            foreach ($strings as $lang => $string) {
159                if (in_array($lang, $langs)) {
160                    $add[$pageID][$lang][$stringID] = $string;
161                }
162            }
163            return $this->_add($add);
164        }
165    }
166
167    // }}}
168    // {{{ remove()
169
170    /**
171     * Remove an entry from the domain.
172     *
173     * @param string $stringID string ID
174     * @param string $pageID   page/group ID
175     *
176     * @return true|PEAR_Error on failure
177     */
178    function remove($stringID, $pageID)
179    {
180        if (!isset($pageID)) {
181            $pageID = $this->options['default_domain'];
182        }
183
184        if ($this->_bulk) {
185            $this->_queue['remove'][$pageID][$stringID] = true;
186            return true;
187        } else {
188            $tmp = array($pageID => array($stringID => true));
189            return $this->_remove($tmp);
190        }
191
192    }
193
194    // }}}
195    // {{{ removePage
196
197    /**
198     * Remove all the strings in the given page/group (domain)
199     *
200     * @param string $pageID page/group ID
201     * @param string $path   path to gettext data dir
202     *
203     * @return mixed true on success, PEAR_Error on failure
204     */
205    function removePage($pageID = null, $path = null)
206    {
207        if (!isset($pageID)) {
208            $pageID = $this->options['default_domain'];
209        }
210
211        if (!isset($path)) {
212            if (!empty($this->_domains[$pageID])) {
213                $path = $this->_domains[$pageID];
214            } else {
215                $path = $this->_domains[$this->options['default_domain']];
216            }
217        }
218
219        if (PEAR::isError($e = $this->_removeDomain($pageID))) {
220            return $e;
221        }
222
223        $this->fetchLangs();
224        foreach ($this->langs as $langID => $lang) {
225            $domain_file = $path .'/'. $langID .'/LC_MESSAGES/'. $pageID .'.';
226            if (!@unlink($domain_file.'mo') || !@unlink($domain_file.'po')) {
227                return $this->raiseError('Cannot delete page ' . $pageID. ' (file '.$domain_file.'.*)',
228                    TRANSLATION2_ERROR
229                );
230            }
231        }
232
233        return true;
234    }
235
236    // }}}
237    // {{{ update()
238
239    /**
240     * Update
241     *
242     * Alias for Translation2_Admin_Container_gettext::add()
243     *
244     * @param string $stringID string ID
245     * @param string $pageID   page/group ID
246     * @param array  $strings  strings
247     *
248     * @return  mixed
249     * @access  public
250     * @see add()
251     */
252    function update($stringID, $pageID, $strings)
253    {
254        return $this->add($stringID, $pageID, $strings);
255    }
256
257    // }}}
258    // {{{ removeLang()
259
260    /**
261     * Remove Language
262     *
263     * @param string $langID language ID
264     * @param bool   $force  (unused)
265     *
266     * @return true|PEAR_Error
267     * @access public
268     */
269    function removeLang($langID, $force = false)
270    {
271        include_once 'System.php';
272        foreach ((array) $this->_domains as $domain => $path) {
273            if (is_dir($fp = $path .'/'. $langID)) {
274                if (PEAR::isError($e = System::rm(array('-rf', $fp))) || !$e) {
275                    return $e ? $e : PEAR::raiseError(sprintf(
276                            'Could not remove language "%s" from domain "%s" '.
277                            'in path "%s" (probably insufficient permissions)',
278                            $langID, $domain, $path
279                        ),
280                        TRANSLATION2_ERROR
281                    );
282                }
283            }
284        }
285        return true;
286    }
287
288    // }}}
289    // {{{ updateLang()
290
291    /**
292     * Update the lang info in the langs_avail file
293     *
294     * @param array $langData language data
295     *
296     * @return mixed Returns true on success or PEAR_Error on failure.
297     * @access public
298     */
299    function updateLang($langData)
300    {
301        if (PEAR::isError($changed = $this->_updateLangData($langData))) {
302            return $changed;
303        }
304        return $changed ? $this->_writeLangsAvailFile() : true;
305    }
306
307    // }}}
308    // {{{ getPageNames()
309
310    /**
311     * Get a list of all the domains
312     *
313     * @return array
314     * @access public
315     */
316    function getPageNames()
317    {
318        return array_keys($this->_domains);
319    }
320
321    // }}}
322    // {{{ begin()
323
324    /**
325     * Begin
326     *
327     * @return  void
328     * @access  public
329     */
330    function begin()
331    {
332        $this->_bulk = true;
333    }
334
335    // }}}
336    // {{{ commit()
337
338    /**
339     * Commit
340     *
341     * @return true|PEAR_Error on failure.
342     * @access public
343     */
344    function commit()
345    {
346        $this->_bulk = false;
347        if (isset($this->_queue['remove'])) {
348            if (PEAR::isError($e = $this->_remove($this->_queue['remove']))) {
349                return $e;
350            }
351        }
352        if (isset($this->_queue['add'])) {
353            if (PEAR::isError($e = $this->_add($this->_queue['add']))) {
354                return $e;
355            }
356        }
357        return true;
358    }
359
360    // }}}
361    // {{{ _add()
362
363    /**
364     * Add
365     *
366     * @param array &$bulk array('pageID' => array([languages]))
367     *
368     * @return true|PEAR_Error on failure.
369     * @access private
370     */
371    function _add(&$bulk)
372    {
373        include_once 'File/Gettext.php';
374        $gtFile = &File_Gettext::factory($this->options['file_type']);
375        $langs  = $this->getLangs('array');
376
377        foreach ((array) $bulk as $pageID => $languages) {
378            //create the new domain on demand
379            if (!isset($this->_domains[$pageID])) {
380                if (PEAR::isError($e = $this->_addDomain($pageID))) {
381                    return $e;
382                }
383            }
384            $path = $this->_domains[$pageID];
385            if ($path[strlen($path)-1] != '/' && $path[strlen($path)-1] != '\\') {
386                $path .= '/';
387            }
388            $file = '/LC_MESSAGES/'. $pageID .'.'. $this->options['file_type'];
389
390            foreach ($languages as $lang => $strings) {
391
392                if (is_file($path . $lang . $file)) {
393                    if (PEAR::isError($e = $gtFile->load($path . $lang . $file))) {
394                        return $e;
395                    }
396                }
397
398                if (!isset($gtFile->meta['Content-Type'])) {
399                    $gtFile->meta['Content-Type'] = 'text/plain; charset=';
400                    if (isset($langs[$lang]['encoding'])) {
401                        $gtFile->meta['Content-Type'] .= $langs[$lang]['encoding'];
402                    } else {
403                        $gtFile->meta['Content-Type'] .= $this->options['default_encoding'];
404                    }
405                }
406
407                foreach ($strings as $stringID => $string) {
408                    $gtFile->strings[$stringID] = $string;
409                }
410
411                if (PEAR::isError($e = $gtFile->save($path . $lang . $file))) {
412                    return $e;
413                }
414
415                //refresh cache
416                $this->cachedDomains[$lang][$pageID] = $gtFile->strings;
417            }
418        }
419
420        $bulk = null;
421        return true;
422    }
423
424    // }}}
425    // {{{ _remove()
426
427    /**
428     * Remove
429     *
430     * @param array &$bulk array('pageID' => array([languages]))
431     *
432     * @return true|PEAR_Error on failure.
433     * @access private
434     */
435    function _remove(&$bulk)
436    {
437        include_once 'File/Gettext.php';
438        $gtFile = &File_Gettext::factory($this->options['file_type']);
439
440        foreach ($this->getLangs('ids') as $lang) {
441            foreach ((array) $bulk as $pageID => $stringIDs) {
442                $file = sprintf(
443                    '%s/%s/LC_MESSAGES/%s.%s',
444                    $this->_domains[$pageID],
445                    $lang,
446                    $pageID,
447                    $this->options['file_type']
448                );
449
450                if (is_file($file)) {
451                    if (PEAR::isError($e = $gtFile->load($file))) {
452                        return $e;
453                    }
454
455                    foreach (array_keys($stringIDs) as $stringID) {
456                        unset($gtFile->strings[$stringID]);
457                    }
458
459                    if (PEAR::isError($e = $gtFile->save($file))) {
460                        return $e;
461                    }
462
463                    //refresh cache
464                    $this->cachedDomains[$lang][$pageID] = $gtFile->strings;
465                }
466            }
467        }
468
469        $bulk = null;
470        return true;
471    }
472
473    // }}}
474    // {{{ _addDomain()
475
476    /**
477     * Add the path-to-the-new-domain to the domains-path-INI-file
478     *
479     * @param string $pageID domain name
480     *
481     * @return true|PEAR_Error on failure
482     * @access private
483     */
484    function _addDomain($pageID)
485    {
486        $domain_path = count($this->_domains) ? reset($this->_domains) : 'locale/';
487
488        if (!is_resource($f = fopen($this->options['domains_path_file'], 'a'))) {
489            return $this->raiseError(sprintf(
490                    'Cannot write to domains path INI file "%s"',
491                    $this->options['domains_path_file']
492                ),
493                TRANSLATION2_ERROR_CANNOT_WRITE_FILE
494            );
495        }
496
497        $CRLF = $this->options['carriage_return'];
498
499        while (true) {
500            if (@flock($f, LOCK_EX)) {
501                fwrite($f, $CRLF . $pageID . ' = ' . $domain_path . $CRLF);
502                @flock($f, LOCK_UN);
503                fclose($f);
504                break;
505            }
506        }
507
508        $this->_domains[$pageID] = $domain_path;
509
510        return true;
511    }
512
513    // }}}
514    // {{{ _removeDomain()
515
516    /**
517     * Remove the path-to-the-domain from the domains-path-INI-file
518     *
519     * @param string $pageID domain name
520     *
521     * @return true|PEAR_Error on failure
522     * @access private
523     */
524    function _removeDomain($pageID)
525    {
526        $domain_path = count($this->_domains) ? reset($this->_domains) : 'locale/';
527
528        if (!is_resource($f = fopen($this->options['domains_path_file'], 'r+'))) {
529            return $this->raiseError(sprintf(
530                    'Cannot write to domains path INI file "%s"',
531                    $this->options['domains_path_file']
532                ),
533                TRANSLATION2_ERROR_CANNOT_WRITE_FILE
534            );
535        }
536
537        $CRLF = $this->options['carriage_return'];
538
539        while (true) {
540            if (@flock($f, LOCK_EX)) {
541                $pages = file($this->options['domains_path_file']);
542                foreach ($pages as $page) {
543                    if (preg_match('/^'.$pageID.'\s*=/', $page)) {
544                        //skip
545                        continue;
546                    }
547                    fwrite($f, $page . $CRLF);
548                }
549                fflush($f);
550                ftruncate($f, ftell($f));
551                @flock($f, LOCK_UN);
552                fclose($f);
553                break;
554            }
555        }
556
557        unset($this->_domains[$pageID]);
558
559        return true;
560    }
561
562    // }}}
563    // {{{ _writeLangsAvailFile()
564
565    /**
566     * Write the langs_avail INI file
567     *
568     * @return true|PEAR_Error on failure.
569     * @access private
570     */
571    function _writeLangsAvailFile()
572    {
573        if (PEAR::isError($langs = $this->getLangs())) {
574            return $langs;
575        }
576
577        if (!is_resource($f = fopen($this->options['langs_avail_file'], 'w'))) {
578            return $this->raiseError(sprintf(
579                    'Cannot write to available langs INI file "%s"',
580                    $this->options['langs_avail_file']
581                ),
582                TRANSLATION2_ERROR_CANNOT_WRITE_FILE
583            );
584        }
585        $CRLF = $this->options['carriage_return'];
586
587        @flock($f, LOCK_EX);
588
589        foreach ($langs as $id => $data) {
590            fwrite($f, '['. $id .']'. $CRLF);
591            foreach ($this->_fields as $k) {
592                if (isset($data[$k])) {
593                    fwrite($f, $k . ' = ' . $data[$k] . $CRLF);
594                }
595            }
596            fwrite($f, $CRLF);
597        }
598
599        @flock($f, LOCK_UN);
600        fclose($f);
601        return true;
602    }
603
604    // }}}
605    // {{{ _updateLangData()
606
607    /**
608     * Update Lang Data
609     *
610     * @param array $langData language data
611     *
612     * @return true|PEAR_Error on failure.
613     * @access private
614     */
615    function _updateLangData($langData)
616    {
617        if (PEAR::isError($langs = $this->getLangs())) {
618            return $langs;
619        }
620
621        $lang    = &$langs[$langData['lang_id']];
622        $changed = false;
623        foreach ($this->_fields as $k) {
624            if (    isset($langData[$k]) &&
625                    (!isset($lang[$k]) || $langData[$k] != $lang[$k])) {
626                $lang[$k] = $langData[$k];
627                $changed  = true;
628            }
629        }
630
631        if ($changed) {
632            $lang['id']  = $langData['lang_id'];
633            $this->langs = $langs;
634        }
635        return $changed;
636    }
637
638    // }}}
639}
640?>