- <?php
- /**
- * Themes and modules are 'packages'. Themes and modules have some
- * shared functionality and this class exists to avoid duplicating code.
- *
- * @copyright 2011 Perforce Software. All rights reserved.
- * @license Please see LICENSE.txt in top-level folder of this distribution.
- * @version <release>/<patch>
- */
- abstract class P4Cms_PackageAbstract extends P4Cms_Model
- {
- const PACKAGE_FILENAME = 'package.ini';
-
- protected $_path = null;
- protected $_packageInfo = null;
- protected $_dojoModules = null;
- protected static $_documentRoot = null;
- protected static $_packagesPaths = array();
-
- /**
- * Return the name of this package as the id.
- * Needed to satisfy model config parent class.
- *
- * @return string the name of this package.
- * @todo remove this method when class no longer extends model config.
- */
- public function getId()
- {
- return $this->getName();
- }
-
- /**
- * Fetch a single package by name from the set of packages.
- *
- * @param string $name the name of the package to fetch.
- * @return P4Cms_PackageAbstract the matching package if one exists.
- * @throws P4Cms_Model_NotFoundException if the requested package can't be found.
- */
- public static function fetch($name)
- {
- // throw exception if no name given.
- if (!is_string($name) || !$name) {
- throw new InvalidArgumentException(
- "Can't fetch package. No package name given."
- );
- }
-
- // validate package name - package must exist.
- if (!static::exists($name)) {
- throw new P4Cms_Model_NotFoundException(
- "Invalid package name: '" . $name . "'."
- );
- }
-
- // return requested package model.
- $packages = static::fetchAll();
- return $packages[strtolower($name)];
- }
-
- /**
- * Get all packages (of this class type) available to the system.
- *
- * Looks for packages under the paths that have been registered
- * via addPackagesPath().
- *
- * @return P4Cms_Model_Iterator all installed packages.
- */
- public static function fetchAll()
- {
- $cacheId = static::_getCacheId();
- $cached = P4Cms_Cache::load($cacheId);
- if ($cached !== false) {
- return $cached;
- }
-
- // collect all packages.
- $packages = new P4Cms_Model_Iterator;
- foreach (static::getPackagesPaths() as $packagesPath) {
- if (is_dir($packagesPath)) {
- $directory = new DirectoryIterator($packagesPath);
- foreach ($directory as $entry) {
- if ($entry->isDir()
- && !$entry->isDot()
- && is_file($entry->getPathname() . '/' . static::PACKAGE_FILENAME)
- ) {
- $package = new static;
- $package->setPath($entry->getPathname());
-
- // force a populate now if we are caching, to avoid
- // repeated lazy loading when reading from cache.
- if (P4Cms_Cache::canCache()) {
- $package->populate();
- }
-
- $packages[strtolower($package->getName())] = $package;
- }
- }
- }
- }
-
- // put packages in sorted order.
- $packages->sortBy('name', array(P4Cms_Model_Iterator::SORT_ALPHA));
-
- // cache packages.
- P4Cms_Cache::save($packages, $cacheId);
-
- return $packages;
- }
-
- /**
- * Clear the cached list of packages.
- *
- * @return bool true if the cache entry was cleared; otherwise false.
- */
- public static function clearCache()
- {
- return P4Cms_Cache::remove(static::_getCacheId());
- }
-
- /**
- * Read in relevant data for this package.
- *
- * Useful when caching packages as it ensures the cached
- * copies are primed with the information we care about.
- *
- * @return P4Cms_PacakgeAbstract provides fluent interface.
- */
- public function populate()
- {
- $this->getPackageInfo();
-
- // get dojo modules to determine which modules the user
- // has access to. we want this cached to avoid querying
- // acl every request, we are able to cache it because we
- // incorporate the user's roles into the cache key.
- $this->getDojoModules();
- }
-
- /**
- * Determine if a package with the given name exists.
- *
- * @param string $name the name of the package to look for.
- * @return bool true if the named package exists.
- */
- public static function exists($name)
- {
- $packages = static::fetchAll();
- if (!isset($packages[strtolower($name)]) || empty($name)) {
- return false;
- } else {
- return true;
- }
- }
-
- /**
- * Add a path to the set of paths from which packages (of this
- * class type) can be sourced.
- *
- * The order that packages paths are added is significant.
- * If a package exists in two paths, the path that was added last
- * wins.
- *
- * @param string $path a path that can contain packages.
- */
- public static function addPackagesPath($path)
- {
- if (!in_array($path, static::$_packagesPaths)) {
- static::$_packagesPaths[] = $path;
- }
- }
-
- /**
- * Get the set of paths from which packages (of this class
- * type) can be sourced.
- *
- * @return array the list of paths that can contain packages.
- */
- public static function getPackagesPaths()
- {
- return static::$_packagesPaths;
- }
-
- /**
- * Set the set of paths from which packages (of this class
- * type) can be sourced.
- *
- * @param array $paths the list of paths that can contain packages.
- */
- public static function setPackagesPaths($paths)
- {
- // don't do anything if $paths is not an array
- if (!is_array($paths)) {
- return;
- }
-
- static::$_packagesPaths = $paths;
- }
-
- /**
- * Clear the set of paths from which packages can be sourced.
- */
- public static function clearPackagesPaths()
- {
- static::$_packagesPaths = array();
- }
-
- /**
- * Set the full path to the package folder.
- *
- * @param string $path the full path to the package folder.
- * @return P4Cms_PackageAbstract provides fluent interface.
- */
- public function setPath($path)
- {
- // ensure given path is a string.
- if (!is_string($path) || !$path) {
- throw new InvalidArgumentException("Cannot set path. Path is not a string.");
- }
-
- // ensure path exists.
- if (!is_dir($path)) {
- throw new P4Cms_Package_Exception(
- "Cannot set package path. Path does not exist."
- );
- }
-
- // set the path in the instance.
- $this->_path = rtrim($path, '/');
-
- return $this;
- }
-
- /**
- * Get the path to this package.
- *
- * @return string the path to this package.
- * @throws P4Cms_PackageException if the path has not been set.
- */
- public function getPath()
- {
- if ($this->_path === null) {
- throw new P4Cms_Package_Exception("Cannot get path. Path has not been set.");
- }
-
- return $this->_path;
- }
-
- /**
- * Get the name of this package.
- * The name is dervied from the basename of the path.
- *
- * @return string the name of this package.
- */
- public function getName()
- {
- return basename($this->getPath());
- }
-
- /**
- * Get the package configuration by parsing the package file.
- *
- * @param string $key optional - the name of a specific value to get,
- * null if no such key exists.
- * @return array an array containing the package definition.
- */
- public function getPackageInfo($key = null)
- {
- // parse package info file into array - if we haven't already
- $info = $this->_packageInfo;
- if ($info === null) {
- $packageFile = $this->getPath() . '/' . static::PACKAGE_FILENAME;
- if (is_readable($packageFile)) {
- try {
- $config = new Zend_Config_Ini($packageFile);
- $info = $config->toArray();
- } catch (Zend_Config_Exception $e) {
- P4Cms_Log::logException("Unable to read package information.", $e);
- }
- }
- $this->_packageInfo = isset($info) ? $info : array();
- }
-
- // if caller gave a key, return value at key, or null if no such key.
- if ($key) {
- return isset($info[$key]) ? $info[$key] : null;
- }
-
- return $info;
- }
-
- /**
- * Get a friendly label for this package. The value is taken from the
- * 'label' field of the package falling back to 'title' as an alternate
- * storage location and lastly running a 'ucfirst' on name.
- *
- * @return null|string a friendly label for this package
- */
- public function getLabel()
- {
- $info = $this->getPackageInfo() + array('label' => '', 'title' => '');
- $label = $info['label'] ?: $info['title'];
- return $label ?: ucfirst($this->getName());
- }
-
- /**
- * Get the description of this package from the package info file.
- *
- * @return null|string the description of this package if it has one.
- */
- public function getDescription()
- {
- $info = $this->getPackageInfo();
- return isset($info['description']) ? (string) $info['description'] : null;
- }
-
- /**
- * Get the version of this package from the package info file.
- *
- * @return null|string the version of this package if it has one.
- */
- public function getVersion()
- {
- $info = $this->getPackageInfo();
- return isset($info['version']) ? (string) $info['version'] : null;
- }
-
- /**
- * Determine if there is an icon for this package.
- *
- * @return bool true if this package has an icon.
- */
- public function hasIcon()
- {
- $info = $this->getPackageInfo();
- return isset($info['icon']) && is_string($info['icon']);
- }
-
- /**
- * Get the URI to the package icon file.
- *
- * @return string the URI of the package icon.
- * @throws P4Cms_Package_Exception if there is no icon.
- */
- public function getIconUrl()
- {
- if (!$this->hasIcon()) {
- throw new P4Cms_Package_Exception(
- "Cannot get icon URI. This package has no icon."
- );
- }
-
- $info = $this->getPackageInfo();
- $uri = $info['icon'];
-
- return (P4Cms_Uri::isRelativeUri($uri)) ? $this->getBaseUrl() . '/' . $uri : $uri;
- }
-
- /**
- * Get information about the maintainer of this package if available.
- * For example: name, email and url.
- *
- * @param string $field optional - the name of a specific maintainer
- * field to get (e.g. name, email, url).
- * @return array|string|null array of all maintainer information, or a specific
- * field, or null if no maintainer info.
- */
- public function getMaintainerInfo($field = null)
- {
- $info = $this->getPackageInfo();
- if ($field) {
- return isset($info['maintainer'][$field]) ? $info['maintainer'][$field] : null;
- } else {
- return isset($info['maintainer']) && is_array($info['maintainer'])
- ? $info['maintainer'] : null;
- }
- }
-
- /**
- * Get the url to this package folder.
- *
- * @return string the base url of this package.
- */
- public function getBaseUrl()
- {
- // can't produce base url if the package is not under the public path.
- if (strpos($this->getPath(), static::getDocumentRoot()) !== 0) {
- throw new P4Cms_Package_Exception(
- "Cannot get package base url. Package is not under the public path."
- );
- }
-
- $request = Zend_Controller_Front::getInstance()->getRequest();
- if ($request instanceof Zend_Controller_Request_Http) {
- $baseUrl = $request->getBaseUrl();
- } else {
- $baseUrl = null;
- }
-
- $baseUrl = $baseUrl . "/" . str_replace(
- static::getDocumentRoot() . '/',
- '',
- $this->getPath()
- );
-
- // On Windows, getPath() returns a path containing backslashes.
- // Replace backslashes with the forward slashes.
- return str_replace('\\', '/', $baseUrl);
- }
-
- /**
- * Get meta listed in the package file in a format suitable for
- * passing to Zend's headMeta helper.
- *
- * Only supports arrays that include a key, so charset[] is not supported
- *
- * @return array associative array of meta included by this package.
- */
- public function getHtmlMeta()
- {
- // ensure metas is an array
- $info = $this->getPackageInfo();
- if (!isset($info['meta']) || !is_array($info['meta'])) {
- return array();
- }
-
- // build set of valid meta fields
- $meta = array();
- $types = $info['meta'];
-
- foreach ($types as $type => $fields) {
- foreach ($fields as $field => $content) {
- // content must be string with length.
- if (!is_string($content) || !strlen($content)) {
- continue;
- }
-
- $meta[] = array(
- 'type' => $type,
- 'field' => $field,
- 'content' => $content
- );
- }
- }
-
- return $meta;
- }
-
- /**
- * Get stylesheets listed in the package file in a format suitable for
- * passing to Zend's headLink helper.
- *
- * The package file groups stylesheets by media type for aesthetic reasons.
- * Here we flatten the list to make it easier to work with.
- *
- * @return array associative array of stylesheets included by this package.
- */
- public function getStylesheets()
- {
- // ensure stylesheets is an array.
- $info = $this->getPackageInfo();
- if (!isset($info['stylesheets']) || !is_array($info['stylesheets'])) {
- return array();
- }
-
- // build set of valid stylesheets.
- $styles = array();
- $groups = $info['stylesheets'];
- foreach ($groups as $name => $group) {
- // set media to 'all' if it's not provided or empty
- if (isset($group['media'])) {
- $media = implode(', ', (array) $group['media']);
- }
- if (!isset($media) || !trim($media)) {
- $media = 'all';
- }
-
- // conditional stylesheet
- $conditional = isset($group['condition']) && is_string($group['condition'])
- ? $group['condition'] : '';
-
- // skip the stylesheet if no url set
- if (!isset($group['href'])) {
- continue;
- }
-
- // nomalize the hrefs to an array
- foreach ((array) $group['href'] as $url) {
- // url must be string with length.
- if (!is_string($url) || !strlen($url)) {
- continue;
- }
-
- // make url relative to package baseUrl.
- if (P4Cms_Uri::isRelativeUri($url)) {
- $url = $this->getBaseUrl() . '/' . $url;
- }
-
- // add to styles list.
- $style = array(
- 'href' => $url,
- 'media' => $media,
- 'conditional' => $conditional
- );
- $styles[] = $style;
- }
- }
-
- return $styles;
- }
-
- /**
- * Get tags listed in the package file.
- *
- * @return array The list of tags included in this package; the array could be empty.
- */
- public function getTags()
- {
- $info = $this->getPackageInfo();
- $tags = isset($info['tags']) ? preg_split('/,|\s/', $info['tags']) : array();
- $tags = array_filter(array_map('trim', $tags));
-
- return $tags;
- }
-
- /**
- * Get scripts listed in the package file in a format suitable for
- * passing to the headScript helper.
- *
- * The package file groups scripts by type for aesthetic reasons.
- * Here we flatten the list to make it easier to work with.
- *
- * @return array associative array of scripts included by this package.
- */
- public function getScripts()
- {
- // ensure scripts is an array.
- $info = $this->getPackageInfo();
- if (!isset($info['scripts']) || !is_array($info['scripts'])) {
- return array();
- }
-
- // build set of valid scripts.
- $scripts = array();
- $types = $info['scripts'];
- foreach ($types as $type => $urls) {
- foreach ($urls as $url) {
-
- // url must be string with length.
- if (!is_string($url) || !strlen($url)) {
- continue;
- }
-
- // make url relative to package baseUrl.
- if (P4Cms_Uri::isRelativeUri($url)) {
- $url = $this->getBaseUrl() . '/' . $url;
- }
-
- // add to scripts list.
- $script = array(
- 'src' => $url,
- 'type' => "text/" . $type,
- 'attrs' => array()
- );
- $scripts[] = $script;
- }
- }
-
- return $scripts;
- }
-
- /**
- * Get all dojo modules that are defined by this module
- *
- * @return array a list of dojo modules
- */
- public function getDojoModules()
- {
- $info = $this->getPackageInfo();
- if (!isset($info['dojo']) || !is_array($info['dojo'])) {
- return array();
- }
-
- // if we already have a cached set return it.
- // note: we cache mainly for the acl checks.
- if ($this->_dojoModules) {
- return $this->_dojoModules;
- }
-
- $modules = array();
- $groups = $info['dojo'];
- foreach ($groups as $name => $group) {
- if ($name === 'addOnLoad') {
- continue;
- }
-
- // path must be string with length.
- if (!isset($group['path']) || !is_string($group['path']) || !strlen($group['path'])) {
- continue;
- }
-
- // make path relative to package baseUrl.
- $path = $group['path'];
- if (P4Cms_Uri::isRelativeUri($path)) {
- $path = $this->getBaseUrl() . '/' . $path;
- }
-
- // dojo modules can be limited by acl. this is intended to avoid
- // loading modules for features that the user can't access anyway.
- // acl limits be must declared as a list of resources with each
- // resource having a list of privileges (may be comma delimited).
- $acl = array();
- if (isset($group['acl']) && is_array($group['acl'])) {
- foreach ($group['acl'] as $resource => $privileges) {
- $privileges = is_array($privileges)
- ? $privileges
- : explode(",", $privileges);
- $acl[$resource] = array_filter($privileges, 'trim');
- }
- }
-
- $module = array(
- 'namespace' => $group['namespace'],
- 'path' => $path,
- 'allowed' => $this->_passesAcl($acl)
- );
-
- $modules[] = $module;
- }
-
- $this->_dojoModules = $modules;
- return $modules;
- }
-
- /**
- * Get dojo 'addOnLoad' entries for this package.
- *
- * @return array a list of addOnLoad scripts
- */
- public function getDojoOnLoads()
- {
- $info = $this->getPackageInfo();
- if (!isset($info['dojo']['addOnLoad'])
- || !is_array($info['dojo']['addOnLoad'])
- ) {
- return array();
- }
-
- return $info['dojo']['addOnLoad'];
- }
-
- /**
- * Get current view object from the view renderer.
- *
- * @return Zend_View_Interface the current view object.
- */
- public static function getView()
- {
- $renderer = static::getViewRenderer();
- if (!$renderer->view) {
- $renderer->initView();
- }
- return $renderer->view;
- }
-
- /**
- * Get the P4CMS (theme-aware) view renderer - load it if necessary.
- *
- * @return P4Cms_Controller_Action_Helper_ViewRenderer the view renderer.
- */
- public static function getViewRenderer()
- {
- $renderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
- if (!$renderer instanceof P4Cms_Controller_Action_Helper_ViewRenderer) {
- $renderer = new P4Cms_Controller_Action_Helper_ViewRenderer;
- Zend_Controller_Action_HelperBroker::addHelper($renderer);
- }
- return $renderer;
- }
-
- /**
- * Set the file-system path to the document root.
- *
- * @param string $path the location of the public folder.
- */
- public static function setDocumentRoot($path)
- {
- static::$_documentRoot = rtrim($path, '/');
- }
-
- /**
- * Get the file-system path to the document root.
- *
- * @return string the location of the public folder.
- * @throws P4Cms_Package_Exception if the doc root has not been set.
- */
- public static function getDocumentRoot()
- {
- if (!strlen(static::$_documentRoot)) {
- throw new P4Cms_Package_Exception(
- "Cannot get document root. The document root has not been set."
- );
- }
-
- return static::$_documentRoot;
- }
-
- /**
- * Get any menus configured for this module.
- *
- * @return array list of menus from module.ini.
- */
- public function getMenus()
- {
- $info = $this->getPackageInfo();
- return isset($info['menus']) && is_array($info['menus']) ? $info['menus'] : array();
- }
-
- /**
- * Get the widget configuration defined by the package (grouped by region).
- *
- * @return array a list of regions and default widget configuration for those regions.
- */
- public function getWidgetConfig()
- {
- $info = $this->getPackageInfo();
- $widgets = array();
- if (isset($info['regions']) && is_array($info['regions'])) {
- $widgets = $info['regions'];
- }
- return $widgets;
- }
-
- /**
- * Get cache id for this class constructed from the called class name and
- * a serialized set of packages source paths. We also include the user's
- * roles as the applicable dojo modules depend on the user's permissions.
- */
- protected static function _getCacheId()
- {
- $packagesPaths = array_unique(static::getPackagesPaths());
- sort($packagesPaths);
-
- $roles = P4Cms_User::hasActive()
- ? P4Cms_User::fetchActive()->getRoles()->invoke('getId')
- : array();
-
- return get_called_class() . md5(serialize(array($packagesPaths, $roles)));
- }
-
- /**
- * Load the meta tags in this package into the view headMeta helper
- */
- protected function _loadHtmlMeta()
- {
- $view = $this->getView();
- foreach ($this->getHtmlMeta() as $meta) {
- switch ($meta['type']) {
- case 'httpEquiv':
- $view->headMeta()->setHttpEquiv($meta['field'], $meta['content']);
- break;
- case 'name':
- $view->headMeta()->setName($meta['field'], $meta['content']);
- break;
- }
- }
- }
-
- /**
- * Load the stylesheets in this package into the view headLink helper.
- */
- protected function _loadStylesheets()
- {
- $view = $this->getView();
- foreach ($this->getStylesheets() as $stylesheet) {
- $view->headLink()->appendStylesheet(
- $stylesheet['href'],
- $stylesheet['media'],
- $stylesheet['conditional'],
- array('buildGroup' => 'packages')
- );
- }
- }
-
- /**
- * Load the scripts in this package into the view headScript helper.
- */
- protected function _loadScripts()
- {
- $view = $this->getView();
- foreach ($this->getScripts() as $script) {
- $view->headScript()->appendFile($script['src'], $script['type'], $script['attrs']);
- }
- }
-
- /**
- * Takes care of the 'dojo' section of package config including
- * requires, provides and onLoad.
- */
- protected function _loadDojo()
- {
- // enable dojo view helper.
- $view = $this->getView();
- Zend_Dojo::enableView($view);
-
- // load defined dojo modules
- $dojoModules = $this->getDojoModules();
- foreach ($dojoModules as $module) {
- // always register every module path
- $view->dojo()->registerModulePath($module['namespace'], $module['path']);
-
- // require modules that pass acl
- if ($module['allowed']) {
- $view->dojo()->requireModule($module['namespace']);
- }
- }
-
- // deal with addOnLoad
- foreach ($this->getDojoOnLoads() as $onLoad) {
- $view->dojo()->addOnLoad($onLoad);
- }
- }
-
- /**
- * Checks whether a dojo module should be loaded based on the resource privileges
- * If any resource or privilege matches, the user needs this dojo module.
- *
- * @param array $acl list of resources as keys with privileges as values
- * @return bool true if dojo module's resources/privilege are allowed by
- * the current user; false otherwise.
- *
- */
- protected function _passesAcl($acl)
- {
- // if item has no acl resource, nothing to check.
- if (empty($acl)) {
- return true;
- }
-
- // if no active user, can't check acl - assume the worst.
- if (!P4Cms_User::hasActive()) {
- return false;
- }
-
- // match any of the resource privileges
- foreach ($acl as $resource => $privileges) {
- foreach ($privileges as $privilege) {
- if (P4Cms_User::fetchActive()->isAllowed($resource, $privilege)) {
- return true;
- }
- }
- }
-
- return false;
- }
- }