<?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;
}
}