diff --git a/README-GIT.md b/README-GIT.md index 9a99309eea..874b2a6fdc 100644 --- a/README-GIT.md +++ b/README-GIT.md @@ -1,17 +1,23 @@ -# USING THE GIT REPOSITORY TO WORK ON PI ENGINE - 1. Setup a GitHub account (http://github.com/), if you haven't yet - 2. Clone the repo locally and enter it (use your own GitHub username - in the statement below) +Pi Engine Github Skeleton +========================= - ```sh - % git clone https://github.com/pi-engine/pi.git - % cd pi - ``` +Pi Engine Core +---------------- +* [pi-engine/pi](https://github.com/pi-engine/pi): Pi Engine core repo +* [pi-engine/pi/wiki](https://github.com/pi-engine/pi/wiki): Pi Engine documents -# GIT REPO SKELETON FOR PI ENGINE -* [pi-engine/pi](https://github.com/pi-engine/pi): Pi Engine core repo -* pi-engine/pi-{modulename}: repo for module {modulename}, for instance [pi-engine/pi-tag](https://github.com/pi-engine/pi-tag) for module tag -* pi-engine/pi-theme-{themename}: repo for theme {themename}, for instance [pi-engine/pi-theme-pi](https://github.com/pi-engine/pi-theme-pi) for theme pi -* pi-engine/{reponame}: repo for non-pi components +Pi Engine Module +---------------- +* [pi-module](https://github.com/pi-module): repos for modules +* Eeach module has its own repo, for instance [pi-module/tag](https://github.com/pi-module/tag) for module tag + +Pi Engine Theme +--------------- +* [pi-theme](https://github.com/pi-theme): repos for themes +* Each theme has its ownrepo, for instance [pi-theme/pi](https://github.com/pi-theme/pi) for theme pi + +Pi Engine Extras +---------------- +* [pi-extra](https://github.com/pi-extra): repos for extra components diff --git a/README.md b/README.md index b27e7af501..c0752b0010 100644 --- a/README.md +++ b/README.md @@ -2,31 +2,37 @@ Pi Engine ================= Pi Engine is a role oriented application development engine for web and mobile, designed as the next generation and a successor to Xoops. -Pi is developed upon PHP and MySQL with selected third-party frameworks including but not limited to [Zend Framework library](https://github.com/zendframework/zf2), [jQuery](https://github.com/jquery/jquery), [Bootstrap](https://github.com/twitter/bootstrap) and [Backbone](https://github.com/documentcloud/backbone). +Pi is developed upon PHP and MySQL with selected third-party frameworks including but not limited to [Zend Framework 2](https://github.com/zendframework/zf2), jQuery, Bootstrap and Backbone. -Pi Project follows the philosophy of open standard, open design, open development and open structure. Pi is born as a complete open source project and intended to build a sustainable ecosystem that benefits all contributors and users. +Pi Project follows the philosophy of open standard, open design, open development and open management. Pi is born as a complete open source project and intended to build a sustainable ecosystem that benefits all contributors and users. Pi Engine is started by [Taiwen Jiang](http://github.com/taiwen) (Xoops Project Lead and Dev Lead), [Marc Desrousseaux](http://github.com/MarcoXoops) (Xoops QA Lead), [Hossein Azizabadi](http://github.com/voltan) (Xoops Persian Lead), [Kris](http://github.com/krisxoofoo) (Xoops Council Member and Xoops France Lead), etc. [Pi Team](https://github.com/pi-engine/pi/wiki/Pi-Team) is highly inspired by the Xoops led by onokazu (Ono Kazumi), skalpa and phppp (Taiwen Jiang, or D.J.) successively since 2001 and will continue to support and promote Xoops. **Check out [Latest Release](https://github.com/pi-engine/pi/blob/master/doc/releasenotes.md).** +Highlights +------------- +1. **Sustainable ecosystem:** A sustainable ecosystem built upon open standard, open source code, open development and open management on Github. +2. **Engineered development:** Quality ensured engineering development with short learning curve, low skill requirements with clean MVC architecture, semantic templating, sophisticated API and strict starndards. +3. **Visualized management:** Easy and responsive application and content management based on visualized mangement tools and interface with page and widget mechanism. +4. **Agile workflow:** Role oriented architecture and deployment skeleton supports manageable agile development workflow. Features and practices ---------------------- -1. Modularization for functionality and applications -2. Components for basic libraries and services for fundamental system functions -3. Theming for presentation and appearance -4. Design-friendly templating -5. DevOps oriented architecture -6. Centralized security enhancement +* Modularization for functionality and applications +* Components for basic libraries and services for fundamental system functions +* Theming for presentation and appearance +* Design-friendly templating +* DevOps oriented architecture +* Centralized security enhancement Quick start ----------- -* Documents at [Pi Wiki](https://github.com/pi-engine/pi/wiki) -* Download the [latest stable code](https://github.com/xoops/pi/zipball/master) and [latest dev code](https://github.com/xoops/pi/zipball/develop). -* Clone Pi repo `git clone git://github.com/pi-engine/pi.git` and [keep updated](https://help.github.com/articles/fork-a-repo#pull-in-upstream-changes). -* Demo sites with shared deployment: Pi Dialogue ([pialog.org](http://pialog.org) | [pialogue.org](http://pialogue.org)), Pi Demo ([pi-demo.org](http://pi-demo.org)) and Xoops/Pi ([xoopsengine.org](http://demo.xoopsengine.org)). +* Documents at [Pi Wiki](https://github.com/pi-engine/pi/wiki). +* Download the [latest stable code](https://github.com/pi-engine/pi/zipball/master) and [latest dev code](https://github.com/pi-engine/pi/zipball/develop). +* Clone Pi repo `git clone git://github.com/pi-engine/pi.git`. +* Get extension resources at [Pi module and theme repos](https://github.com/pi-engine/pi/blob/master/README-GIT.md). Development ---------- @@ -40,3 +46,9 @@ Copyright and License The Engine is released under a [New BSD License](https://github.com/pi-engine/pi/blob/master/doc/license.txt). +Demos +----- +Demo sites with shared deployment: +* Pi Dialogue ([pialog.org](http://pialog.org) | [pialogue.org](http://pialogue.org)) +* Pi Demo ([pi-demo.org](http://pi-demo.org)) +* Xoops/Pi ([xoopsengine.org](http://demo.xoopsengine.org)) diff --git a/doc/PiArchitectureDiagram-TaiwenJiang.pdf b/doc/PiArchitectureDiagram-TaiwenJiang.pdf new file mode 100644 index 0000000000..17e063394e Binary files /dev/null and b/doc/PiArchitectureDiagram-TaiwenJiang.pdf differ diff --git a/doc/README.html b/doc/README.html new file mode 100644 index 0000000000..f9a5410044 --- /dev/null +++ b/doc/README.html @@ -0,0 +1,34 @@ +

+ Pi Engine is a role oriented application development engine for web and mobile, designed as the next generation and a successor to Xoops. + Pi is developed upon PHP and MySQL with selected third-party frameworks including but not limited to Zend Framework library, jQuery, Bootstrap and Backbone. +

+ +

+ Pi Project follows the philosophy of open standard, open design, open development and open structure. Pi is born as a complete open source project and intended to build a sustainable ecosystem that benefits all contributors and users. +

+ +

+ The Engine is released under a New BSD License. +

+ +

+

    + Selected features and practices: +
  1. Modularization for functionality and applications +
  2. Components for basic libraries and services for fundamental system functions +
  3. Theming for presentation and appearance +
  4. Design-friendly templating +
  5. DevOps oriented architecture +
  6. Centralized security enhancement +
      +

      + +

      +

      +

      diff --git a/doc/changelog.txt b/doc/changelog.txt index 5e2663b703..fcb57b9ba9 100644 --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -1,5 +1,24 @@ Pi Engine Changelog + +June 24th, 2013 +============================= +1. Added doc "Pi Engine Architecture Diagram" +2. Added Pi\File\Transfer\Download for file download and script/download.php for single file download +3. Added Pi\Service\Log::mute() and Debugger::mute to disable debugger output for some use cases. +4. Fixes Issue #113: Navigation page active is not properly detected +5. Added function for translation register: _t('Message to be translated later.') +6. Renamed Setup translation function name to _s() to avoid conflicts with translation placeholder function _t() +7. Added listener for error ViewModel under JSON/AJAX/Feed request +8. Added "pi-feature" and "pi-spotlight" blocks to homepage on installation +9. Added two new block zones to theme font layout: $block['0'] for head blocks and $block['99'] for foot blocks +10. Added browser support indication to theme configuration +11. Fixed Issue #121: exception messages were missed by ErrorStrategy template overwritten +12. Merged theme/template/error-exception.phtml to error.phtml +13. Modified config: var/config/config.application.php -- changed 'exception_template' value +14. Formulated return of Ajax/JSON request with format: array('status' => 1, 'message' => '', 'data' => array()) + + June 19th, 2013 ============================= 1. Upgraded ZF2 to 2.2.1 diff --git a/lib/Pi/Application/Model/RowGateway/Config.php b/lib/Pi/Application/Model/RowGateway/Config.php index 81bdec40e0..779b7b0eeb 100644 --- a/lib/Pi/Application/Model/RowGateway/Config.php +++ b/lib/Pi/Application/Model/RowGateway/Config.php @@ -35,7 +35,7 @@ class Config extends RowGateway */ protected function encode($data) { - if (!empty($data['filter'])) { + if (!empty($data['filter']) && isset($data['value'])) { $data['value'] = $this->encodeValueColumn($data['value'], $data['filter']); } diff --git a/lib/Pi/Application/Registry/AbstractRegistry.php b/lib/Pi/Application/Registry/AbstractRegistry.php index 850fc79906..b19c4dac3d 100644 --- a/lib/Pi/Application/Registry/AbstractRegistry.php +++ b/lib/Pi/Application/Registry/AbstractRegistry.php @@ -217,7 +217,7 @@ protected function loadData($meta = array()) * Load cache data matching the meta * * @param array $meta - * @return array + * @return array|bool */ protected function loadCacheData($meta = array()) { diff --git a/lib/Pi/Application/Service/I18n.php b/lib/Pi/Application/Service/I18n.php index f2c9a0ff00..3c9a75a7a6 100644 --- a/lib/Pi/Application/Service/I18n.php +++ b/lib/Pi/Application/Service/I18n.php @@ -14,8 +14,6 @@ * @author Taiwen Jiang * @package Pi\Application * @subpackage Service - * @since 3.0 - * @version $Id$ */ /** @@ -138,7 +136,35 @@ * __('A test message', 'theme/default', 'en'); * * - * 7. Format a date + * + * 7. Register a message that will be translated in different lanauges, but not translated at the place where it is registered + * + * _t('Message to be translated and used later.'); + * + * // Use case, register module config + * // Registered in a module's config.php + * + * $config['key'] = array('title' => _t('Config Title'), 'description' => _t('Config hint'), '...')); + * + * // Load translated message in the module config setting page + * // module/system/src/Controller/Admin/ConfigController.php calls ConfigForm.php: + * + * // Module\System\Form\ConfigForm::addElement() + * + * protected function addElement($config) + * { + * // ... + * $attributes['description'] = __($config->description); + * $options = array( + * 'label' => __($config->title), + * 'module' => $this->module, + * ); + * // ... + * } + * + * + * + * 8. Format a date * * _date(time(), 'fa-IR', 'long', 'short', 'Asia/Tehran', 'persian'); * _date(time(), array('locale' => 'fa-IR', 'datetype' => 'long', 'timetype' => 'short', 'timezone' => 'Asia/Tehran', 'calendar' => 'persian')); @@ -167,7 +193,7 @@ * _date(time(), ...); * * - * 8. Format a number + * 9. Format a number * * _number(123.4567, 'decimal', '#0.# kg', 'zh-CN', 'default'); * _number(123.4567, 'decimal', '#0.# kg', 'zh-CN'); @@ -175,14 +201,14 @@ * _number(123.4567, 'spellout'); * * - * 9. Format a currency + * 10. Format a currency * * _currency(123.45, 'USD', 'en-US'); * _currency(123.45, 'USD'); * _currency(123.45); * * - * 10. Get a date formatter + * 11. Get a date formatter * * Pi::service('i18n')->getDateFormatter('fa-IR', 'long', 'short', 'Asia/Tehran', 'persian'); * Pi::service('i18n')->getDateFormatter(array('locale' => 'fa-IR', 'datetype' => 'long', 'timetype' => 'short', 'timezone' => 'Asia/Tehran', 'calendar' => 'persian')); @@ -194,7 +220,7 @@ * Pi::service('i18n')->getDateFormatter(array('pattern' => 'yyyy-MM-dd HH:mm:ss')); * * - * 11. Get a number formatter + * 12. Get a number formatter * * // Get a number formatter * Pi::service('i18n')->getNumberFormatter('decimal', '#0.# kg', 'zh-CN'); @@ -392,7 +418,7 @@ public function __get($name) * Normalize domain in Intl resources, including Translator, Locale, Date, NumberFormatter, etc. * * @param string $domain - * @return array pair of component and domain + * @return array pair of component and domain */ public function normalizeDomain($rawDomain) { @@ -410,17 +436,19 @@ public function normalizeDomain($rawDomain) * * @param array|string $domain * @param string|null $locale - * @return Intl + * @return I18n */ public function load($domain, $locale = null) { $domain = is_array($domain) ? $domain : $this->normalizeDomain($domain); $locale = $locale ?: $this->getLocale(); - - $this->getTranslator()->load($domain, $locale); + $result = $this->getTranslator()->load($domain, $locale); if (Pi::service()->hasService('log')) { - Pi::service()->getService('log')->info(sprintf('Translation "%s" is loaded', implode(':', $domain))); + $message = $result + ? sprintf('Translation "%s.%s" is loaded.', implode(':', $domain), $locale) + : sprintf('Translation "%s.%s" is empty.', implode(':', $domain), $locale); + Pi::service()->getService('log')->info($message); } return $this; @@ -432,14 +460,14 @@ public function load($domain, $locale = null) * @param string $domain * @param string $module * @param string $locale - * @return Intl + * @return I18n */ public function loadModule($domain, $module = null, $locale = null) { $module = $module ?: Pi::service('module')->current(); $component = array('module/' . $module, $domain); - $this->load($component, $locale); + return $this; } @@ -449,13 +477,14 @@ public function loadModule($domain, $module = null, $locale = null) * @param string $domain * @param string $theme * @param string $locale - * @return Intl + * @return I18n */ public function loadTheme($domain, $theme = null, $locale = null) { $theme = $theme ?: Pi::service('theme')->current(); $component = array('theme/' . $theme, $domain); $this->load($component, $locale); + return $this; } @@ -688,6 +717,18 @@ function _e($message, $domain = null, $locale = null) echo __($message, $domain, $locale); } + /** + * Register a message to translation queue + * + * + * @param string $message The string to be localized + * @return void + */ + function _t($message) + { + return $message; + } + /** * Check if Intl functions are available */ diff --git a/lib/Pi/Application/Service/Log.php b/lib/Pi/Application/Service/Log.php index a6636aceb0..ebe879206a 100644 --- a/lib/Pi/Application/Service/Log.php +++ b/lib/Pi/Application/Service/Log.php @@ -33,7 +33,7 @@ class Log extends AbstractService /** * Whether or not to the service is active - * @var boolean + * @var bool */ protected $active; protected $debugger; @@ -133,6 +133,21 @@ public function active($flag = null) return $this->active; } + /** + * Enable/disable debugger + * + * @param bool $flag + * @return bool|null return previous muted value or null if no debugger available + */ + public function mute($flag = true) + { + $muted = null; + if ($this->debugger) { + $muted = $this->debugger->mute($flag); + } + return $muted; + } + /** * Get logger, instantiate it if not available * diff --git a/lib/Pi/File/Transfer/Download.php b/lib/Pi/File/Transfer/Download.php new file mode 100644 index 0000000000..58cc0cff81 --- /dev/null +++ b/lib/Pi/File/Transfer/Download.php @@ -0,0 +1,319 @@ + + * @package Pi\File + */ + +namespace Pi\File\Transfer; + +use Pi; +use Pi\Filter\File\Rename; +use ZipArchive; + +/** + * Download content and files + * + * Download conent generated on-fly + * + * $file = array( + * 'source' => 'Generated content', + * // Required + * 'type' => 'string', + * // Optional + * 'filename' => 'pi-download', + * // Optional + * 'content_type => 'application/octet-stream', + * ); + * $downloader = new Download; + * $downloader->send($file) + * + * + * Download a file + * + * $file = 'path/to/file'; + * // Or + * $file = array( + * 'source' => 'path/to/file' + * // Optional + * 'filename' => 'pi-download', + * // Optional + * 'content_type => 'application/octet-stream', + * ); + * $downloader = new Download; + * $downloader->send($file) + * + * + * Download multiple files, compressed and sent as a zip file + * + * $file = array( + * 'path/to/file1', + * 'path/to/file2', + * 'path/to/file3', + * ); + * // Or + * $file = array( + * array( + * 'filename' => 'path/to/file1', + * 'localname' => 'filea', + * ), + * array( + * 'filename' => 'path/to/file2', + * 'localname' => 'fileb', + * ), + * array( + * 'filename' => 'path/to/file3', + * 'localname' => 'fileb', + * ), + * ); + * // Or + * $file = array( + * 'source' => array( + * 'path/to/file1', + * 'path/to/file2', + * 'path/to/file3', + * ), + * // Optional + * 'filename' => 'pi-download', + * // Optional + * 'type' => 'zip', + * // Optional + * 'content_type => 'application/octet-stream', + * ); + * // Or + * $file = array( + * 'source' => array( + * array( + * 'filename' => 'path/to/file1', + * 'localname' => 'filea', + * ), + * array( + * 'filename' => 'path/to/file2', + * 'localname' => 'fileb', + * ), + * array( + * 'filename' => 'path/to/file3', + * 'localname' => 'fileb', + * ), + * ), + * // Optional + * 'filename' => 'pi-download', + * // Optional + * 'type' => 'zip', + * // Optional + * 'content_type => 'application/octet-stream', + * ); + * $downloader = new Download; + * $downloader->send($file) + * + * + * Download without auto exit + * + * $downloader = new Download(array('exit' => false)); + * $downloader->send(array(...)); + * // Do something + * exit; + * + */ +class Download +{ + /** + * Exit current execution after the download + * + * @var bool + */ + protected $exit = true; + + /** + * Path to temporary file for zip file + * + * @var string + */ + protected $tmp = ''; + + /** + * Creates a file download handler + * + * @param array $options OPTIONAL Options + */ + public function __construct($options = array()) + { + $this->setOptions($options); + } + + /** + * Set options + * + * @param array $options + * @return Download + */ + public function setOptions($options = array()) + { + if (isset($options['exit'])) { + $this->exit = (bool) $options['exit']; + } + $this->tmp = isset($options['tmp']) ? $options['tmp'] : Pi::path('cache'); + return $this; + } + + /** + * Send the file to the client (Download) + * + * @param string|array $options Options for the file(s) to send + * @return bool + */ + public function send($options = null) + { + // Disable logging service + Pi::service('log')->mute(); + + // Canonize download options + $options = $this->canonizeDownload($options); + if (!$options) { + return false; + } + list($resource, $filename, $type, $contentType, $contentLength) = $options; + if ('string' == $type) { + $source = $resource; + } else { + $source = fopen($resource, 'rb'); + } + + // Send the content to client + $this->download($source, $filename, $contentType, $contentLength); + + // Close resource handler + if (is_resource($source)) { + fclose($source); + } + + // Remove tmp zip file + if ('zip' == $type) { + @unlink($resource); + } + + if ($this->exit) { + // Exit request to avoid extra output + exit; + } + return true; + } + + /** + * Canonize download options + * + * @param array|string $options + * @return array + */ + protected function canonizeDownload($options) + { + $resource = null; + $filename = ''; + $contentType = 'application/octet-stream'; + $contentLength = 0; + + $source = array(); + if (is_array($options) && isset($options['source'])) { + $source = (array) $options['source']; + $type = isset($options['type']) ? $options['type'] : 'file'; + $filename = isset($options['filename']) ? $options['filename'] : ''; + $contentType = isset($options['content_type']) ? $options['content_type'] : ''; + } else { + $source = (array) $options; + } + if (count($source) > 1) { + $type = 'zip'; + } + + if ('string' == $type) { + $resource = array_shift($source); + $contentLength = strlen($resource); + $source = array(); + } elseif ('zip' != $type) { + $resource = array_shift($source); + $filename = $filename ?: basename($resource); + $contentLength = filesize($resource); + } else { + if ($filename) { + if (strtolower(substr($filename, -4)) != '.zip') { + $filename .= '.zip'; + } + } else { + $filename = 'archive.zip'; + } + $contentType = 'application/zip'; + $zipFile = tempnam($this->tmp, 'zip'); + $zip = new ZipArchive; + if ($zip->open($zipFile, ZipArchive::CREATE) !== true) { + return array(); + } + + foreach ($source as $item) { + $localname = null; + $file = null; + if (is_array($item)) { + $file = $item['filename']; + $localname = isset($item['localname']) ? $item['localname'] : basename($file); + } elseif (is_file($item)) { + $file = $item; + $localname = basename($file); + } else { + continue; + } + $zip->addFile($file, $localname); + } + $zip->close(); + $resource = $zipFile; + $contentLength = filesize($resource); + } + + return array($resource, $filename, $type, $contentType, $contentLength); + } + + /** + * Send content to client + * + * @param Resource|string $source + * @param string $filename + * @param string $contentType + * @param int $contentLength + * @return bool + */ + protected function download($source, $filename, $contentType, $contentLength = 0) + { + header('Content-Description: File Transfer'); + header('Content-Type: ' . $contentType); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Transfer-Encoding: chunked'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Pragma: public'); + if ($contentLength) { + header('Content-Length: ' . $contentLength); + } + + ob_clean(); + flush(); + if (is_resource($source)) { + //Send the content in chunks + while (false !== ($chunk = fread($source, 4096))) { + echo $chunk; + } + } elseif (is_string($source)) { + echo $source; + } + + return true; + } + +} diff --git a/lib/Pi/File/Transfer/Upload.php b/lib/Pi/File/Transfer/Upload.php index 75f512a084..5f44e4694a 100644 --- a/lib/Pi/File/Transfer/Upload.php +++ b/lib/Pi/File/Transfer/Upload.php @@ -22,6 +22,11 @@ class Upload extends Transfer { + /** + * Destination path for uploaded files + * + * @var string + */ protected $destination; /** @@ -29,7 +34,6 @@ class Upload extends Transfer * * @param array $options OPTIONAL Options to set for this adapter * @param string $adapter Adapter to use - * @throws Exception\InvalidArgumentException */ public function __construct($options = array(), $adapter = 'Http') { @@ -44,10 +48,7 @@ public function __construct($options = array(), $adapter = 'Http') /** * Returns adapter * - * @param boolean $direction On null, all directions are returned - * On false, download direction is returned - * On true, upload direction is returned - * @return array|Adapter\AbstractAdapter + * {@inheriteDoc} * @note Zend\File\Transfer\Transfer does not support for $direction = 1 yet */ public function getAdapter($direction = false) @@ -81,6 +82,11 @@ public function setDestination($value, $verify = true) return $this; } + /** + * Get upload destination path + * + * @return string + */ public function getDestination() { return $this->destination; diff --git a/lib/Pi/I18n/Translator/Loader/Csv.php b/lib/Pi/I18n/Translator/Loader/Csv.php index 59ed2cd647..446f834d1c 100644 --- a/lib/Pi/I18n/Translator/Loader/Csv.php +++ b/lib/Pi/I18n/Translator/Loader/Csv.php @@ -22,11 +22,22 @@ use Pi; use Zend\I18n\Translator\Loader\FileLoaderInterface; use Zend\I18n\Translator\TextDomain; +use Zend\I18n\Exception; +use Zend\Stdlib\ErrorHandler; class Csv implements FileLoaderInterface { + /** + * File extension + * @var string + */ protected $fileExtension = '.csv'; + /** + * Options for CSV file + * @var array + * @see http://www.php.net/manual/en/function.fgetcsv.php + */ protected $options = array( 'delimiter' => ',', 'length' => 0, @@ -47,38 +58,37 @@ public function setOptions($options = array()) /** * {@inheritdoc} + * @return TextDomain|false */ public function load($locale, $filename) { $filename .= $this->fileExtension; $messages = array(); - if (is_file($filename) && is_readable($filename)) { - $file = @fopen($filename, 'rb'); - while(($data = fgetcsv($file, $this->options['length'], $this->options['delimiter'], $this->options['enclosure'])) !== false) { - if (substr($data[0], 0, 1) === '#') { - continue; - } + ErrorHandler::start(); + $file = fopen($filename, 'rb'); + $error = ErrorHandler::stop(); + if (false === $file) { + return false; + } - if (!isset($data[1])) { - continue; - } + while(($data = fgetcsv($file, $this->options['length'], $this->options['delimiter'], $this->options['enclosure'])) !== false) { + if (substr($data[0], 0, 1) === '#') { + continue; + } - if (count($data) == 2) { - $messages[$data[0]] = $data[1]; - } else { - $singular = array_shift($data); - $messages[$singular] = $data; - } + if (!isset($data[1])) { + continue; } - } else { - if (Pi::service()->hasService('log')) { - Pi::service()->getService('log')->info(sprintf('Translation file is not loaded: %s', $filename)); + + if (count($data) == 2) { + $messages[$data[0]] = $data[1]; + } else { + $singular = array_shift($data); + $messages[$singular] = $data; } } - //return $messages; - $textDomain = new TextDomain($messages); return $textDomain; } diff --git a/lib/Pi/I18n/Translator/Translator.php b/lib/Pi/I18n/Translator/Translator.php index 2e470a45f5..e96f68dc0a 100644 --- a/lib/Pi/I18n/Translator/Translator.php +++ b/lib/Pi/I18n/Translator/Translator.php @@ -57,10 +57,7 @@ class Translator extends ZendTranslator protected $loader; /** - * Set the default locale. - * - * @param string $locale - * @return Translator + * {@inheritDoc} */ public function setLocale($locale) { @@ -72,9 +69,7 @@ public function setLocale($locale) } /** - * Get the default locale. - * - * @return string + * {@inheritDoc} */ public function getLocale() { @@ -86,9 +81,7 @@ public function getLocale() } /** - * Get the fallback locale. - * - * @return string + * {@inheritDoc} */ public function getFallbackLocale() { @@ -181,12 +174,7 @@ public function getLoader() } /** - * Translate a message. - * - * @param string $message - * @param string|null $textDomain - * @param string|null $locale - * @return string + * {@inheritDoc} */ public function translate($message, $textDomain = null, $locale = null) { @@ -198,14 +186,7 @@ public function translate($message, $textDomain = null, $locale = null) } /** - * Translate a plural message. - * - * @param string $singular - * @param string $plural - * @param int $number - * @param string|null $textDomain - * @param string|null $locale - * @return string + * {@inheritDoc} */ public function translatePlural( $singular, @@ -221,12 +202,7 @@ public function translatePlural( } /** - * Get a translated message. - * - * @param string $message - * @param string $locale - * @param string $textDomain - * @return string|null + * {@inheritDoc} */ protected function getTranslatedMessage( $message, @@ -259,7 +235,7 @@ protected function getTranslatedMessage( * * @param array|string $domain * @param string|null $locale - * @return Translator + * @return bool */ public function load($domain, $locale = null) { @@ -268,7 +244,7 @@ public function load($domain, $locale = null) $this->setTextDomain($domain[0]); $this->setLocale($locale); - $messages = Pi::service('registry')->i18n->setGenerator(array($this, 'loadResource'))->read($domain, $this->locale); + $messages = (array) Pi::service('registry')->i18n->setGenerator(array($this, 'loadResource'))->read($domain, $this->locale); $this->messages[$this->textDomain][$this->locale] = new TextDomain($messages); //$this->messages[$this->textDomain][$this->locale] = $messages; if ($this->textDomain && $messages) { @@ -283,7 +259,8 @@ public function load($domain, $locale = null) //$this->messages[''][$this->locale] = $messages; } } - return $this; + + return $messages ? true : false; } /** @@ -295,7 +272,19 @@ public function load($domain, $locale = null) public function loadResource($options) { $filename = Pi::service('i18n')->getPath(array($options['domain'], $options['file']), $options['locale']); - $result = (array) $this->loader->load($options['locale'], $filename); + try { + $result = $this->loader->load($options['locale'], $filename); + } catch (\Exception $e) { + $result = false; + } + if (false === $result) { + if (Pi::service()->hasService('log')) { + Pi::service()->getService('log')->info(sprintf('Translation "%s-%s.%s" load failed.', $options['domain'], $options['file'], $options['locale'])); + } + $result = array(); + } else { + $result = (array) $result; + } return $result; } } diff --git a/lib/Pi/Log/Formatter/Debugger.php b/lib/Pi/Log/Formatter/Debugger.php index c16cb6886e..55d62399c3 100644 --- a/lib/Pi/Log/Formatter/Debugger.php +++ b/lib/Pi/Log/Formatter/Debugger.php @@ -73,7 +73,7 @@ public function format($event) } if (!empty($event['extra']['file'])) { /**#@++ - * Remove path prefix for security considerations + * Remove path prefix for security concerns */ $location .= sprintf(' in %s', Pi::service('security')->path($event['extra']['file'])); /**#@-*/ diff --git a/lib/Pi/Log/Logger.php b/lib/Pi/Log/Logger.php index c1d8143cb5..71fa94e21a 100644 --- a/lib/Pi/Log/Logger.php +++ b/lib/Pi/Log/Logger.php @@ -184,6 +184,7 @@ public function getWriters() { return $this->writers; } + /** * Set the writers * diff --git a/lib/Pi/Log/Writer/Debugger.php b/lib/Pi/Log/Writer/Debugger.php index d014ef35a5..9fb060f81a 100644 --- a/lib/Pi/Log/Writer/Debugger.php +++ b/lib/Pi/Log/Writer/Debugger.php @@ -47,6 +47,23 @@ class Debugger extends AbstractWriter 'system' => array(), ); + protected $muted = false; + + /** + * Enable/disable + * + * @param bool $flag + * @return bool return previous muted value + */ + public function mute($flag = true) + { + $muted = $this->muted; + if (null !== $flag) { + $this->muted = (bool) $flag; + } + return $muted; + } + /** * get formatter for loggder writer * @@ -111,6 +128,9 @@ public function systemInfoFormatter() */ protected function doWrite(array $event) { + if ($this->muted) { + return; + } if ($event['priority'] > Logger::DEBUG) { return; } @@ -134,6 +154,9 @@ protected function doWrite(array $event) */ public function doProfiler(array $event) { + if ($this->muted) { + return; + } $message = $this->profilerFormatter()->format($event); $this->logger['profiler'][] = $message; @@ -147,6 +170,9 @@ public function doProfiler(array $event) */ public function doDb(array $event) { + if ($this->muted) { + return; + } $message = $this->dbProfilerFormatter()->format($event); $this->logger['db'][] = $message; } @@ -253,6 +279,9 @@ public function systemInfo() public function render() { + if ($this->muted) { + return; + } $this->systemInfo(); // Use heredoc for log contents diff --git a/lib/Pi/Mvc/View/Http/ErrorStrategy.php b/lib/Pi/Mvc/View/Http/ErrorStrategy.php index 215bb5b32e..f606fdf7e5 100644 --- a/lib/Pi/Mvc/View/Http/ErrorStrategy.php +++ b/lib/Pi/Mvc/View/Http/ErrorStrategy.php @@ -83,6 +83,10 @@ public function prepareErrorViewModel(MvcEvent $e) $viewModel = null; if (!$result instanceof ViewModel) { $viewModel = new ViewModel; + $config = $e->getApplication()->getServiceManager()->get('Config'); + $viewConfig = $config['view_manager']; + $template = isset($viewConfig[$templateName]) ? $viewConfig[$templateName] : 'error'; + $viewModel->setTemplate($template); } else { $viewModel = $result; } @@ -96,11 +100,6 @@ public function prepareErrorViewModel(MvcEvent $e) } $viewModel->setVariable('code', $statusCode); - $config = $e->getApplication()->getServiceManager()->get('Config'); - $viewConfig = $config['view_manager']; - $template = isset($viewConfig[$templateName]) ? $viewConfig[$templateName] : 'error'; - $viewModel->setTemplate($template); - $e->setResult($viewModel); // Inject error ViewModel to root ViewModel in case InjectViewModelListener is not triggered diff --git a/lib/Pi/Mvc/View/Http/ViewStrategyListener.php b/lib/Pi/Mvc/View/Http/ViewStrategyListener.php index 78f6aaff14..5279da0676 100644 --- a/lib/Pi/Mvc/View/Http/ViewStrategyListener.php +++ b/lib/Pi/Mvc/View/Http/ViewStrategyListener.php @@ -65,8 +65,11 @@ public function attach(Events $events) $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, array($this, 'renderThemeAssemble'), 10000); $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER_ERROR, array($this, 'renderThemeAssemble'), 10000); + // Canonize ViewModel for error/exception + $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, array($this, 'canonizeErrorResult'), 10); + // Canonize theme layout if necessary - $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, array($this, 'canonizeThemeLayout'), 10); + $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, array($this, 'canonizeThemeLayout'), 5); // Complete meta assemble for theme $this->listeners[] = $events->attach(MvcEvent::EVENT_FINISH, array($this, 'completeThemeAssemble'), 10000); @@ -107,13 +110,7 @@ public function prepareRequestType(MvcEvent $e) // Disable error debugging for AJAX and Flash if ($type) { - Pi::service('log')->active(false); - /* - // Not implemented yet - if (Pi::service('log')->debugger()) { - Pi::service('log')->debugger()->disable(); - } - */ + Pi::service('log')->mute(); } } @@ -266,6 +263,85 @@ public function canonizeActionResult(MvcEvent $e) $e->setResult($model); } + /** + * Inspect the result for erroneous action of JSON/AJAX/Feed + * + * @param MvcEvent $e + * @return void + */ + public function canonizeErrorResult(MvcEvent $e) + { + $result = $e->getResult(); + if ($result instanceof Response) { + return; + } + if (!$this->type) { + return; + } + + $response = $e->getResponse(); + $statusCode = $response->getStatusCode(); + if ($statusCode < 400) { + return; + } + + // Cast controller view model to result viewmodel + switch ($this->type) { + // For Feed + case 'feed': + if ($result instanceof FeedModel) { + $model = $result; + $model->setTemplate(''); + $model->clearChildren(); + } else { + $variables = array(); + $options = array(); + if ($result instanceof ViewModel) { + $variables = $result->getVariables(); + $options = $result->getOptions(); + } elseif ($result instanceof FeedDataModel) { + $variables = (array) $result; + $options = array('feed_type' => $result->getType()); + } + $model = new FeedModel($variables, $options); + } + break; + // For Json + case 'json': + if ($result instanceof JsonModel) { + $model = $result; + $model->setTemplate(''); + $model->clearChildren(); + } else { + $variables = array(); + $options = array(); + if ($result instanceof ViewModel) { + $variables = $result->getVariables(); + $options = $result->getOptions(); + } + $model = new JsonModel($variables, $options); + } + break; + + // For AJAX/Flash + case 'ajax': + // MISC + default: + if ($result instanceof ViewModel) { + $model = $result; + $model->setTemplate(''); + $model->clearChildren(); + $result = null; + } else { + $model = new ViewModel; + } + $model->terminate(true); + + break; + } + $e->setViewModel($model); + } + /** * Inject a template into the ViewModel if none present, to skip Zend native InjectTemplateListener * diff --git a/lib/Pi/Navigation/Page/AbstractPage.php b/lib/Pi/Navigation/Page/AbstractPage.php index 48c36d643b..33e71cd583 100644 --- a/lib/Pi/Navigation/Page/AbstractPage.php +++ b/lib/Pi/Navigation/Page/AbstractPage.php @@ -28,16 +28,6 @@ */ abstract class AbstractPage extends ZendAbstractPage { - /**#@+ - * Re-initialize - * Modified by Taiwen Jiang - */ - /** - * {@inheritDoc} - */ - protected $active = null; - /**#@-*/ - /** * {@inheritDoc} */ diff --git a/lib/Pi/Navigation/Page/Mvc.php b/lib/Pi/Navigation/Page/Mvc.php index 7b3daac460..5b4975547a 100644 --- a/lib/Pi/Navigation/Page/Mvc.php +++ b/lib/Pi/Navigation/Page/Mvc.php @@ -30,6 +30,16 @@ */ class Mvc extends ZendMvcPage { + /**#@+ + * Re-initialize + * Modified by Taiwen Jiang + */ + /** + * {@inheritDoc} + */ + protected $active = null; + /**#@-*/ + /**#@+ * Added by Taiwen Jiang */ diff --git a/lib/Pi/Navigation/Page/Uri.php b/lib/Pi/Navigation/Page/Uri.php index ac9bda44c8..c8e4c0ed42 100644 --- a/lib/Pi/Navigation/Page/Uri.php +++ b/lib/Pi/Navigation/Page/Uri.php @@ -27,15 +27,18 @@ */ class Uri extends ZendUriPage { + /**#@+ + * Re-initialize + * Modified by Taiwen Jiang + */ + /** + * {@inheritDoc} + */ + protected $active = null; + /**#@-*/ + /** - * Adds a page to the container - * - * This method will inject the container as the given page's parent by - * calling {@link Page\AbstractPage::setParent()}. - * - * @param Page\AbstractPage|array|Traversable $page page to add - * @return AbstractContainer fluent interface, returns self - * @throws Exception\InvalidArgumentException if page is invalid + * {@inheritDoc} * @see Pi\Navigation\Navigation::addPage() * @see Pi\Navigation\Page\Mvc::addPage() */ @@ -75,4 +78,28 @@ public function addPage($page) return $this; } + /** + * {@inheritDoc} + */ + public function isActive($recursive = false) + { + /**#@+ + * Modified by Taiwen Jiang + */ + //if (!$this->active && $recursive) { + if (null === $this->active && $recursive) { + foreach ($this->pages as $page) { + if ($page->isActive(true)) { + $this->active = true; + return true; + } + } + $this->active = false; + return false; + } + /**#@-*/ + + return $this->active; + } + } diff --git a/lib/Pi/Utility/Filter.php b/lib/Pi/Utility/Filter.php index 54da6e0cb3..a8ddd4b36c 100644 --- a/lib/Pi/Utility/Filter.php +++ b/lib/Pi/Utility/Filter.php @@ -225,6 +225,12 @@ public static function fromPost($variable, $filter, $options = null) * $paramFiltered = _filter('+1234.5', 'float', array('flags' => 'allow_thousand')); * * + * Filter a value with regexp, only alphabetic and numeric characters are allowed: + * + * $paramFiltered = _filter($paramRaw, , 'regexp', array('regexp' => '/^[a-z0-9]+$/')); + * $paramFiltered = _get($paramName, , 'regexp', array('regexp' => '/^[a-z0-9]+$/')); + * + * * Sanitize a value: * * $paramSanitized = _sanitize('1234.5', 'int'); diff --git a/lib/Pi/View/Helper/Block.php b/lib/Pi/View/Helper/Block.php index 15172374db..6f26ec553a 100644 --- a/lib/Pi/View/Helper/Block.php +++ b/lib/Pi/View/Helper/Block.php @@ -149,14 +149,6 @@ protected function renderBlock(BlockRow $blockRow, $options = array()) $blockData = null; if ('tab' != $block['type'] && $block['cache_ttl']) { $cacheKey = empty($options) ? md5($block['id']) : md5($block['id'] . serialize($options)); - /* - $cacheKey = 'b' . $cacheKey; - $cacheOptions = array( - 'ttl' => $block['cache_ttl'], - 'namespace' => $block['module'] ?: 'system', - ); - $blockData = Pi::service('cache')->getItem($cacheKey, $cacheOptions); - */ $renderCache = Pi::service('render')->setType('block'); $renderCache->meta('key', $cacheKey) ->meta('namespace', $block['module'] ?: 'system') @@ -179,11 +171,6 @@ protected function renderBlock(BlockRow $blockRow, $options = array()) if (false === $blockData) { return false; } - /* - if ($cacheOptions) { - Pi::service('cache')->setItem($cacheKey, json_encode($blockData), $cacheOptions); - } - */ if ($renderCache) { $renderCache->saveCache(json_encode($blockData)); } diff --git a/lib/Pi/View/Helper/Blocks.php b/lib/Pi/View/Helper/Blocks.php index 5971270ebf..97786351a9 100644 --- a/lib/Pi/View/Helper/Blocks.php +++ b/lib/Pi/View/Helper/Blocks.php @@ -81,14 +81,16 @@ class Blocks extends AbstractHelper * @var array */ protected $zoneMap = array( - 0 => '1', - 1 => '2', - 2 => '3', - 3 => '4', - 4 => '5', - 5 => '6', - 6 => '7', - 7 => '8' + 0 => '0', + 1 => '1', + 2 => '2', + 3 => '3', + 4 => '4', + 5 => '5', + 6 => '6', + 7 => '7', + 8 => '8', + 99 => '99', ); /** diff --git a/usr/locale/README.md b/usr/locale/README.md new file mode 100644 index 0000000000..964ad0ef78 --- /dev/null +++ b/usr/locale/README.md @@ -0,0 +1,42 @@ +Pi Locale +========= + +SPECs +----- +* Top folder name as identifier for a language +* Folder name must be in lowercase +* Folder name, i.e. language tag must respect [RFC 4646](http://www.ietf.org/rfc/rfc4646.txt) +* Loacle files are in CSV format except mail templates which are in plain text +* Both keys and values in CSV must be quoted with double quote (```"```), delimited with comma (```,```) +* All files must be encoded in the same charset as Pi system, default as ```UTF-8``` + +Skeleton +-------- + +* ```usr/locale```: Global + * ```/en``` + * ```main.csv```: Global, loaded on every request + * ```navigation.csv```: Navigation and menu + * ```timezone.csv```: Timezone + * ... + * ```/zh-cn``` + * ... +* ```usr/module/demo```: Module ```demo``` + * ```/en``` + * ```/mail```: Mail templates + * ```mail-template.text``` + * ```main.csv```: module global, loaded on every request of current module + * ```navigation.csv``` + * ```admin.csv```: Admin area + * ```config.csv```: Config edit + * ```feed.csv```: Feed + * ... + * ```/zh-cn``` + * ... +* ```usr/theme/default```: Theme ```default``` + * ```/en``` + * ```main.csv```: theme global + * ```admin.csv```: Admin area + * ... + * ```/zh-cn``` + * ... diff --git a/usr/module/page/src/Route/Page.php b/usr/module/page/src/Route/Page.php index 31a181765e..3e6e307d53 100644 --- a/usr/module/page/src/Route/Page.php +++ b/usr/module/page/src/Route/Page.php @@ -134,7 +134,9 @@ public function assemble(array $params = array(), array $options = array()) if (empty($url)) { $url = $action; } elseif (!empty($action)) { - $url = $action . $this->paramDelimiter . $url; + if ($action != 'index') { + $url = $action . $this->paramDelimiter . $url; + } } else { $url = 'view' . $this->paramDelimiter . $url; } diff --git a/usr/module/page/template/admin/page-list.phtml b/usr/module/page/template/admin/page-list.phtml index f5e4930b0a..9947374c32 100644 --- a/usr/module/page/template/admin/page-list.phtml +++ b/usr/module/page/template/admin/page-list.phtml @@ -1,6 +1,8 @@ css($this->assetModule('script/system-ui.css','system')); $this->jQuery(); + $this->Backbone(); + $this->js($this->assetModule('script/system-msg.js','system')); ?> -
      +
        -
      • +
      • @@ -24,18 +26,19 @@

        escape($page['title']); ?>

        -
        -
        -
        -
        escape($page['slug']) : ''; ?> +
        +
        +
        +
        escape($page['slug']) : ''; ?>
        +
      -
      +
        -
      • +
      • @@ -44,11 +47,12 @@

        escape($page['title']); ?>

        -
        -
        -
        -
        escape($page['slug']) : ''; ?> +
        +
        +
        +
        escape($page['slug']) : ''; ?>
        +
      @@ -56,64 +60,40 @@
      \ No newline at end of file diff --git a/usr/module/system/asset/script/system-msg.js b/usr/module/system/asset/script/system-msg.js index 9d52192547..417088b527 100755 --- a/usr/module/system/asset/script/system-msg.js +++ b/usr/module/system/asset/script/system-msg.js @@ -1,10 +1,10 @@ var systemMessage = { - tmp: '
      {msg}
      ', - _css: '.system-layer {position: fixed;top: -21px;height: 20px;line-height: 20px;z-index: 13;width: 100%;text-align: center;}.system-layer-show {top: 0;}.system-layer-msg {margin-left: 10px;}', + tmp: '
      ' + + '' + + '{msg}' + + '
      ', _init: function() { - var b = document.body; - $('