* @license PHP License
* @package WB
* @subpackage base
*/
WBClass::load('WBFormProcessor');
/**
* Markup Wxml
*
* load and save WXML markup string
*
* @todo configure groups and permissions: editor, chief editor, translators etc.
*
* @version 1.0.0
* @package WB
* @subpackage base
*/
class WBMarkup_Wxml extends WBFormProcessor
{
/**
* whether to use template engine or not
* @var bool
*/
protected $withFormTmpl = true;
/**
* markup scanner
* @var WBMarkup_Scanner
*/
private $scanner;
/**
* markup scanner handler
* @var WBMarkup_Handler_Xml2Html
*/
private $handler;
/**
* table
* @var WBDatasource_Table
*/
private $table;
/**
* template engine
* @var patTemplate
*/
protected $tmpl;
/**
* location of template files
* @var string
*/
private $tmplDir = 'Wxml';
/**
* user
* @var WBUser_Auth
*/
private $user;
/**
* md5 checksum of current content
* @var string
*/
private $cs;
/**
* actual content
* @var string
*/
private $content;
/**
* content's revision
* @var int
*/
private $revision;
/**
* locale setting
* @var WBConfig
*/
private $locale;
/**
* exml settings
* @var WBConfig
*/
private $config;
/**
* language dictionary
* @var WBDictionary_Lanaguage
*/
private $lang;
/**
* session container
* @var WBUser_Session
*/
private $sess;
/**
* constructor
*
*
*/
public function __construct()
{
$this->table = WBClass::create('WBDatasource_Table');
$this->user = WBUser::getCurrent();
$this->tmpl = WBClass::create('patTemplate');
$this->sess = WBClass::create('WBUser_Session');
// load locale config
$this->locale = WBClass::create('WBConfig');
$this->locale->load('locale');
$this->config = WBClass::create('WBConfig');
$this->config->load('wxml/config');
$this->lang = WBClass::create('WBDictionary_Language');
}
/**
* load content by checksum or given content
*
* Set internal article
*
* @param string $cs
* @param string $content
*/
public function load($cs, $content = '')
{
$this->content = $content;
$this->cs = $cs;
$this->revision = 0;
$this->loadCurrent();
}
/**
* set WXML url
*
* store current URL in session
* @param string $url
*/
public function setUrl($url)
{
if (empty($this->cs)) {
WBClass::load('WBException_Call');
throw new WBException_Call('Set URL requires loaded content - call load() first.', 1, __CLASS__);
return;
}
$this->sess->set(__CLASS__ . '/' . $this->cs . '/url/current', $url);
}
/**
* Get URL of current content snippet
*
* @return string
*/
public function getUrl()
{
return $this->sess->get(__CLASS__ . '/' . $this->cs. '/url/current');
}
/**
* Set user group for editors
*
* This allows to set an other user group then the one in config as editors
*
* @param string $group
* @throws WBException_Call
*/
public function setEditorGroup($group = '')
{
if (empty($this->cs)) {
WBClass::load('WBException_Call');
throw new WBException_Call('Call load() first', __CLASS__, 1);
}
if (empty($group)) {
$this->sess->clear(__CLASS__ . '/' . $this->cs . '/group/editor');
return;
}
$this->sess->set(__CLASS__ . '/' . $this->cs . '/group/editor', $group);
}
/**
* Set class name for envelope div
*
* Inject HTML attribute class="someclass otherclass" to surrunding DIV container
*
* @param string $group
* @throws WBException_Call
*/
public function setClassName($class = '')
{
if (empty($this->cs)) {
WBClass::load('WBException_Call');
throw new WBException_Call('Call load() first', __CLASS__, 2);
}
if (empty($class)) {
$this->sess->clear(__CLASS__ . '/' . $this->cs . '/attributes/class');
return;
}
$this->sess->set(__CLASS__ . '/' . $this->cs . '/attributes/class', $class);
}
/**
* convert to HTML output
*
* Use markup scannern and handler to transform xml content.
* Also add editor button.
*
* @return string
*/
public function getHtml()
{
$lang = $this->locale->get('locale/languages/origin', null);
if (class_exists('patI18n', false)) {
$lang = patI18n::getLocale(patI18n::LOCALE_TYPE_COMPLETE);
}
if ($lang == $this->locale->get('locale/languages/origin', null)) {
// no translation for original content required
$html = $this->toHtml($this->content);
$this->addEditorButton($html);
return $html;
}
// get translation of selected langauge
$trans = $this->getTranslation($lang);
// use fallback language
if ($trans['revision'] == -1) {
$default = $this->locale->get('locale/languages/default', $lang);
if ($default != $lang) {
$trans = $this->getTranslation($default);
}
}
$html = $this->toHtml($trans['body']);
$this->addEditorButton($html);
return $html;
}
/**
* transform XML to HTML
*
* Use scanner and handler to convert XML content. Put parsed content in
* envelope DIV container and add classname, if any.
*
* @see setClassName()
* @param string $content
* @return string
*/
protected function toHtml($content)
{
if (!$this->scanner) {
$this->scanner = WBClass::create('WBMarkup_Scanner');
$this->handler = WBClass::create('WBMarkup_Handler_Xml2Html');
$this->scanner->setHandler($this->handler);
}
$this->scanner->scan($content);
$class = '';
if ($this->sess->has(__CLASS__ . '/' . $this->cs . '/attributes/class')) {
$class = sprintf(' class="%s"', htmlspecialchars($this->sess->get(__CLASS__ . '/' . $this->cs . '/attributes/class')));
}
return sprintf('
%s
', $class, $this->handler->getParsedContent());
}
/**
* add "edit" button
*
* See whether current user my edit content. If so, add GUI-element to edit
* curren content.
*
* @param string $html
*/
private function addEditorButton(&$html)
{
$editor = $this->isUserEditor();
if (!$editor && !$this->isUserTranslator()) {
return;
}
$shared = $this->config->get('sharedvfs/user');
if (!empty($shared)) {
$sess = WBClass::create('patSession');
/** @var $sess patSession_Storage */
$sess->set('vfsfile_user', $shared);
}
$this->loadTemplates('editable');
$available = $this->locale->get('locale/languages/available', array());
$globals = array(
'cs' => $this->cs,
'content' => $html,
'available' => count($available),
'editable' => $editor
);
$this->tmpl->addGlobalVars($globals);
$origin = $this->locale->get('locale/languages/origin', 'en_GB');
$this->lang->load($origin);
$this->tmpl->addGlobalVars($this->lang->get(), 'LANG_ORIGIN_');
$this->lang->load($this->locale->get('locale/languages/default', 'en_GB'));
$this->tmpl->addGlobalVars($this->lang->get(), 'LANG_DEFAULT_');
$list = array();
foreach ($available as $a) {
if ($a == $origin) {
continue;
}
// load language information
$this->lang->load($a);
$data = $this->lang->get();
// add article status
$trans = $this->getTranslation($a);
$data['status'] = 'ok';
if ($this->revision != $trans['revision']) {
$data['status'] = 'fuzzy';
} else if ($trans['revision'] == -1) {
$data['status'] = 'missing';
}
$data['editable'] = intval($this->isUserTranslator($a));
$list[] = $data;
}
$this->tmpl->addRows('lang_available_list_entry', $list);
$html = $this->tmpl->getParsedTemplate('snippet');
}
/**
* save content
*
* Remove old drafts and save new content (either public or as draft)
*
* @param string $content
* @param bool $public mark as public or save as draft
*/
private function save($content, $public = true)
{
// remove drafts
$clause = array();
$clause[] = array(
'field' => 'cs',
'value' => $this->cs
);
$clause[] = array(
'field' => 'status',
'value' => 'draft'
);
$this->table->delete('xmlarticle', null, null, $clause);
$save = array(
'cs' => $this->cs,
'body' => $content,
'status' => 'draft',
$this->table->getIdentifier('user') => $this->user->getId()
);
$event = 'xmlarticle:draft:saved';
if($public) {
++$this->revision;
$save['status'] = 'public';
$save['revision'] = $this->revision;
$event = 'xmlarticle:public:saved';
}
$this->content = $content;
$this->table->save('xmlarticle', '__new', $save);
// add url and trigger event
$save['url'] = $this->getUrl();
WBEvent::trigger($event, 'Saved XML article as {STATUS}.', $save);
}
/**
* display editor
*
* Edit original content
*
* @return string HTML
*/
public function edit()
{
if (!$this->isUserEditor()) {
$this->loadTemplates('accessdenied');
return $this->tmpl->getParsedTemplate('snippet');
}
$values = array(
'body' => $this->content
);
$this->tmpl->addGlobalVar('cs', $this->cs);
return $this->processForm('edit', $values);
}
/**
* on successful form processing
*
* Save values, transform updated content to HTML and
* add it to template engine
*
* @param patForms $form
* @param array $values
* @return bool always true
*/
protected function onEditValid($form, $values)
{
$this->save($values['body']);
$html = $this->toHtml($this->content);
$this->addEditorButton($html);
$this->tmpl->addGlobalVar('content', $html);
return true;
}
/**
* edit translation of original content
*
* @param string $lang
* @return string HTML
*/
public function translate($lang = null)
{
if (!$this->isUserEditor() && !$this->isUserTranslator($lang)) {
$this->loadTemplates('accessdenied');
return $this->tmpl->getParsedTemplate('snippet');
}
$available = $this->locale->get('locale/languages/available', array());
if (!in_array($lang, $available)) {
$this->loadTemplates('badrequest');
return $this->tmpl->getParsedTemplate('snippet');
}
$origin = array(
'revision' => $this->revision,
'body' => $this->toHtml($this->content),
'lang' => $this->locale->get('locale/languages/origin')
);
$this->tmpl->addGlobalVar('cs', $this->cs);
$this->tmpl->addGlobalVars($origin, 'origin_');
$this->lang->load($lang);
$this->tmpl->addGlobalVars($this->lang->get(), 'trans_');
$values = $this->getTranslation($lang);
return $this->processForm('translate', $values);
}
/**
* save new translation
*
* @param patForms $form
* @param array $values
* @return bool always true
*/
protected function onTranslateValid($form, $values)
{
// remove old translations
$clause = array();
$clause[] = array(
'field' => 'cs',
'value' => $this->cs
);
$clause[] = array(
'field' => 'revision',
'value' => $this->revision
);
$clause[] = array(
'field' => 'lang',
'value' => $values['lang']
);
$this->table->delete('xmlarticletranslation', null, null, $clause);
$save = array(
'cs' => $this->cs,
'revision' => $this->revision,
'body' => $values['body'],
'lang' => $values['lang'],
$this->table->getIdentifier('user') => $this->user->getId()
);
$this->table->save('xmlarticletranslation', '__new', $save);
// add url and trigger event
$save['url'] = $this->getUrl();
WBEvent::trigger('xmlarticletranslation:saved', 'Saved XML article translation {LANG}.', $save);
// translate content, eventually
$body = $this->content;
$lang = $this->locale->get('locale/languages/origin', null);
if (class_exists('patI18n', false)) {
$lang = patI18n::getLocale(patI18n::LOCALE_TYPE_COMPLETE);
}
if ($lang == $values['lang']) {
$body = $values['body'];
} else if ($lang != $this->locale->get('locale/languages/origin', null)) {
$trans = $this->getTranslation($lang);
$body = $trans['body'];
}
$html = $this->toHtml($body);
$this->addEditorButton($html);
$this->tmpl->addGlobalVar('content', $html);
return true;
}
/**
* load translated articles from database
*
*
* @param string $lang
* @return array
*/
private function getTranslation($lang)
{
$options = array(
'limit' => 1,
'order' => array(
array(
'field' => 'revision',
'asc' => 0
)
)
);
$clause = array();
$clause[] = array(
'field' => 'cs',
'value' => $this->cs
);
$clause[] = array(
'field' => 'lang',
'value' => $lang
);
$trans = $this->table->get('xmlarticletranslation', null, null, $clause, $options);
if (count($trans) == 1) {
return $trans[0];
}
$fake = array(
'lang' => $lang,
'body' => $this->content,
'revision' => -1,
$this->table->getIdentifier('user') => 0
);
return $fake;
}
/**
* Check whether current use may alter content
*
* @return bool if true
*/
private function isUserEditor()
{
$group = $this->config->get('editor/group', 'contenteditor');
if ($this->sess->has(__CLASS__ . '/' . $this->cs . '/group/editor')) {
$group = $this->sess->get(__CLASS__ . '/' . $this->cs . '/group/editor');
}
return $this->user->isInGroup($group);
}
/**
* Check whether user may translate content
*
* Buffer langauge list in session to avoid databse queries
*
* @param $lang
* @return bool
*/
private function isUserTranslator($lang = null)
{
$group = $this->config->get('translator/group', '');
if (empty($group)) {
return $this->isUserEditor();
}
if (!$this->user->isInGroup($group)) {
return false;
}
// check per langauge permissions for each translator
if (1 > intval($this->config->get('translator/uselangperm', 0))) {
return true;
}
if ($this->sess->has(__CLASS__ . '/translator/languages')) {
$langs = $this->sess->get(__CLASS__ . '/translator/languages');
}
else {
$list = $this->table->get('nlslangtranslator', null, $this->user->getId());
$langs = array();
foreach ($list as $l) {
$langs[] = $l['lang'];
}
}
if (empty($lang)) {
return !empty($langs);
}
$this->sess->set(__CLASS__ . '/translator/languages', $langs);
return in_array($lang, $langs);
}
/**
* location of form config
*
* Return sub directory where form element definitions are located
*
* @return string folder
*/
protected function getFormConfigDir()
{
return 'wxml/form';
}
/**
* merge form parameters
*
* replace action and onsubmit
*
* @see include/WB/WBFormProcessor#mergeFormParameters()
* @param array $params
* @return bool always true
*/
protected function mergeFormParameters(&$params)
{
$params['method'] = 'post';
$params['onsubmit'] = 'return this.wxml.submit();';
return true;
}
/**
* load templates from file
*
* @param string $tmpl
*/
protected function loadTemplates($tmpl, $local = true)
{
if ($local) {
$tmpl = $this->tmplDir . '/' . $tmpl;
}
return $this->tmpl->readTemplatesFromInput($tmpl . '.tmpl');
}
/**
* load article from database
*
*
*/
private function loadCurrent()
{
// load single article from database
$options = array(
'limit' => 1,
'order' => array(
array(
'field' => 'revision',
'asc' => 0
)
)
);
$clause = array();
$clause[] = array(
'field' => 'cs',
'value' => $this->cs
);
$clause[] = array(
'field' => 'status',
'value' => 'public'
);
$article = $this->table->get('xmlarticle', null, null, $clause, $options);
if (count($article) != 1) {
$save = array(
'cs' => $this->cs,
'body' => $this->content
);
$this->table->save('xmlarticle', '__new', $save);
return;
}
$article = array_shift($article);
$this->revision = $article['revision'];
$this->content = $article['body'];
}
}