<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Contains the Translation2_Admin_Container_gettext class
 *
 * PHP versions 4 and 5
 *
 * LICENSE: Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category  Internationalization
 * @package   Translation2
 * @author    Lorenzo Alberton <l.alberton@quipo.it>
 * @author    Michael Wallner <mike@php.net>
 * @copyright 2004-2007 Lorenzo Alberton, Michael Wallner
 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @version   CVS: $Id$
 * @link      http://pear.php.net/package/Translation2
 */

/**
 * require Translation2_Container_gettext class
 */
require_once 'Translation2/Container/gettext.php';

/**
 * Storage driver for storing/fetching data to/from a gettext file
 *
 * This storage driver requires the gettext extension
 *
 * @category  Internationalization
 * @package   Translation2
 * @author    Lorenzo Alberton <l.alberton@quipo.it>
 * @author    Michael Wallner <mike@php.net>
 * @copyright 2004-2007 Lorenzo Alberton, Michael Wallner
 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @version   CVS: $Id$
 * @link      http://pear.php.net/package/Translation2
 */
class Translation2_Admin_Container_gettext extends Translation2_Container_gettext
{
    // {{{ class vars

    var $_bulk   = false;
    var $_queue  = array();
    var $_fields = array('name', 'meta', 'error_text', 'encoding');

    // }}}
    // {{{ addLang()

    /**
     * Creates a new entry in the langs_avail .ini file.
     *
     * @param array  $langData language data
     * @param string $path     path to gettext data dir
     *
     * @return  mixed   Returns true on success or PEAR_Error on failure.
     */
    function addLang($langData, $path = null)
    {
        if (!isset($path) || !is_string($path)) {
            $path = $this->_domains[$this->options['default_domain']];
        }

        $path .= '/'. $langData['lang_id'] . '/LC_MESSAGES';

        if (!is_dir($path)) {
            include_once 'System.php';
            if (!System::mkdir(array('-p', $path))) {
                return $this->raiseError(sprintf(
                        'Cannot create new language in path "%s"', $path
                    ),
                    TRANSLATION2_ERROR_CANNOT_CREATE_DIR
                );
            }
        }

        return true;
    }

    // }}}
    // {{{ addLangToList()

    /**
     * Creates a new entry in the langsAvail .ini file.
     * If the file doesn't exist yet, it is created.
     *
     * @param array $langData array('lang_id'    => 'en',
     *                              'name'       => 'english',
     *                              'meta'       => 'some meta info',
     *                              'error_text' => 'not available'
     *                              'encoding'   => 'iso-8859-1',
     * );
     *
     * @return true|PEAR_Error on failure
     */
    function addLangToList($langData)
    {
        if (PEAR::isError($changed = $this->_updateLangData($langData))) {
            return $changed;
        }
        return $changed ? $this->_writeLangsAvailFile() : true;
    }

    // }}}
    // {{{ add()

    /**
     * Add a new entry in the strings domain.
     *
     * @param string $stringID string ID
     * @param string $pageID   page/group ID
     * @param array  $strings  Associative array with string translations.
     *               Sample format:  array('en' => 'sample', 'it' => 'esempio')
     *
     * @return true|PEAR_Error on failure
     */
    function add($stringID, $pageID, $strings)
    {
        if (!isset($pageID)) {
            $pageID = $this->options['default_domain'];
        }

        $langs = array_intersect(array_keys($strings), $this->getLangs('ids'));

        if (!count($langs)) {
            return true; // really?
        }

        if ($this->_bulk) {
            foreach ($strings as $lang => $string) {
                if (in_array($lang, $langs)) {
                    $this->_queue['add'][$pageID][$lang][$stringID] = $string;
                }
            }
            return true;
        } else {
            $add = array();
            foreach ($strings as $lang => $string) {
                if (in_array($lang, $langs)) {
                    $add[$pageID][$lang][$stringID] = $string;
                }
            }
            return $this->_add($add);
        }
    }

    // }}}
    // {{{ remove()

    /**
     * Remove an entry from the domain.
     *
     * @param string $stringID string ID
     * @param string $pageID   page/group ID
     *
     * @return true|PEAR_Error on failure
     */
    function remove($stringID, $pageID)
    {
        if (!isset($pageID)) {
            $pageID = $this->options['default_domain'];
        }

        if ($this->_bulk) {
            $this->_queue['remove'][$pageID][$stringID] = true;
            return true;
        } else {
            $tmp = array($pageID => array($stringID => true));
            return $this->_remove($tmp);
        }

    }

    // }}}
    // {{{ removePage

    /**
     * Remove all the strings in the given page/group (domain)
     *
     * @param string $pageID page/group ID
     * @param string $path   path to gettext data dir
     *
     * @return mixed true on success, PEAR_Error on failure
     */
    function removePage($pageID = null, $path = null)
    {
        if (!isset($pageID)) {
            $pageID = $this->options['default_domain'];
        }

        if (!isset($path)) {
            if (!empty($this->_domains[$pageID])) {
                $path = $this->_domains[$pageID];
            } else {
                $path = $this->_domains[$this->options['default_domain']];
            }
        }

        if (PEAR::isError($e = $this->_removeDomain($pageID))) {
            return $e;
        }
        
        $this->fetchLangs();
        foreach ($this->langs as $langID => $lang) {
            $domain_file = $path .'/'. $langID .'/LC_MESSAGES/'. $pageID .'.';
            if (!@unlink($domain_file.'mo') || !@unlink($domain_file.'po')) {
                return $this->raiseError('Cannot delete page ' . $pageID. ' (file '.$domain_file.'.*)',
                    TRANSLATION2_ERROR
                );
            }
        }

        return true;
    }

    // }}}
    // {{{ update()

    /**
     * Update
     *
     * Alias for Translation2_Admin_Container_gettext::add()
     *
     * @param string $stringID string ID
     * @param string $pageID   page/group ID
     * @param array  $strings  strings
     *
     * @return  mixed
     * @access  public
     * @see add()
     */
    function update($stringID, $pageID, $strings)
    {
        return $this->add($stringID, $pageID, $strings);
    }

    // }}}
    // {{{ removeLang()

    /**
     * Remove Language
     *
     * @param string $langID language ID
     * @param bool   $force  (unused)
     *
     * @return true|PEAR_Error
     * @access public
     */
    function removeLang($langID, $force = false)
    {
        include_once 'System.php';
        foreach ((array) $this->_domains as $domain => $path) {
            if (is_dir($fp = $path .'/'. $langID)) {
                if (PEAR::isError($e = System::rm(array('-rf', $fp))) || !$e) {
                    return $e ? $e : PEAR::raiseError(sprintf(
                            'Could not remove language "%s" from domain "%s" '.
                            'in path "%s" (probably insufficient permissions)',
                            $langID, $domain, $path
                        ),
                        TRANSLATION2_ERROR
                    );
                }
            }
        }
        return true;
    }

    // }}}
    // {{{ updateLang()

    /**
     * Update the lang info in the langs_avail file
     *
     * @param array $langData language data
     *
     * @return mixed Returns true on success or PEAR_Error on failure.
     * @access public
     */
    function updateLang($langData)
    {
        if (PEAR::isError($changed = $this->_updateLangData($langData))) {
            return $changed;
        }
        return $changed ? $this->_writeLangsAvailFile() : true;
    }

    // }}}
    // {{{ getPageNames()

    /**
     * Get a list of all the domains
     *
     * @return array
     * @access public
     */
    function getPageNames()
    {
        return array_keys($this->_domains);
    }

    // }}}
    // {{{ begin()

    /**
     * Begin
     *
     * @return  void
     * @access  public
     */
    function begin()
    {
        $this->_bulk = true;
    }

    // }}}
    // {{{ commit()

    /**
     * Commit
     *
     * @return true|PEAR_Error on failure.
     * @access public
     */
    function commit()
    {
        $this->_bulk = false;
        if (isset($this->_queue['remove'])) {
            if (PEAR::isError($e = $this->_remove($this->_queue['remove']))) {
                return $e;
            }
        }
        if (isset($this->_queue['add'])) {
            if (PEAR::isError($e = $this->_add($this->_queue['add']))) {
                return $e;
            }
        }
        return true;
    }

    // }}}
    // {{{ _add()

    /**
     * Add
     *
     * @param array &$bulk array('pageID' => array([languages]))
     *
     * @return true|PEAR_Error on failure.
     * @access private
     */
    function _add(&$bulk)
    {
        include_once 'File/Gettext.php';
        $gtFile = &File_Gettext::factory($this->options['file_type']);
        $langs  = $this->getLangs('array');

        foreach ((array) $bulk as $pageID => $languages) {
            //create the new domain on demand
            if (!isset($this->_domains[$pageID])) {
                if (PEAR::isError($e = $this->_addDomain($pageID))) {
                    return $e;
                }
            }
            $path = $this->_domains[$pageID];
            if ($path[strlen($path)-1] != '/' && $path[strlen($path)-1] != '\\') {
                $path .= '/';
            }
            $file = '/LC_MESSAGES/'. $pageID .'.'. $this->options['file_type'];

            foreach ($languages as $lang => $strings) {

                if (is_file($path . $lang . $file)) {
                    if (PEAR::isError($e = $gtFile->load($path . $lang . $file))) {
                        return $e;
                    }
                }

                if (!isset($gtFile->meta['Content-Type'])) {
                    $gtFile->meta['Content-Type'] = 'text/plain; charset=';
                    if (isset($langs[$lang]['encoding'])) {
                        $gtFile->meta['Content-Type'] .= $langs[$lang]['encoding'];
                    } else {
                        $gtFile->meta['Content-Type'] .= $this->options['default_encoding'];
                    }
                }

                foreach ($strings as $stringID => $string) {
                    $gtFile->strings[$stringID] = $string;
                }

                if (PEAR::isError($e = $gtFile->save($path . $lang . $file))) {
                    return $e;
                }

                //refresh cache
                $this->cachedDomains[$lang][$pageID] = $gtFile->strings;
            }
        }

        $bulk = null;
        return true;
    }

    // }}}
    // {{{ _remove()

    /**
     * Remove
     *
     * @param array &$bulk array('pageID' => array([languages]))
     *
     * @return true|PEAR_Error on failure.
     * @access private
     */
    function _remove(&$bulk)
    {
        include_once 'File/Gettext.php';
        $gtFile = &File_Gettext::factory($this->options['file_type']);

        foreach ($this->getLangs('ids') as $lang) {
            foreach ((array) $bulk as $pageID => $stringIDs) {
                $file = sprintf(
                    '%s/%s/LC_MESSAGES/%s.%s',
                    $this->_domains[$pageID],
                    $lang,
                    $pageID,
                    $this->options['file_type']
                );

                if (is_file($file)) {
                    if (PEAR::isError($e = $gtFile->load($file))) {
                        return $e;
                    }

                    foreach (array_keys($stringIDs) as $stringID) {
                        unset($gtFile->strings[$stringID]);
                    }

                    if (PEAR::isError($e = $gtFile->save($file))) {
                        return $e;
                    }

                    //refresh cache
                    $this->cachedDomains[$lang][$pageID] = $gtFile->strings;
                }
            }
        }

        $bulk = null;
        return true;
    }

    // }}}
    // {{{ _addDomain()

    /**
     * Add the path-to-the-new-domain to the domains-path-INI-file
     *
     * @param string $pageID domain name
     *
     * @return true|PEAR_Error on failure
     * @access private
     */
    function _addDomain($pageID)
    {
        $domain_path = count($this->_domains) ? reset($this->_domains) : 'locale/';

        if (!is_resource($f = fopen($this->options['domains_path_file'], 'a'))) {
            return $this->raiseError(sprintf(
                    'Cannot write to domains path INI file "%s"',
                    $this->options['domains_path_file']
                ),
                TRANSLATION2_ERROR_CANNOT_WRITE_FILE
            );
        }

        $CRLF = $this->options['carriage_return'];

        while (true) {
            if (@flock($f, LOCK_EX)) {
                fwrite($f, $CRLF . $pageID . ' = ' . $domain_path . $CRLF);
                @flock($f, LOCK_UN);
                fclose($f);
                break;
            }
        }

        $this->_domains[$pageID] = $domain_path;

        return true;
    }

    // }}}
    // {{{ _removeDomain()

    /**
     * Remove the path-to-the-domain from the domains-path-INI-file
     *
     * @param string $pageID domain name
     *
     * @return true|PEAR_Error on failure
     * @access private
     */
    function _removeDomain($pageID)
    {
        $domain_path = count($this->_domains) ? reset($this->_domains) : 'locale/';

        if (!is_resource($f = fopen($this->options['domains_path_file'], 'r+'))) {
            return $this->raiseError(sprintf(
                    'Cannot write to domains path INI file "%s"',
                    $this->options['domains_path_file']
                ),
                TRANSLATION2_ERROR_CANNOT_WRITE_FILE
            );
        }

        $CRLF = $this->options['carriage_return'];

        while (true) {
            if (@flock($f, LOCK_EX)) {
                $pages = file($this->options['domains_path_file']);
                foreach ($pages as $page) {
                    if (preg_match('/^'.$pageID.'\s*=/', $page)) {
                        //skip
                        continue;
                    }
                    fwrite($f, $page . $CRLF);
                }
                fflush($f);
                ftruncate($f, ftell($f));
                @flock($f, LOCK_UN);
                fclose($f);
                break;
            }
        }

        unset($this->_domains[$pageID]);

        return true;
    }

    // }}}
    // {{{ _writeLangsAvailFile()

    /**
     * Write the langs_avail INI file
     *
     * @return true|PEAR_Error on failure.
     * @access private
     */
    function _writeLangsAvailFile()
    {
        if (PEAR::isError($langs = $this->getLangs())) {
            return $langs;
        }

        if (!is_resource($f = fopen($this->options['langs_avail_file'], 'w'))) {
            return $this->raiseError(sprintf(
                    'Cannot write to available langs INI file "%s"',
                    $this->options['langs_avail_file']
                ),
                TRANSLATION2_ERROR_CANNOT_WRITE_FILE
            );
        }
        $CRLF = $this->options['carriage_return'];

        @flock($f, LOCK_EX);

        foreach ($langs as $id => $data) {
            fwrite($f, '['. $id .']'. $CRLF);
            foreach ($this->_fields as $k) {
                if (isset($data[$k])) {
                    fwrite($f, $k . ' = ' . $data[$k] . $CRLF);
                }
            }
            fwrite($f, $CRLF);
        }

        @flock($f, LOCK_UN);
        fclose($f);
        return true;
    }

    // }}}
    // {{{ _updateLangData()

    /**
     * Update Lang Data
     *
     * @param array $langData language data
     *
     * @return true|PEAR_Error on failure.
     * @access private
     */
    function _updateLangData($langData)
    {
        if (PEAR::isError($langs = $this->getLangs())) {
            return $langs;
        }

        $lang    = &$langs[$langData['lang_id']];
        $changed = false;
        foreach ($this->_fields as $k) {
            if (    isset($langData[$k]) &&
                    (!isset($lang[$k]) || $langData[$k] != $lang[$k])) {
                $lang[$k] = $langData[$k];
                $changed  = true;
            }
        }

        if ($changed) {
            $lang['id']  = $langData['lang_id'];
            $this->langs = $langs;
        }
        return $changed;
    }

    // }}}
}
?>