<?php

# PLUGIN PREVIEW BY TEXTPATTERN.INFO

/**
 * smd_babel
 *
 * A Textpattern CMS plugin for managing language strings translations.
 *
 * @author Stef Dawson
 * @link   https://stefdawson.com/
 */
if (txpinterface === 'admin') {
    new 
smd_babel();
}

/**
 * Admin-side user interface.
 */
class smd_babel
{
    
/**
     * The plugin's event as registered in Txp.
     *
     * @var string
     */
    
protected $event 'smd_babel';

    
/**
     * Constructor to set up callbacks and environment.
     */
    
public function __construct()
    {
        
add_privs($this->event'1,2');
        
register_tab('admin'$this->eventgTxt('smd_babel'));
        
register_callback(array($this'smd_babel'), $this->event);
        
register_callback(array($this'inject_css'), 'admin_side''head_end');
    }

    
/**
     * Plugin jumpoff point.
     *
     * @param  string $evt Textpattern event
     * @param  string $stp Textpattern step (action)
     */
    
public function smd_babel($evt$stp)
    {
        
$available_steps = array(
            
'ui'         => false,
            
'fetchGroup' => true,
            
'save'       => true,
            
'delete'     => true,
            
'export'     => true,
        );

        if (!
$stp or !bouncer($stp$available_steps)) {
            
$stp 'ui';
        }

        
$this->$stp();
    }

    
/**
     * Inject style rules into the &lt;head&gt; of the page.
     *
     * @param  string $evt Textpattern event (panel)
     * @param  string $stp Textpattern step (action)
     * @return string      Style rules, or nothing if not the correct $event
     */
    
public function inject_css($evt$stp)
    {
        global 
$event;

        if (
$event === $this->event) {
            
$smd_babel_styles = <<<EOCSS
.smd_babel_string { width: 100%; }
.smd_babel_panel { float: right; }
.smd_babel_delete { cursor: pointer; }
textarea.smd_babel_string { min-height: auto; height: auto; }
EOCSS;

            echo 
'<style type="text/css">' $smd_babel_styles '</style>';
        }

        return;
    }

    
/**
     * Table of language strings by group.
     *
     * @param  string $message Flash message to display success/error
     * @return string          HTML
     */
    
public function ui($message '')
    {
        
pagetop(gTxt('smd_babel'), $message);
        
require_privs('smd_babel');

        
$groups safe_column('event''txp_lang''1 GROUP BY event');
        
$langObj Txp::get('\Textpattern\L10n\Lang');
        
$defaultOwner TEXTPATTERN_LANG_OWNER_SITE;

        
$activeLang $langObj->available(TEXTPATTERN_LANG_ACTIVE);
        
$activeIdentifier key($activeLang);

        
$siteLang get_pref('language'TEXTPATTERN_DEFAULT_LANGtrue);
        
$uiLang get_pref('language_ui'$siteLangtrue);
        
$selectedLang get_pref('smd_babel_lang'$activeIdentifier);
        
$baseLang $siteLang;

        
$primary $langObj->languageSelect('smd_babel_lang_xlate'$selectedLang);
        
$installed = (method_exists($langObj'languageList')) ? $langObj->languageList() : $this->languageList();

        
$showUI = ($uiLang !== $baseLang);
        
$uiSelect hInput('smd_babel_lang_ui'$uiLang);
        
$baseSelect hInput('smd_babel_lang_site'$baseLang);

        
$group get_pref('smd_babel_group''admin');
        
$groups selectInput('smd_babel_group'$groups$group);

        
$searchBlock n.tag(
            
'<div class="smd_babel_panel">
                <a class="txp-button smd_babel_add" href="#">' 
gTxt('add') . '</a>
                <a class="txp-button smd_babel_export" href="#">' 
gTxt('export') . '</a>
            </div>'
.n.
            
tag(
                
form(
                    
inputLabel('key'fInput('text', array('name' => 'key''required' => 1), ''), 'smd_babel_key').
                    
inputLabel('group'fInput('text', array('name' => 'group''required' => 1), $group), 'smd_babel_group').
                    
inputLabel('lang'selectInput('lang'$installed$selectedLangfalse), 'smd_babel_lang').
                    
inputLabel('value'fInput('text', array('name' => 'value''required' => 1), ''), 'smd_babel_value').
                    
hInput('smd_babel_added''1').
                    
fInput('submit''smd_babel_submit'gTxt('save'))
                    .
eInput($this->event)
                    .
sInput('save'),
                    
'',
                    
'',
                    
'post',
                    
'async'
                
),
                
'div', array(
                    
'class'      => 'smd_babel_addform',
                    
'aria-label' => gTxt('smd_babel_add_string'),
                    
'title'      => gTxt('smd_babel_add_string'),
                )).
n.
            
tag(
                
form(
                    
inputLabel('group'fInput('text', array('name' => 'group'), $group), 'smd_babel_group').
                    
inputLabel('lang'selectInput('lang'$installed$selectedLangfalse), 'smd_babel_lang').
                    
inputLabel('key'fInput('text', array('name' => 'key'), ''), 'smd_babel_key').
                    
fInput('submit''smd_babel_submit'gTxt('download'))
                    .
eInput($this->event)
                    .
sInput('export'),
                    
'',
                    
'',
                    
'post'
                
),
                
'div', array(
                    
'class'      => 'smd_babel_exportform',
                    
'aria-label' => gTxt('smd_babel_export_strings'),
                    
'title'      => gTxt('smd_babel_export_strings'),
                )),
            
'div', array(
                
'class' => 'txp-layout-4col-3span',
                
'id'    => $this->event.'_control',
            )
        );

        
$pageBlock '';
        
$total 1;
        
$criteria '';

        
// Three columns:
        // 1) Keys and their base language translations.
        // 2) Primary (current admin language) translations in 2nd column if base not
        //    in use on admin side.
        // 3) Translation column with lang selector at top to load the strings from that
        //    lang corresponding to the currently selected group (event). Editable.
        
$createBlock tag($groups'div', array('class' => 'txp-control-panel'));
        
$contentBlock tag_start('div', array('class' => 'txp-listtables')).
                
n.tag_start('table', array('class' => 'txp-list')).
                
n.tag_start('thead').
                
tr(
                    
hCell(gTxt('smd_babel_lang_site', array('{lang}' => $baseLang)).n.$baseSelectnull, array('class' => 'langCol '.$baseLang)).
                    (
                        (
$showUI)
                        ? 
hCell(gTxt('smd_babel_lang_ui', array('{lang}' => $uiLang)).n.$uiSelectnull, array('class' => 'langCol '.$uiLang))
                        : 
''
                    
).
                    
hCell(gTxt('smd_babel_lang_xlate').n.$primarynull, array('class' => 'langCol''width' => '40%'))
                ).
                
n.tag_end('thead').
                
n.tag_start('tbody', array('class' => 'smd_babel_table')).
                
n.tag_end('tbody').
                
n.tag_end('table').
                
n.tag_end('div');

        
$table = new \Textpattern\Admin\Table($this->event);
        echo 
$table->render(compact('total''criteria'), $searchBlock$createBlock$contentBlock$pageBlock);

        echo 
script_js(<<<EOJS
jQuery(function() {
    /**
     * Group change handler.
     */
    jQuery('select[name=smd_babel_group]').on('change', smd_babel_rebuild_table).change();

    /**
     * Language change handler.
     */
    jQuery('select[name=smd_babel_lang_xlate]').on('change', smd_babel_rebuild_table).change();

    /**
     * Add button handlers.
     */
    $(document).on('click', '.smd_babel_add', function (ev) {
        ev.preventDefault();
        $('.smd_babel_addform').dialog('open');
    });

    jQuery('.smd_babel_addform, .smd_babel_exportform').dialog({
        dialogClass: 'txp-tagbuilder-container',
        autoOpen: false,
        focus: function (ev, ui) {
            $(ev.target).closest('.ui-dialog input').focus();
        }
    });

    /**
     * Export button handlers.
     */
    $(document).on('click', '.smd_babel_export', function (ev) {
        ev.preventDefault();
        $('.smd_babel_exportform').dialog('open');
    });

    /**
     * Store the given string in the given language when input changes.
     */
    jQuery('.smd_babel_table').on('change', 'textarea', function() {
        var me = jQuery(this);
        var key = me.attr('name');
        var lng = me.data('lang');
        var val = me.val();
        var grp = jQuery('select[name=smd_babel_group]').val();

        sendAsyncEvent(
        {
            event: textpattern.event,
            step: 'save',
            key: key,
            lang: lng,
            group: grp,
            value: val
        }, function (data) {
            textpattern.Console.addMessage([data.msg, 0], 'smd_babel').announce('smd_babel');
        },
        'json');
    });

    /**
     * Delete the given string in the given language.
     */
    jQuery('.smd_babel_table').on('click', '.smd_babel_delete', function(ev) {
        ev.preventDefault();
        var me = jQuery(this);
        var key = me.data('key');
        var lng = jQuery('select[name=smd_babel_lang_xlate]').val();
        var grp = jQuery('select[name=smd_babel_group]').val();

        sendAsyncEvent(
        {
            event: textpattern.event,
            step: 'delete',
            key: key,
            lang: lng,
            group: grp
        }, function (data) {
            textpattern.Console.addMessage([data.msg, 0], 'smd_babel').announce('smd_babel');
            me.closest('tr').remove();
        },
        'json');
    });

    /**
     * Fetch new strings and reconstruct the main table body.
     */
    function smd_babel_rebuild_table() {
        var grp = jQuery('select[name=smd_babel_group]').val();
        var langs = [];

        jQuery('.langCol').each(function() {
            langs.push(jQuery(this).find('input, select').val());
        });

        sendAsyncEvent(
        {
            event: textpattern.event,
            step: 'fetchGroup',
            group: grp,
            langs: langs
        }, function (data) {
            var keys = [];
            var owners = [];
            var strings = [];

            // Loop through the languages and extract matching values with translations.
            jQuery.each(langs, function(idx, lng) {
                strings[lng] = [];

                jQuery.each(data[lng], function(key, str) {
                    // Assume base language contains all keys: faulty assumption in
                    // some cases but there's no guarantee English is installed.
                    if (idx === 0) {
                        keys.push(key);
                    }

                    owners.push(str.owner);

                    // Guard against languages with missing keys.
                    if (keys.includes(key)) {
                        strings[lng].push(str.data);
                    } else {
                        strings[lng].push('');
                    }
                });
            });

            // Reconstruct the table.
            var tbl = jQuery('.smd_babel_table');
            tbl.empty();
            var selectedLang = '';

            jQuery.each(keys, function(idx, key) {
                var row = [];
                var maxLangs = langs.length - 1;
                var link = '';

                var canDelete = (owners[idx] === '
{$defaultOwner}');

                jQuery.each(langs, function(jdx, lng) {
                    if (jdx === maxLangs) {
                        // Todo: escape strings in case they contain double quotes.
                        selectedLang = lng;
                        link = '<textarea name="'+key+'" class="smd_babel_string" data-lang="'+lng+'">'+strings[lng][idx]+'</textarea>';
                    } else {
                        link = (jdx === 0 && (canDelete ? '<a class="smd_babel_delete ui-icon ui-icon-close" data-key="'+key+'">x</a>&nbsp;' : '')) + strings[lng][idx] + ((jdx === 0) ? '<br/><span class="txp-form-field-instructions">' + key + '</span>': '');
                    }
                    row.push('<td>'+ link + '</td>');
                });

                tbl.append('<tr>'+row.join(' ')+'</tr>');
            });

            // Resync the language and group selectors in the Add form in case they've changed.
            jQuery('.smd_babel_addform, .smd_babel_exportform').find('[name=lang]').val(selectedLang).prop('selected', true);
            jQuery('.smd_babel_addform, .smd_babel_exportform').find('[name=group]').val(grp);
        },
        'json');
    }
})
EOJS
        );
    }

    
/**
     * Ajax: Fetch all Textpack strings from the given group.
     *
     * Requires POST variables:
     *  param  string group The language group (event)
     *  param  array  langs The language (refs) to fetch
     * @return array        JSON response
     */
    
public function fetchGroup()
    {
        
$grp doSlash(ps('group'));
        
$lng doSlash(ps('langs'));

        if (!
$lng) {
            
$lng TEXTPATTERN_DEFAULT_LANG;
        }

        
$lng = (array) $lng;
        
$last_lang end($lng);

        
$lang_list "lang IN (".implode(','quote_list($lng)).")";

        
$rs safe_rows('name, lang, data, owner''txp_lang'"event='$grp' AND $lang_list ORDER BY name");
        
$out = array();

        foreach (
$rs as $row) {
            
$out[$row['lang']][$row['name']] = array('data' => $row['data'], 'owner' => $row['owner']);
        }

        
set_pref('smd_babel_group'$grp'smd_babel'PREF_HIDDEN''0PREF_PRIVATE);
        
set_pref('smd_babel_lang'$last_lang'smd_babel'PREF_HIDDEN''0PREF_PRIVATE);

        echo 
json_encode($out);
    }

    
/**
     * Ajax: Save (overwrite) the given string with the new translation.
     *
     * Requires POST variables:
     *  param  string key   The key name to change
     *  param  string lang  The language (ref) to alter
     *  param  string group The language group (event)
     *  param  string value The new string value
     * @return array        JSON response
     */
    
public function save()
    {
        
$key ps('key');
        
$lng ps('lang');
        
$grp ps('group');
        
$val ps('value');
        
$isAdd ps('smd_babel_added');

        
$langObj Txp::get('\Textpattern\L10n\Lang');
        
$installed $langObj->installed();
        
$msg '';

        if (!
in_array($lng$installed)) {
            
$msg gTxt('smd_babel_language_not_installed', array('{name}' => $lng));
        } else {
            
$ret safe_upsert(
                
'txp_lang',
                array(
'data' => $val'event' => $grp'lastmod' => 'NOW()''owner' => TEXTPATTERN_LANG_OWNER_SITE),
                array(
'name' => $key'lang' => $lng)
            );

            
$this->syncKeys($key$grp$lng);

            if (
$ret) {
                
$msg gTxt('smd_babel_string_updated', array('{name}' => $key));
            }
        }

        if (
$isAdd) {
            
// Ajax form submission.
            
send_script_response('textpattern.Console.addMessage(['.json_encode($msgTEXTPATTERN_JSON).', 0], "smd_babel").announce("smd_babel");').n;
        } else {
            
// JSON Ajax call.
            
echo json_encode(array('msg' => $msg));
        }

        return;
    }

    
/**
     * Ajax: Delete a string if it exists and is non-core.
     *
     * Requires POST variables:
     *  param  string key   The key name to change
     *  param  string lang  The language (ref) to alter
     *  param  string group The language group (event)
     * @return array        JSON response
     */
    
public function delete()
    {
        
$msg '';
        
$lng doSlash(ps('lang'));
        
$grp doSlash(ps('group'));
        
$key doSlash(ps('key'));

        
$exists safe_field('name''txp_lang'"name='$key' AND lang='$lng' AND event='$grp' AND owner='" TEXTPATTERN_LANG_OWNER_SITE  "'");

        if (
$exists) {
            
$safe_exists doSlash($exists);
            
$done safe_delete('txp_lang'"name='$safe_exists' AND lang='$lng' AND event='$grp' AND owner='" TEXTPATTERN_LANG_OWNER_SITE  "'");

            if (
$done) {
                
$msg gTxt('smd_babel_string_deleted', array('{name}' => $exists));
            }
        }

        echo 
json_encode(array('msg' => $msg));

        return;
    }

    
/**
     * Ajax: Export a string set as a file.
     *
     * Available POST variables:
     *  param  string lang  The language (ref) to export
     *  param  string group The group (event) to export. Empty = all
     *  param  string key   The partial key (name) to match. Empty = ignored
     *
     * @return string
     */
    
public function export()
    {
        
$lng ps('lang');
        
$grp ps('group');
        
$key ps('key');

        
$grpClause = ($grp) ? " AND event IN (".implode(','quote_list(do_list($grp))).")" '';
        
$keyClause = ($key) ? " AND name LIKE '%" doSlash($key) . "%'" '';
        
$rs safe_rows('event, name, data''txp_lang'"lang='".doSlash($lng)."'".$grpClause.$keyClause." ORDER BY event, name");

        
$out $this->createIni($rs);

        
set_headers(array(
            
'content-type' => 'text/plain',
            
'content-disposition' => 'attachment; filename="'.$lng.'.ini"',
            
'content-description' => 'File Download',
            
'content-length' => count($out),
            
// Fix for IE6 PDF bug on servers configured to send cache headers.
            
'cache-control' => 'private'
        
));

        echo 
implode(n$out);

        exit;
    }

    
/**
     * Ensure any new keys are represented in all languages.
     *
     * @param string $key Key to check
     * @param string $grp Group (event) in which the key belongs
     * @param string $lng Language that's been saved already
     */
    
protected function syncKeys($key$grp$lng)
    {
        
$langObj Txp::get('\Textpattern\L10n\Lang');
        
$installed $langObj->installed();

        foreach (
$installed as $lang) {
            if (
$lang === $lng) {
                continue;
            }

            
safe_upsert(
                
'txp_lang',
                array(
'event' => $grp'lastmod' => 'NOW()''owner' => TEXTPATTERN_LANG_OWNER_SITE),
                array(
'name' => $key'lang' => $lang)
            );
        }

        return;
    }

    
/**
     * Convert an array into .ini language file format.
     *
     * @param  array  $rs    The record set to store
     * @todo   Come the revolution, put something like this in Textpack\Parser.php?
     * @return [type]        [description]
     */
    
protected function createIni($rs)
    {
        
$res = array();
        
$lastGrp '';

        foreach (
$rs as $row) {
            if (
is_array($row)) {
                
$grp $row['event'];

                if (
$grp != $lastGrp) {
                    
$res[] = "[$grp]";
                }

                
$name $row['name'];
                
$data $row['data'];

                
$res[] = "$name = \"".$data."\"";
                
$lastGrp $grp;
            }
        }

        return 
$res;
    }

    
/**
     * Return a list of available lanugages in Txp.
     *
     * Polyfill for Txp < 4.7.3.
     *
     * @param  int    $flags Logical OR list of flags indiacting the type of list to return:
     *                       TEXTPATTERN_LANG_ACTIVE: the active language
     *                       TEXTPATTERN_LANG_INSTALLED: all installed languages
     *                       TEXTPATTERN_LANG_AVAILABLE: all available languages in the file system
     * @return array
     */
    
protected function languageList($flags null)
    {
        if (
$flags === null) {
            
$flags TEXTPATTERN_LANG_ACTIVE TEXTPATTERN_LANG_INSTALLED;
        }

        
$installed_langs Txp::get('\Textpattern\L10n\Lang')->available((int)$flags);
        
$vals = array();

        foreach (
$installed_langs as $lang => $langdata) {
            
$vals[$lang] = $langdata['name'];

            if (
trim($vals[$lang]) == '') {
                
$vals[$lang] = $lang;
            }
        }

        
ksort($vals);
        
reset($vals);

        return 
$vals;
    }
}