* @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.2.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'; /** * location of template files for hander * @var string */ private $tmplDirHdl = ''; /** * 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; /** * WXML Document Title * @var string */ private $title = 'CMS'; /** * 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 Informal Title String * * @param string */ public function setTitle($title) { $this->title = $title; } /** * Set Template Dir for Handler * * @param string */ public function setTmplDir($dir = '') { $this->tmplDirHdl = $dir; if ($this->handler) { $this->handler->setTmplDir($this->tmplDirHdl); } } /** * 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'); if (!empty($this->tmplDirHdl)) { $this->handler->setTmplDir($this->tmplDirHdl); } $this->scanner->setHandler($this->handler); } $this->scanner->scan($content); $class = array('wb-wxml-content'); if ($this->sess->has(__CLASS__ . '/' . $this->cs . '/attributes/class')) { $class[] = htmlspecialchars($this->sess->get(__CLASS__ . '/' . $this->cs . '/attributes/class')); } $class = sprintf(' class="%s"', implode(' ', $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( 'title' => $this->title, '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' ); $pUser = $this->table->getIdentifier('user'); $pUrl = $this->table->getIdentifier('url'); $this->table->delete('xmlarticle', null, null, $clause); $save = array( 'cs' => $this->cs, 'body' => $content, 'status' => 'draft', $pUser => $this->user->getId(), $pUrl => 0 ); $dict = WBClass::create('WBDictionary_URL'); $dict->addWord($this->getUrl()); $save[$pUrl] = $dict->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']; } }