* @license PHP License * @package WB * @subpackage base */ WBClass::load('WBLog' , 'WBString'); /** * Site * * @version 0.5.0 * @package WB * @subpackage base */ abstract class WBService extends WBStdClass { /** * my part * @var string */ protected $part = 'site'; /** * my path * @var array */ protected $path = array(); /** * request * @var WBRequest */ protected $req; /** * response * @var WBResponse */ protected $res; /** * config object * @var WBConfig */ protected $conf; /** * config object locales * @var WBConfig */ protected $confLocale; /** * Content renderer * @var WBContent */ protected $cont; /** * logger * @var WBLog */ protected $log; /** * session * @var patSession_Storage */ protected $sess; /** * Decide whether to start locale tools * @var bool */ protected $useI18n = true; /** * Decide whether to select locale settings according to http-header * @var bool */ protected $useI18nFromHeader = false; /** * decide whether to select locale settings from path * @var bool */ protected $useI18nFromPath = false; /** * decide wheter to start WBConfig * @var bool */ protected $useConfig = true; /** * Constructor * */ public function __construct($params) { if (isset($params['part']) || !empty($params['part'])) { $this->part = $params['part']; } $this->req = WBClass::create('WBRequest'); $this->res = WBClass::create('WBResponse'); $this->sess = WBClass::create('patSession'); $this->log = WBLog::start('service'); $path = trim($this->req->path, ' /'); if (!empty($path)) { $path = array_map('trim', explode('/', $path)); $this->path = $path; } } /** * Serve * * Configure locale settings, run service and deliver output * * @return bool true on success */ public function serve() { // load locale config if ($this->useI18n) { $this->confLocale = WBClass::create('WBConfig'); $this->confLocale->load('locale'); } $data = ''; $status = 200; $checksum = ''; $this->setLocale($status, $data); if ($this->useConfig) { $param = 'wb/config/loader/service/' . strtolower(substr(get_class($this), strlen(__CLASS__) + 1)); $loader = WBParam::get($param, 'File'); $this->conf = WBClass::create('WBConfig'); $this->conf->setLoader($loader); $this->conf->load($this->part); } // default TTL - default is 31 days $this->res->setMaxAge(WBParam::get('wb/cache/ttl', 2678400)); if (!$this->run($status, $data, $checksum)) { return true; } if (is_resource($data)) { $this->res->addStream($data, $checksum); } else { $this->res->add($data); } $this->res->setStatus($status); $this->res->send($this->req); return true; } /** * Do whatever has to be done * * @param int $status * @param mixed $data to be send * @param string $checksum string, if any * @return bool true on success */ abstract public function run(&$status, &$data, &$checksum = null); /** * pospone event * * @param string $name * @return bool true on success */ static public function addEvent($name) { $sess = WBClass::create('patSession'); $q = $sess->get('wb.service.event.queue'); if (!is_array($q)) { $q = array(); } $q[] = array( 'time' => microtime(true), 'name' => $name ); $sess->set('wb.service.event.queue', $q); return true; } /** * fetch list of events * * @return array */ protected function getEventQueue() { $q = $this->sess->get('wb.service.event.queue'); if (!is_array($q)) { $q = array(); } return $q; } /** * flush event queue * * @return bool true on success */ protected function flushEventQueue() { $this->sess->clear('wb.service.event.queue'); return true; } /** * setup NLS * * Set language if useI18n-switch is set. First see whether there are any locale * settings in session stored. * * @todo this method is sort of to long and complicated - refactor * @see $useI18n * @see $useI18nFromPath * @param int $status * @param string $data * @return bool true to continue */ protected function setLocale(&$status, &$data) { if (!$this->useI18n) { return true; } WBClass::create('patI18n'); // very basic default language $langDef = $this->confLocale->get('locale/languages/default', 'en_GB'); $lang = $langDef; $langOld = $langDef; // try to load language from session if ($this->sess->has($this->part . ':lang')) { $lang = $this->sess->get($this->part . ':lang'); $langOld = $lang; } $switch = $lang; // try to get from browser settings if(!$this->sess->has($this->part . ':lang')) { if ($this->useI18nFromHeader || 0 < intval($this->confLocale->get('locale/languages/select/usehttpheader', 1))) { $httpLang = $this->req->getMeta('HTTP_ACCEPT_LANGUAGE', ''); if (!empty($httpLang)) { $httpLang = explode(',', $httpLang); foreach ($httpLang as $hl) { $hl = explode(';', $hl); if (strlen($hl[0]) > 1) { $switch = substr($hl[0], 0, 2); if (5 == strlen($hl[0])) { $switch .= '_' . strtoupper(substr($hl[0], 3, 2)); } break; } } } } } // path languages $redirect = false; $reqPath = $this->confLocale->get('locale/languages/requestpath', array()); $langPath = $langOld; $query = $this->req->export(); if ($this->useI18nFromPath) { $redirect = true; if (empty($reqPath)) { $redirect = false; } else { $tmp = $this->findLangInPath($this->path); if ($tmp) { $langPath = $tmp; $switch = $tmp; $this->req->path = implode('/', $this->path); $redirect = false; } else { $langPath = $reqPath[$lang]; $redirect = true; } } } // get language switch from request parameter if (intval($this->confLocale->get('locale/languages/select/usehttpget', 1))) { $get = $this->req->get('__lang'); if ($get) { unset($query['__lang']); if ($this->useI18nFromPath && !empty($reqPath) && $switch != $get) { $redirect = true; } $switch = $get; } } // available languages $available = $this->confLocale->get('locale/languages/available', array()); // on invalid switch value, use default language if (2 > strlen($switch) || 5 < strlen($switch)) { $switch = $lang; } // use excact match else if (in_array($switch, $available)) { $lang = $switch; } // use the first two letter code that matches else { foreach ($available as $a) { if (strncmp($switch, $a, 2) == 0) { $lang = $a; break; } } } // redirect or not $useRedirect = intval($this->confLocale->get('locale/languages/select/usehttpredirect', 1)); // finally set language patI18n::setLocale($lang); $this->res->addHeader('Content-Language', patI18n::getLocale(patI18n::LOCALE_TYPE_SHORT)); if (!empty($reqPath) && '__default' != $reqPath[patI18n::getLocale(patI18n::LOCALE_TYPE_LANG)]) { WBString::setLanguagePath($reqPath[patI18n::getLocale(patI18n::LOCALE_TYPE_LANG)]); } // new language was selected before if ($lang == $langOld && $lang == $langPath) { return true; } // store chosen language in session $this->sess->set($this->part . ':lang', $lang); if (!$redirect || 1 > $useRedirect) { return true; } // redirect $status = 301; $data = 'Redirect according locale settings'; $path = $this->path; array_unshift($path, $reqPath[$lang]); $url = '[[PROTOCOL]]://[[SERVER]][[SELF_NO_LANG]]' . implode('/', $path); if (!empty($query)) { $url .= '?' . http_build_query($query); } $this->res->addHeader('Location', WBString::replaceSuperPlaceholders($url)); return false; } /** * Find language in request path * * If pagePath start with one of the available languages, select it and strip * pagePath' first element. Available languages are configured in locale.xml. * * @param array $pagePath * @return string $lang */ protected function findLangInPath(&$pagePath) { if (!$this->useI18n) { return ''; } $reqPath = $this->confLocale->get('locale/languages/requestpath', array()); $default = ''; foreach ($reqPath as $k => $v) { if ('__default' == $v) { $default = $k; } } if (!is_array($reqPath) || empty($reqPath)) { return $default; } // check whether first element of pagePath is in list of available languages if (empty($pagePath) || !in_array($pagePath[0], $reqPath)){ return $default; } // select language and strip from pagePath foreach ($reqPath as $k => $v) { if ($pagePath[0] == $v) { array_shift($pagePath); return $k; break; } } return $default; } /** * load page according to request * * Pages' files are located in etc/site/*.xml (repsecively etc-default). * The folder "site" is just the default value of member variable "part". * * To implement some sort if inheritance, each page definition must only * include the "differences" between the previous page and this one. In * ohter words, you just need to configure those areas that change from * page to page. This is propably what you want if just one area changes * from page to page. * * @param array $params * @param array $pagePath * @return bool always true */ protected function processPath(&$params, $pagePath) { $conf = WBClass::create('WBConfig'); /** @var $conf WBConfig */ $conf->setLoader($this->conf->getLoader()->getName()); $log = array( 'process' => 'path', 'part' => $this->part, 'path' => implode('/', $pagePath), 'load' => 'direct', 'areas' => 0 ); // try to load actual page $page = array(); if ($conf->load($this->part . '/page/' . implode('/', $pagePath), true)) { $page = $conf->get(); // try to load default page } else { $rest = array(); $path = $pagePath; while (!empty($path)) { array_unshift($rest, array_pop($path)); if ($conf->load($this->part . '/page/' . implode('/', $path) . '/__default', true)) { $page = $conf->get(); $log['load'] = 'default'; // override parameter with rest of path if (!isset($page['pathparams'])) { break; } foreach ($page['pathparams'] as $key => $list) { $key = trim($key, ' /'); $key = explode('/', $key); if (!isset($page['content'][$key[0]])) { continue; } // dig down key path $tmp = &$page['content'][$key[0]]; for ($j = 1; $j < count($key); ++$j) { if (!isset($tmp['params']['content'][$key[$j]])) { break; } $tmp = &$tmp['params']['content'][$key[$j]]; } if (!is_array($tmp['params'])) { $tmp['params'] = array(); } // always add the rest of the path $tmp['params']['__path'] = $rest; // add indivudual path parts foreach ($list as $i => $name) { if (!isset($rest[$i])) { continue; } $tmp['params'][$name] = $rest[$i]; } } break; } } } // load index if (empty($page)) { if (!$conf->load($this->part . '/page/__index', true) ) { return true; } $page = $conf->get(); $log['load'] = 'index'; } if (isset($page['renderer'])) { $params['renderer'] = $page['renderer']; } if (!isset($page['content'])) { $this->log->warn($log); return true; } $this->overrideAreas($params, $page, array()); $this->log->warn($log); return true; } /** * recursivly override content areas and renderer * * * @param array $params actual parameter postion * @param array $page actual page position * @param array $path path in content names so far */ private function overrideAreas(&$params, $page, $path) { if (!is_array($page['content'])) { $page['content'] = array(); } foreach($page['content'] as $name => $c) { // simple override of content module if (!isset($c['params']['content'])) { $path[] = $name; $params['content'][$name] = $c; array_pop($path); continue; } // override renderer if (isset($c['params']['renderer'])) { $params['content'][$name]['params']['renderer'] = $c['params']['renderer']; } // recursion $path[] = $name; $this->overrideAreas($params['content'][$name]['params'], $c['params'], $path); array_pop($path); } } /** * mangle parmeter according to request * * Sometimes it is quite useful to make the parameter of a content component * mendable by request parameter. You thave to write down the changable * parameter in the page's XML definition * * @todo see how recursion may occur here * @param array $params * @param array $pagePath * @return bool always true */ protected function processPathRequest(&$params, $pagePath) { if (empty($params)) { return true; } $log = array( 'process' => 'pathrequest', 'part' => $this->part, 'path' => implode('/', $pagePath), 'area' => '', 'parameter' => '' ); $this->overrideParameter($params, array(), $pagePath); return true; } /** * override parameters from request, recursively * * @param array $params * @param array $pagePath * @param string $path */ private function overrideParameter(&$params, $path, $pagePath) { $log = array( 'process' => 'pathrequest', 'part' => $this->part, 'path' => implode('/', $pagePath), 'area' => '', 'parameter' => '' ); foreach ($params['content'] as $area => &$c) { $path[] = $area; // go to next level if (isset($c['params']['content'])) { $this->overrideParameter($c['params'], $path, $pagePath); } $this->mergeRequestParams($c); array_pop($path); } } protected function mergeRequestParams(&$c) { // see whether request parameters are allowed if (!isset($c['requestparams']) || empty($c['requestparams'])) { return; } if (!is_array($c['requestparams'])) { return; } // override parameter foreach ($c['requestparams'] as $name => $get) { // there is no parameter name mapping if (is_numeric($name)) { $name = $get; } // set default if (!isset($c['params'][$name])) { $c['params'][$name] = null; } // copy parameter from requeist parameter $c['params'][$name] = $this->req->get($get, $c['params'][$name]); } } }