diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 3941f1a..0d1b809 100755 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -150,7 +150,11 @@ public function __set($name, $value) { break; case 'int': - $this->$name = (int)$value; + if ('' === $value and $this->isNullable($this->getMappedField($name))) { + $this->$name = NULL; + } else { + $this->$name = (int)$value; + } break; case 'DateTime': @@ -216,11 +220,24 @@ protected function init() {} /** * Returns array with matching object property name on related db fields. * - * @return array + * @return array */ protected static function getBinds() { + + $db = Database::getInstance(); + $columns = $db->describeTable(static::TABLE_NAME); - return array(); + $maps = []; + + foreach ($columns as $col) { + + // get a camelCase name, with first low case + $property = lcfirst(str_replace(' ', '', ucwords(str_replace(['_','\\'], ' ', $col->Field)))); + $maps[$property] = $col->Field; + + } + + return $maps; } @@ -307,7 +324,7 @@ final private function prepareData($properties) { // join array strings in CSV format case 'csv': - $ret = implode(',', array_filter($this->$prop)); + $ret = implode(',', array_filter((array)$this->$prop)); break; case 'float': @@ -904,6 +921,115 @@ final protected function bindAsCsv() { } + /** + * Return the Pair\ActiveRecord inherited object related to this by a ForeignKey in DB-table. + * + * @param string Related property name. + * + * @return multitype|NULL + */ + final public function getRelated($relatedProperty) { + + $cacheName = $relatedProperty . 'RelatedObject'; + + // object exists in cache, return it + if ($this->issetCache($cacheName)) { + return $this->getCache($cacheName); + } + + // get table foreign-keys + $foreignKeys = $this->db->getForeignKeys(static::TABLE_NAME); + + // get field name by mapped property + $relatedField = $this->getMappedField($relatedProperty); + + // the table referenced by fk + $referencedTable = NULL; + + // search the fk-table + foreach ($foreignKeys as $fk) { + if ($fk->COLUMN_NAME == $relatedField) { + $referencedTable = $fk->REFERENCED_TABLE_NAME; + $referencedColumn = $fk->REFERENCED_COLUMN_NAME; + break; + } + } + + // if not table is referenced, raise an error + if (!$referencedTable) { + $this->addError('Property ' . $relatedProperty . ' has not a foreign-key mapped into DB'); + return NULL; + } + + // class that maps the referenced table + $relatedClass = NULL; + $loadedClasses = \get_declared_classes(); + + // search in loaded classes + foreach ($loadedClasses as $c) { + if (is_subclass_of($c, 'Pair\ActiveRecord') and $c::TABLE_NAME == $referencedTable) { + $relatedClass = $c; + break; + } + } + + // class cannot be found + if (!$relatedClass) { + + // if not found, search in the whole application (FIXME encapsulation violated here...) + $classes = Utilities::getActiveRecordClasses(); + + // search for required one + foreach ($classes as $class => $opts) { + if ($opts['tableName'] == $referencedTable) { + include_once($opts['folder'] . '/' . $opts['file']); + $relatedClass = $class; + break; + } + } + + } + + // class cannot be found + if (!$relatedClass) { + $this->addError('Table ' . $referencedTable . ' has not any Pair-class mapping'); + return NULL; + } + + // create the new wanted Pair object + $obj = new $relatedClass($this->$relatedProperty); + + // if loaded, return it otherwise NULL + $ret = ($obj->isLoaded() ? $obj : NULL); + + // related object is being registered in cache of this object + $this->setCache($cacheName, $ret); + + return $ret; + + } + + /** + * Extended method to return a property value of the Pair\ActiveRecord inherited object related to + * this by a ForeignKey in DB-table. + * + * @param string Related property name. + * @param string Wanted property name. + * + * @return multitype|NULL + */ + final public function getRelatedProperty($relatedProperty, $wantedProperty) { + + $obj = $this->getRelated($relatedProperty); + + if ($obj) { + return $obj->$wantedProperty; + } else { + return NULL; + } + + } + /** * Create an object for a table column configuration within an object or NULL if column * doesn’t exist. @@ -1639,16 +1765,13 @@ final static public function getMappedField($propertyName) { */ final public function populateByRequest() { - $args = func_get_args(); - $class = get_called_class(); + $args = func_get_args(); // all subclass binds - $binds = $class::getBinds(); - - $properties = array(); + $binds = static::getBinds(); foreach ($binds as $property => $field) { - + // check that property is in the args or that args is not defined at all if (!count($args) or (isset($args[0]) and in_array($property, $args[0]))) { @@ -1657,7 +1780,8 @@ final public function populateByRequest() { if (Input::isSent($property) or 'bool' == $type) { // assign the value to this object property - $this->__set($property, Input::get($property, $type)); + $this->__set($property, Input::get($property)); + } } diff --git a/src/Application.php b/src/Application.php index 85443be..ade1662 100755 --- a/src/Application.php +++ b/src/Application.php @@ -17,7 +17,7 @@ class Application { * Singleton property. * @var Application|NULL */ - static private $instance; + static protected $instance; /** * List of temporary variables. @@ -91,12 +91,6 @@ class Application { */ private $template; - /** - * Keep the name of a base template in case of derived one. - * @var NULL|Template - */ - private $baseTemplate; - /** * Template-style’s file name (without extension). * @var string @@ -244,33 +238,36 @@ private function __construct() { * @return multitype */ public function __get($name) { - + switch ($name) { - + + /** + * for login page we need a default template + * @deprecated + */ case 'templatePath': - // for login page we need a default template - $this->checkTemplate(); - $templateName = $this->template->derived ? $this->baseTemplate->name : $this->template->name; - $value = 'templates/' . $templateName . '/'; + $value = $this->getTemplate()->getPath(); break; // useful in html tag to set language code case 'langCode': - $language = new Language($this->currentUser->languageId); - $value = $language->code; + $translator = Translator::getInstance(); + $value = $translator->getCurrentLanguage()->code; break; default: + $allowedProperties = ['activeMenuItem', 'currentUser', 'pageTitle', 'pageContent', 'template']; + // search into variable assigned to the template as first if (array_key_exists($name, $this->vars)) { $value = $this->vars[$name]; // then search in properties - } else if (property_exists($this, $name)) { + } else if (property_exists($this, $name) and in_array($name, $allowedProperties)) { $value = $this->$name; @@ -321,11 +318,10 @@ protected function setCurrentUser($user) { if (is_a($user,'Pair\User')) { $this->currentUser = $user; - $this->checkTemplate(); // sets user language $tran = Translator::getInstance(); - $lang = new Language($user->languageId); + $lang = $user->languageId ? $user->getRelated('languageId') : Language::getDefault(); $tran->setLanguage($lang); } @@ -905,28 +901,6 @@ final public function startMvc() { // populate the placeholder for the content $this->pageContent = ob_get_clean(); - // login page has no template, needs a default - $this->checkTemplate(); - - $templatesPath = APPLICATION_PATH . '/templates/' ; - - // by default load template style - $styleFile = $templatesPath . $this->template->name . '/' . $this->style . '.php'; - - // in case of derived template, should load the base template - if ($this->template->derived) { - - // try to load derived extend file - $derivedFile = $templatesPath . $this->template->name . '/derived.php'; - if (file_exists($derivedFile)) require $derivedFile; - - // if no template style, load default template style - if (!file_exists($styleFile) and is_a($this->baseTemplate, 'Pair\Template')) { - $styleFile = $templatesPath . $this->baseTemplate->name . '/' . $this->style . '.php'; - } - - } - // initialize CSS and scripts $this->pageStyles = ''; $this->pageScripts = ''; @@ -971,25 +945,16 @@ final public function startMvc() { } try { - - if (!file_exists($styleFile)) { - throw new \Exception('Template style file ' . $styleFile . ' was not found'); - } - - // load the style page file - require $styleFile; - - // get output buffer and cleans it - $page = ob_get_clean(); - - print $page; - + $this->getTemplate()->loadStyle($this->style); } catch (\Exception $e) { - print $e->getMessage(); - } + // get output buffer and cleans it + $page = ob_get_clean(); + + print $page; + } /** @@ -1007,10 +972,11 @@ final private function getMessageScript() { $types = array('info', 'warning', 'error'); if (!in_array($m->type, $types)) $m->type = 'info'; - $script .= '$.showMessage("'. - addslashes($m->title) .'","' . + $script .= '$.showMessage("' . + addslashes($m->title) . '","' . addcslashes($m->text,"\"\n\r") . '","' . // removes carriage returns and quotes - addslashes($m->type) ."\");\n"; + addslashes($m->type) . "\");\n"; + } } @@ -1020,24 +986,24 @@ final private function getMessageScript() { } /** - * Set the name of base template in case of derived one in use. + * If current selected template is not valid, replace it with the default one. It’s private to avoid + * loops on derived templates load. * - * @param string Template name. + * @return Pair\Template */ - final public function setBaseTemplate($templateName) { - - $this->baseTemplate = Template::getTemplateByName($templateName); + final private function getTemplate() { - } - - /** - * If current selected template is not valid, replace it with the default one. - */ - final private function checkTemplate() { - if (!$this->template or !$this->template->isPopulated()) { $this->template = Template::getDefault(); } + + // if this is derived template, load derived.php file + if ($this->template->derived) { + $derivedFile = $this->template->getBaseFolder() . '/' . strtolower($this->template->name) . '/derived.php'; + if (file_exists($derivedFile)) require $derivedFile; + } + + return $this->template; } diff --git a/src/Controller.php b/src/Controller.php index 03612d3..1e391ef 100755 --- a/src/Controller.php +++ b/src/Controller.php @@ -73,7 +73,7 @@ final public function __construct() { $this->model = new $modelName(); // sets language subfolder’s name - $this->translator->module = $this->name; + $this->translator->setModule($this->name); // sets same view as the controller action $this->view = $this->route->action ? $this->route->action : 'default'; @@ -209,7 +209,8 @@ final public function enqueueError($text, $title='') { */ final public function logEvent($description, $type='notice', $subtext=NULL) { - $this->app->logEvent($description, $type, $subtext); + $logger = Logger::getInstance(); + $logger->addEvent($description, $type, $subtext); } @@ -220,7 +221,8 @@ final public function logEvent($description, $type='notice', $subtext=NULL) { */ final public function logWarning($description) { - $this->app->logWarning($description); + $logger = Logger::getInstance(); + $logger->addWarning($description); } @@ -231,7 +233,8 @@ final public function logWarning($description) { */ final public function logError($description) { - $this->app->logError($description); + $logger = Logger::getInstance(); + $logger->addError($description); } diff --git a/src/Database.php b/src/Database.php index d183be6..03de381 100755 --- a/src/Database.php +++ b/src/Database.php @@ -688,6 +688,7 @@ public function getInverseForeignKeys($tableName) { */ public function describeTable($tableName): array { + // check if was set in the object cache property if (!isset($this->definitions[$tableName]['describe'])) { $this->setQuery('DESCRIBE `' . $tableName . '`'); diff --git a/src/Menu.php b/src/Menu.php index fceb870..fba99d9 100755 --- a/src/Menu.php +++ b/src/Menu.php @@ -104,7 +104,7 @@ public function render() { foreach ($this->items as $item) { // check on permissions - if (isset($item->url) and !$app->currentUser->canAccess($item->url)) { + if (isset($item->url) and !(is_a($app->currentUser, 'Pair\User') and !$app->currentUser->canAccess($item->url))) { continue; } @@ -143,7 +143,7 @@ public function render() { foreach ($item->list as $i) { // check on permissions - if (isset($i->url) and !$app->currentUser->canAccess($i->url)) { + if (isset($i->url) and !(is_a($app->currentUser, 'Pair\User') and !$app->currentUser->canAccess($i->url))) { continue; } diff --git a/src/Model.php b/src/Model.php index e611e25..1f8e3c2 100755 --- a/src/Model.php +++ b/src/Model.php @@ -125,7 +125,8 @@ public function getErrors() { */ public function logEvent($description, $type='notice', $subtext=NULL) { - $this->app->logEvent($description, $type, $subtext); + $logger = Logger::getInstance(); + $logger->addEvent($description, $type, $subtext); } @@ -136,7 +137,8 @@ public function logEvent($description, $type='notice', $subtext=NULL) { */ public function logWarning($description) { - $this->app->logWarning($description); + $logger = Logger::getInstance(); + $logger->addWarning($description); } @@ -147,7 +149,8 @@ public function logWarning($description) { */ public function logError($description) { - $this->app->logError($description); + $logger = Logger::getInstance(); + $logger->addError($description); } diff --git a/src/Module.php b/src/Module.php index e4f217d..5b14fdf 100755 --- a/src/Module.php +++ b/src/Module.php @@ -11,22 +11,22 @@ class Module extends ActiveRecord implements PluginInterface { /** - * Property that binds db field id. + * ID as primary key. * @var int */ protected $id; /** - * Property that binds db field name. + * Unique name with no space. * @var string */ protected $name; /** - * Property that binds db field version. + * Release version. * @var string */ protected $version; /** - * Property that binds db field date_released. + * Publication date, properly converted when inserted into db. * @var DateTime */ protected $dateReleased; @@ -37,12 +37,12 @@ class Module extends ActiveRecord implements PluginInterface { protected $appVersion; /** - * Property that binds db field installed_by. + * User ID of installer. * @var int */ protected $installedBy; /** - * Property that binds db field date_installed. + * Installation date, properly converted when inserted into db. * @var DateTime */ protected $dateInstalled; @@ -95,6 +95,7 @@ protected static function getBinds() { */ protected function beforeDelete() { + // delete plugin folder $plugin = $this->getPlugin(); $res = Utilities::deleteFolder($plugin->baseFolder); @@ -124,7 +125,9 @@ protected function beforeDelete() { /** * Returns absolute path to plugin folder. * - * @return string + * @return string + * + * @see PluginInterface::getBaseFolder() */ public function getBaseFolder() { @@ -136,32 +139,49 @@ public function getBaseFolder() { * Checks if Module is already installed in this application. * * @param string Name of Module to search. + * * @return boolean + * + * @see PluginInterface::pluginExists() */ public static function pluginExists($name) { $db = Database::getInstance(); - $db->setQuery('SELECT COUNT(*) FROM modules WHERE name = ?'); - $res = $db->loadCount($name); - - return $res ? TRUE : FALSE; + $db->setQuery('SELECT COUNT(1) FROM modules WHERE name = ?'); + return (bool)$db->loadCount($name); } /** * Creates and returns the Plugin object of this Module object. * - * @return Plugin + * @return Plugin + * + * @see PluginInterface::getPlugin() */ public function getPlugin() { - $folder = APPLICATION_PATH . '/modules/' . strtolower(str_replace(array(' ', '_'), '', $this->name)); - $dateReleased = $this->dateReleased->format('Y-m-d'); + $folder = $this->getBaseFolder() . '/' . strtolower(str_replace(array(' ', '_'), '', $this->name)); + $dateReleased = $this->dateReleased->format('Y-m-d'); - $plugin = new Plugin('module', $this->name, $this->version, $dateReleased, $this->appVersion, $folder); + $plugin = new Plugin('Module', $this->name, $this->version, $dateReleased, $this->appVersion, $folder); return $plugin; } + /** + * Get option parameters and store this object loaded by a Plugin. + * + * @param SimpleXMLElement List of options. + * + * @return bool + * + * @see PluginInterface::storeByPlugin() + */ + public function storeByPlugin(\SimpleXMLElement $options) { + + return $this->store(); + + } } \ No newline at end of file diff --git a/src/Plugin.php b/src/Plugin.php index dad9448..56568c2 100755 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -143,14 +143,18 @@ public function installPackage($package) { // TODO managing all ZIP errors (ZipArchive::ER_EXISTS, ZipArchive::ER_INCONS, etc.) + static::checkTemporaryFolder(); + if (TRUE !== $zipOpened) { - trigger_error('ERROR_EXTRACTING_ZIP_CONTENT'); return FALSE; - } - $zip->extractTo(self::TEMP_FOLDER); + // make a random temporary folder + $tempFolder = static::TEMP_FOLDER . '/' . substr(md5(time()),0,6); + + // extract all zip contents + $zip->extractTo($tempFolder); // checks if all contents are in a subfolder $stat = $zip->statIndex(0); @@ -158,79 +162,57 @@ public function installPackage($package) { // locates manifest file $manifestIndex = $zip->locateName('manifest.xml', \ZipArchive::FL_NOCASE|\ZipArchive::FL_NODIR); - - // gets installation info by manifest file - $manifest = simplexml_load_string($zip->getFromIndex($manifestIndex)); - - // plugin tree - $mPlugin = $manifest->plugin; - - $mAttributes = $mPlugin->attributes(); - - // TODO management of unvalid packages - - // gets class name for this plugin (Module, Template, other) - $pluginClass = ucfirst($mAttributes->type); - - if (in_array($pluginClass, array('Module', 'Template'))) { - $pluginClass = 'Pair\\' . $pluginClass; + + try { + // get XML content of manifest from ZIP + $manifest = simplexml_load_string($zip->getFromIndex($manifestIndex)); + } catch (Exception $e) { + $app->enqueueError('Manifest content is not valid: ' . $e->getMessage()); + return FALSE; } - // checks if plugin is already installed - if ($pluginClass::pluginExists($mPlugin->name)) { - - // TODO manage plugin update in case of new version - - $app->enqueueError('PLUGIN_IS_ALREADY_INSTALLED'); - return; - + // check if manifest is valid + if (!is_a($manifest, '\SimpleXMLElement')) { + $app->enqueueError('Manifest file is not valid'); + return FALSE; } - - // creates plugin object - $plugin = new $pluginClass(); - $plugin->name = (string)$mPlugin->name; - $plugin->version = (string)$mPlugin->version; - $plugin->dateReleased = date('Y-m-d H:i:s', strtotime((string)$mPlugin->dateReleased)); - $plugin->appVersion = (string)$mPlugin->appVersion; - $plugin->installedBy = $app->currentUser->id; - $plugin->dateInstalled = time(); - $this->baseFolder = $plugin->getBaseFolder(); + // get installation info by manifest file + $plugin = static::createPluginByManifest($manifest); - // calls specific plugin subclass function - $functionName = 'install' . ucfirst($mAttributes->type) . 'Package'; - if (method_exists(get_called_class(), $functionName)) { - $this->$functionName($plugin, $manifest); + // error check + if (is_null($plugin)) { + $zip->close(); + Utilities::deleteFolder($package); + return FALSE; } - // saves plugin object to db - if (!$plugin->create()) { - $ret = FALSE; - } + // set the plugin-type common folder + $this->baseFolder = $plugin->getBaseFolder(); // the temporary directory where extracted files reside - $sourceFolder = APPLICATION_PATH . '/' . self::TEMP_FOLDER . '/' . $mPlugin->folder; + $sourceFolder = APPLICATION_PATH . '/' . $tempFolder; // this is destination path for copy plugin files - $pluginFolder = $this->baseFolder . '/' . $mPlugin->folder; - + $pluginFolder = $this->baseFolder . '/' . strtolower($manifest->plugin->folder); + // creates final plugin folder $old = umask(0); if (!mkdir($pluginFolder, 0777, TRUE)) { - $ret = FALSE; trigger_error('Directory creation on ' . $pluginFolder . ' failed'); + $ret = FALSE; } umask($old); // sets full permissions on final folder if (!chmod($pluginFolder, 0777)) { - $ret = FALSE; trigger_error('Set permissions on directory ' . $pluginFolder . ' failed'); + $ret = FALSE; } // TODO must notify all lost files when copying - $files = $mPlugin->files->children(); + $files = $manifest->plugin->files->children(); // copy all plugin files foreach ($files as $file) { @@ -266,6 +248,79 @@ public function installPackage($package) { } + /** + * + * @param unknown $file + * @return string + */ + public static function getManifestByFile($file) { + + $app = Application::getInstance(); + + if (!file_exists($file)) { + $app->enqueueError('Manifest file ' . $file . ' doens’t exist'); + return NULL; + } + + $contents = file_get_contents($file); + + try { + $xml = simplexml_load_string($contents); + } catch (Exception $e) { + $app->enqueueError('Manifest content is not valid: ' . $e->getMessage()); + return NULL; + } + + return $xml; + + } + + /** + * Insert a new db record for the plugin in manifest. Return an object of the plugin type. + * + * @param SimpleXMLElement Manifest file XML content. + * + * @return mixed + */ + public static function createPluginByManifest(\SimpleXMLElement $manifest) { + + $app = Application::getInstance(); + + // plugin tree + $mPlugin = $manifest->plugin; + + $mAttributes = $mPlugin->attributes(); + + // get class name for this plugin (Module, Template, other) + $class = ucfirst($mAttributes->type); + + // add namespace for Pair classes + if (in_array($class, array('Module', 'Template'))) { + $class = 'Pair\\' . $class; + } + + // check if plugin is already installed + if ($class::pluginExists($mPlugin->name)) { + $app->enqueueError('PLUGIN_IS_ALREADY_INSTALLED'); + return NULL; + } + + // creates plugin object + $plugin = new $class(); + $plugin->name = (string)$mPlugin->name; + $plugin->version = (string)$mPlugin->version; + $plugin->dateReleased = date('Y-m-d H:i:s', strtotime((string)$mPlugin->dateReleased)); + $plugin->appVersion = (string)$mPlugin->appVersion; + $plugin->installedBy = $app->currentUser->id; + $plugin->dateInstalled = time(); + + // saves plugin object to db + $plugin->storeByPlugin($mPlugin->options); + + return $plugin; + + } + /** * Updates the manifest file, zips plugin’s folder and lets user download it. */ @@ -280,7 +335,7 @@ public function downloadPackage() { $this->createManifestFile(); // removes old archives - self::removeOldFiles(); + static::removeOldFiles(); // useful paths $pathInfos = pathInfo($this->baseFolder); @@ -360,6 +415,34 @@ private static function folderToZip($folder, &$zipFile, $parentPath) { } + /** + * Check temporary folder or create it. + */ + public static function checkTemporaryFolder() { + + if (!file_exists(static::TEMP_FOLDER) or !is_dir(static::TEMP_FOLDER)) { + + // remove any file named as wanted temporary folder + @unlink(static::TEMP_FOLDER); + + // create the folder + $old = umask(0); + if (!mkdir(static::TEMP_FOLDER, 0777, TRUE)) { + trigger_error('Directory creation on ' . static::TEMP_FOLDER . ' failed'); + $ret = FALSE; + } + umask($old); + + // sets full permissions + if (!chmod($pluginFolder, 0777)) { + trigger_error('Set permissions on directory ' . $pluginFolder . ' failed'); + $ret = FALSE; + } + + } + + } + /** * Deletes file with created date older than EXPIRE_TIME const. */ @@ -368,25 +451,25 @@ public static function removeOldFiles() { $app = Application::getInstance(); $counter = 0; - $files = Utilities::getDirectoryFilenames(self::TEMP_FOLDER); - + $files = Utilities::getDirectoryFilenames(static::TEMP_FOLDER); + foreach ($files as $file) { - - $pathFile = self::TEMP_FOLDER . '/' . $file; + + $pathFile = APPLICATION_PATH . '/' .static::TEMP_FOLDER . '/' . $file; $fileLife = time() - filemtime($pathFile); - $maxLife = self::FILE_EXPIRE * 60; + $maxLife = static::FILE_EXPIRE * 60; if ($fileLife > $maxLife) { $counter++; unlink($pathFile); } - + } if ($counter) { - $app->logEvent($counter . ' files has been deleted from ' . self::TEMP_FOLDER); + $app->logEvent($counter . ' files has been deleted from ' . static::TEMP_FOLDER); } else { - $app->logEvent('No old files deleted from ' . self::TEMP_FOLDER); + $app->logEvent('No old files deleted from ' . static::TEMP_FOLDER); } } @@ -397,9 +480,28 @@ public static function removeOldFiles() { * @return bool */ public function createManifestFile() { - + $app = Application::getInstance(); + // lambda method to add an array to the XML + $addChildArray = function ($element, $list) use ($app, &$addChildArray) { + + foreach ($list as $name => $value) { + + // if array, run again recursive + if (is_array($value)) { + $listChild = $element->addChild($name); + $addChildArray($listChild, $value); + } else if (is_int($value) or is_string($value)) { + $element->addChild($name, (string)$value); + } else { + $app->logError('Option item ' . $name . ' is not valid'); + } + + } + + }; + // prevent missing folders if (!is_dir($this->baseFolder)) { $app->logError('Folder ' . $this->baseFolder . ' of ' . $this->name . ' ' . $this->type . ' plugin cannot be accessed'); @@ -423,11 +525,8 @@ public function createManifestFile() { // custom options $optionsChild = $plugin->addChild('options'); - // calls specific plugin subclass function - $functionName = 'create' . ucfirst($this->type) . 'Manifest'; - if (method_exists(get_called_class(), $functionName)) { - $this->$functionName($manifest); - } + // recursively add options elements + $addChildArray($optionsChild, $this->options); // will contains all found files $filesChild = $plugin->addChild('files'); @@ -442,113 +541,16 @@ public function createManifestFile() { $filename = $this->baseFolder . '/manifest.xml'; - // TODO manage unwritable folder - - $res = $manifest->asXML($filename); - - $app->logEvent('Created manifest file ' . $filename . ' for ' . $this->type . ' plugin'); - - return $res; - - } - - /** - * Insert a new db record for the plugin in manifest. - * - * @param string Manifest file XML content. - */ - public static function createPluginByManifest($manifestContent) { - - $app = Application::getInstance(); - - // gets installation info by manifest file - $manifest = simplexml_load_string($manifestContent); - - // plugin tree - $mPlugin = $manifest->plugin; - - $mAttributes = $mPlugin->attributes(); - - // gets class name for this plugin (Module, Template or other) - $pluginClass = ucfirst($mAttributes->type); - - // checks if class name is in Pair framework namespace - $class = class_exists('Pair\\' . $pluginClass) ? 'Pair\\' . $pluginClass : '\\' . $pluginClass; - - // creates plugin object - $plugin = new $class(); - $plugin->name = (string)$mPlugin->name; - $plugin->version = (string)$mPlugin->version; - $plugin->dateReleased = date('Y-m-d H:i:s', strtotime((string)$mPlugin->dateReleased)); - $plugin->installedBy = $app->currentUser->id; - $plugin->dateInstalled = date('Y-m-d H:i:s'); - $plugin->appVersion = (string)$mPlugin->appVersion; - - // calls specific plugin subclass function - $functionName = 'install' . ucfirst($mAttributes->type) . 'Package'; - if (method_exists(get_called_class(), $functionName)) { - self::$functionName($plugin, $manifest); + try { + $res = $manifest->asXML($filename); + $app->logEvent('Created manifest file ' . $filename . ' for ' . $this->type . ' plugin'); + } catch (\Exception $e) { + $res = FALSE; + $app->logError('Manifest file ' . $filename . ' creation failed for ' . $this->type . ' plugin: ' . $e->getMessage()); } - // saves plugin object to db - $plugin->create(); - - } - - /** - * This method is automatically called by installPackage() when a template type - * being installs. - * - * @param Template Plugin subclass object. - * @param SimpleXMLElement Manifest file. - */ - protected static function installTemplatePackage($plugin, $manifest) { - - // get options - $options = $manifest->plugin->options->children(); - - $plugin->derived = (bool)$options->derived; - - // temp variable - $palette = array(); - - // the needed cast to string for each property - foreach ($options->palette->children() as $color) { - $palette[] = (string)$color; - } - - // assigns to Template palette property - $plugin->palette = $palette; - - } - - /** - * This method is automatically called by createManifestFile() when type equals template. - * - * @param SimpleXMLElement Manifest file. - */ - protected function createTemplateManifest($manifest) { - - foreach ($this->options as $optionName=>$optionValue) { - - switch ($optionName) { - - case 'derived': - $derivedChild = $manifest->plugin->options->addChild('derived', (string)intval($optionValue)); - break; - - // if the options has name palette, will split for each color - case 'palette': - $paletteChild = $manifest->plugin->options->addChild('palette'); - foreach ($optionValue as $color) { - $paletteChild->addChild('color', $color); - } - break; - - } - - } + return $res; } -} +} \ No newline at end of file diff --git a/src/PluginInterface.php b/src/PluginInterface.php index 511d5dc..4aec459 100755 --- a/src/PluginInterface.php +++ b/src/PluginInterface.php @@ -36,4 +36,13 @@ public static function pluginExists($name); */ public function getPlugin(); + /** + * Store an object loaded by a Plugin. + * + * @param SimpleXMLElement List of options. + * + * @return bool + */ + public function storeByPlugin(\SimpleXMLElement $options); + } diff --git a/src/Rule.php b/src/Rule.php index cbf9b35..e6ba328 100755 --- a/src/Rule.php +++ b/src/Rule.php @@ -118,10 +118,10 @@ public static function getRuleModuleName($module_id, $action, $adminOnly) { $db = Database::getInstance(); $query = - ' SELECT m.name as moduleName, r.action as ruleAction, r.admin_only '. - ' FROM rules as r '. - ' INNER JOIN modules as m ON m.id = r.module_id '. - ' WHERE m.id = ? and r.action = ? and r.admin_only = ?'; + ' SELECT m.name AS moduleName,r.action AS ruleAction, r.admin_only '. + ' FROM rules AS r '. + ' INNER JOIN modules AS m ON m.id = r.module_id '. + ' WHERE m.id = ? AND r.action = ? AND r.admin_only = ?'; $db->setQuery($query); $module = $db->loadObject(array($module_id, $action, $adminOnly)); diff --git a/src/Template.php b/src/Template.php index ab978d4..9503ae0 100755 --- a/src/Template.php +++ b/src/Template.php @@ -69,6 +69,12 @@ class Template extends ActiveRecord implements PluginInterface { * @var array */ protected $palette; + + /** + * Template from which it derives. It’s NULL if standard Template. + * @var Template|NULL + */ + protected $base; /** * Name of related db table. @@ -83,7 +89,48 @@ class Template extends ActiveRecord implements PluginInterface { const TABLE_KEY = 'id'; /** - * Converts from string to Datetime object in two ways. + * Legacy method to get working the old templates. + * + * @param string Requested property’s name. + * + * @return multitype + * + * @deprecated + */ + public function __get($name) { + + $app = Application::getInstance(); + + // patch for Widget variables + if ('Widget' == substr($name, -6)) { + return $app->$name; + } + + switch ($name) { + + case 'templatePath': + return $this->getPath(); + break; + + case 'langCode': + case 'log': + case 'pageTitle': + case 'pageStyles': + case 'pageContent': + case 'pageScripts': + return $app->$name; + break; + + default: + return $this->$name; + break; + + } + + } + + /** + * Method called by constructor just after having populated the object. */ protected function init() { @@ -94,13 +141,13 @@ protected function init() { $this->bindAsDatetime('dateReleased', 'dateInstalled'); $this->bindAsInteger('id', 'installedBy'); - + } /** * Returns array with matching object property name on related db fields. * - * @return array + * @return array */ protected static function getBinds() { @@ -121,11 +168,11 @@ protected static function getBinds() { } /** - * Changes instances that use this template to default template. + * Removes files of this Module object before its deletion. */ protected function beforeDelete() { - // deletes template plugin folder + // delete plugin folder $plugin = $this->getPlugin(); $res = Utilities::deleteFolder($plugin->baseFolder); @@ -149,7 +196,9 @@ protected function beforeDelete() { /** * Returns absolute path to plugin folder. * - * @return string + * @return string + * + * @see PluginInterface::getBaseFolder() */ public function getBaseFolder() { @@ -163,14 +212,14 @@ public function getBaseFolder() { * @param string Name of Template to search. * * @return boolean + * + * @see PluginInterface::pluginExists() */ public static function pluginExists($name) { $db = Database::getInstance(); - $db->setQuery('SELECT COUNT(*) FROM templates WHERE name = ?'); - $res = $db->loadResult($name); - - return $res ? TRUE : FALSE; + $db->setQuery('SELECT COUNT(1) FROM templates WHERE name = ?'); + return (bool)$db->loadCount($name); } @@ -178,19 +227,51 @@ public static function pluginExists($name) { * Creates and returns the Plugin object of this Template object. * * @return Plugin + * + * @see PluginInterface::getPlugin() */ public function getPlugin() { - $folder = APPLICATION_PATH . '/templates/' . strtolower(str_replace(array(' ', '_'), '', $this->name)); - $dateReleased = $this->dateReleased->format('Y-m-d'); - $options = array('derived' => $this->derived, 'palette' => $this->palette); - - $plugin = new Plugin('template', $this->name, $this->version, $dateReleased, $this->appVersion, $folder, $options); + $folder = $this->getBaseFolder() . '/' . strtolower(str_replace(array(' ', '_'), '', $this->name)); + $dateReleased = $this->dateReleased->format('Y-m-d'); + + // special parameters for Template plugin + $options = [ + 'derived' => (string)\intval($this->derived), + 'palette' => implode(',', $this->palette) + ]; + + $plugin = new Plugin('Template', $this->name, $this->version, $dateReleased, $this->appVersion, $folder, $options); return $plugin; } + /** + * Get option parameters and store this object loaded by a Plugin. + * + * @param SimpleXMLElement List of options. + * + * @return bool + * + * @see PluginInterface::storeByPlugin() + */ + public function storeByPlugin(\SimpleXMLElement $options) { + + // get options + $children = $options->children(); + + $this->derived = (bool)$children->derived; + + // the needed cast to string for each property + foreach ($children->palette->children() as $color) { + $this->palette[] = (string)$color; + } + + return $this->store(); + + } + /** * Returns the default Template object. * @@ -200,8 +281,7 @@ public static function getDefault() { $db = Database::getInstance(); $db->setQuery('SELECT * FROM templates WHERE is_default=1'); - $template = new Template($db->loadObject()); - return $template; + return new Template($db->loadObject()); } @@ -212,7 +292,7 @@ public static function getDefault() { * * @return Template|NULL */ - public static function getTemplateByName($name) { + public static function getPluginByName($name) { $db = Database::getInstance(); $db->setQuery('SELECT * FROM templates WHERE name=?'); @@ -222,42 +302,44 @@ public static function getTemplateByName($name) { } /** - * Returns list of all registered templates. + * Set a standard Template object as the base for a derived Template. * - * @return array:Template + * @param string Template name. */ - public static function getAllTemplates() { - - $db = Database::getInstance(); - $db->setQuery('SELECT * FROM templates'); - $list = $db->loadObjectList(); + public function setBase($templateName) { - $templates = array(); - - foreach ($list as $item) { - $calledClass = get_called_class(); - $templates[] = new $calledClass($item); - } - - return $templates; + $this->base = static::getPluginByName($templateName); } - /** - * Creates a list of colors boxes for this template. - * - * @return string - */ - public function getPaletteSamples() { + public function loadStyle($styleName) { - $ret = ''; + // by default load template style + $styleFile = $this->getBaseFolder() . '/' . strtolower($this->name) . '/' . $styleName . '.php'; + + // if this is derived template, try to load the file from its folder + if (!file_exists($styleFile) and $this->derived and is_a($this->base, 'Pair\Template')) { + $styleFile = $this->getBaseFolder() . '/' . strtolower($this->base->name) . '/' . $styleName . '.php'; + } - foreach ($this->palette as $color) { - $ret .= '
'; + if (!file_exists($styleFile)) { + + throw new \Exception('Template style file ' . $styleFile . ' was not found'); + + } else { + + // load the style page file + require $styleFile; + } - return $ret; - } + public function getPath() { + + $templateName = $this->derived ? $this->base->name : $this->name; + return 'templates/' . strtolower($templateName) . '/'; + + } + } \ No newline at end of file diff --git a/src/Translator.php b/src/Translator.php index 92e5cb8..a317d98 100755 --- a/src/Translator.php +++ b/src/Translator.php @@ -14,128 +14,141 @@ class Translator { * Singleton object. * @var object|NULL */ - protected static $instance = NULL; + protected static $instance; /** - * The default language 2 alpha code. - * @var string + * The default Language object. + * @var Language */ - private $default = 'en'; + private $default; /** - * The current language 2 alpha code. - * @var string + * The current Language object. + * @var Language */ - private $current = NULL; + private $current; /** - * Current module in where look for language files. + * Current module in where to look for language files. * @var string */ - private $module = NULL; + private $module; /** * Translation strings, as loaded from ini language file. * @var NULL|array */ - private $strings = NULL; + private $strings; /** * Default language strings, loaded if needed and stored for next use. * @var NULL|array */ - private $defaultStrings = NULL; + private $defaultStrings; /** - * Sets current language reading the favorite browser language variable. + * Set current language reading the favorite browser language variable. */ private function __construct() { // config module for language - $route = Router::getInstance(); - $this->module = $route->module; - $this->default = Language::getDefault()->code; + $this->default = Language::getDefault(); } - + /** - * Will returns property’s value if set. Throw an exception and returns NULL if not set. + * Return the singleton object. * - * @param string Property’s name. - * @throws Exception - * @return mixed|NULL + * @return object */ - public function __get($name) { - - try { - - if (!isset($this->$name)) { - throw new \Exception('Property “'. $name .'” doesn’t exist for object '. get_class()); - } - - return $this->$name; - - } catch (\Exception $e) { - - return NULL; + public static function getInstance() { + if (is_null(static::$instance)) { + static::$instance = new static(); } - } + return static::$instance; + } + /** - * Magic method to set an object property value. - * - * @param string Property’s name. - * @param mixed Property’s value. + * Return the current Language object. + * + * @return Language */ - public function __set($name, $value) { - - try { - $this->$name = $value; - } catch (\Exception $e) { - print $e->getMessage(); - } - + public function getCurrentLanguage() { + + $this->checkLanguageSet(); + + return $this->current; + } /** - * Returns singleton object. + * Return the default Language object, cached. * - * @return object + * @return Language */ - public static function getInstance() { - - if (is_null(self::$instance)) { - self::$instance = new self(); - } - - return self::$instance; - + public function getDefaultLanguage() { + + $this->checkLanguageSet(); + + return $this->default; + } /** - * Set a new current language. + * Set a new current language by preparing language strings and locale. * * @param Language Language object to set. */ - public function setLanguage(Language $lang) { + public function setLanguage(Language $newLang) { - $this->current = $lang->code; + // apply some changes only if new Language really differs + if (!$this->current or ($this->current and $newLang->code != $this->current->code)) { + + $this->current = $newLang; + + // if new language code equals the default one, move lang-strings + if ($this->default and $newLang->code == $this->default->code) { - setlocale(LC_ALL, $lang->representation); + $this->strings = $this->defaultStrings; + $this->defaultStrings = NULL; + + // otherwise reload current strings + } else { + + $this->strings = NULL; + $this->loadStrings(); + + } + + } + + setlocale(LC_ALL, $newLang->representation); + + } + + /** + * Set a module name. + * + * @param string Module name. + */ + public function setModule($moduleName) { + + $this->module = $moduleName; } /** - * Checks that both default and current languages are set. + * Check that both default and current languages are set. */ private function checkLanguageSet() { if (!$this->default) { $lang = Language::getDefault(); - $this->default = $lang->code; + $this->default = $lang; // server variable setlocale(LC_ALL, $lang->representation); @@ -155,17 +168,13 @@ private function checkLanguageSet() { $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches, PREG_SET_ORDER); // if browser’s lang matches and it’s different by current, will set as current - if (array_key_exists(0, $matches) and array_key_exists(1, $matches[0]) and $this->current!=$matches[0][1]) { + if (isset($matches[0][1]) and $this->current->code != $matches[0][1]) { $lang = Language::getLanguageByCode($matches[0][1]); if ($lang) { - - // overwrites current - $this->current = $lang->code; - - // sets server variable - setlocale(LC_ALL, $lang->representation); + + $this->setLanguage($lang); } @@ -178,11 +187,12 @@ private function checkLanguageSet() { } /** - * Returns the translated string from expected lang file, if there, else - * from default, else returns the key string. + * Return the translated string from expected lang file, if there, else + * from default, else return the key string. * * @param string The language key. * @param array|NULL List of parameters to bind on string (optional). + * * @return string */ public function translate($key, $vars=NULL) { @@ -190,7 +200,7 @@ public function translate($key, $vars=NULL) { $app = Application::getInstance(); // load translation strings - $this->loadStrings(); + $this->loadStrings(); // searches into strings if (array_key_exists($key, $this->strings) and $this->strings[$key]) { @@ -200,7 +210,7 @@ public function translate($key, $vars=NULL) { // searches into strings of default language } else if (is_array($this->defaultStrings) and array_key_exists($key, $this->defaultStrings) and $this->defaultStrings[$key]) { - $app->logWarning('Language string ' . $key . ' is untranslated for current language [' . $this->current . ']'); + $app->logWarning('Language string ' . $key . ' is untranslated for current language [' . $this->current->code . ']'); $string = $this->defaultStrings[$key]; // will returns the string constant, as debug info @@ -228,7 +238,7 @@ public function translate($key, $vars=NULL) { } /** - * Returns TRUE if passed language is available for translation. + * Return TRUE if passed language is available for translation. * * @param string Language key. * @@ -248,7 +258,7 @@ public function stringExists($key) { } /** - * Loads translation strings from current and default (if different) language ini file. + * Load translation strings from current and default (if different) language ini file. */ private function loadStrings() { @@ -260,11 +270,18 @@ private function loadStrings() { // avoid failures $this->strings = array(); + // useful for landing page + if (!$this->module) { + $app = Application::getInstance(); + $route = Router::getInstance(); + $this->module = $route->module ? $route->module : $app->currentUser->getLanding()->module; + } + // checks that languages are set $this->checkLanguageSet(); // common strings in current language - $common = 'languages/' . $this->current . '.ini'; + $common = 'languages/' . $this->current->code . '.ini'; if (file_exists($common)) { try { $this->strings = @parse_ini_file($common); @@ -278,9 +295,9 @@ private function loadStrings() { // if module is not set, won’t find language file if ($this->module) { - + // module strings in current language - $file1 = 'modules/' . strtolower($this->module) . '/languages/' . $this->current . '.ini'; + $file1 = 'modules/' . strtolower($this->module) . '/languages/' . $this->current->code . '.ini'; if (file_exists($file1)) { try { $moduleStrings = @parse_ini_file($file1); @@ -295,11 +312,11 @@ private function loadStrings() { } - // if current language is different by default, will load also... - if ($this->current!=$this->default) { + // if current language is different by default, will load also + if ($this->current->code != $this->default->code) { // common strings in default language - $common = 'languages/' . $this->default . '.ini'; + $common = 'languages/' . $this->default->code . '.ini'; if (file_exists($common)) { try { $this->defaultStrings = @parse_ini_file($common); @@ -312,7 +329,7 @@ private function loadStrings() { if ($this->module) { // module strings in default language - $file2 = 'modules/' . strtolower($this->module) . '/languages/' . $this->default. '.ini'; + $file2 = 'modules/' . strtolower($this->module) . '/languages/' . $this->default->code . '.ini'; if (file_exists($file2)) { try { $moduleStrings = @parse_ini_file($file2); @@ -329,7 +346,7 @@ private function loadStrings() { } /** - * Translates the text in an array of select-options strings if uppercase. + * Translate the text in an array of select-options strings if uppercase. * * @param array List of (value=>text)s to translate. * @return array diff --git a/src/View.php b/src/View.php index ced81f2..8fb09ad 100755 --- a/src/View.php +++ b/src/View.php @@ -107,9 +107,6 @@ final public function __construct() { $this->model->pagination = $this->pagination; - // sets language subfolder’s name - $this->translator->module = $this->name; - // sets the default menu item -- can be overwritten if needed $this->app->activeMenuItem = $route->module; @@ -270,7 +267,8 @@ public function enqueueError($text, $title='') { */ public function logEvent($description, $type='notice', $subtext=NULL) { - $this->app->logEvent($description, $type, $subtext); + $logger = Logger::getInstance(); + $logger->addEvent($description, $type, $subtext); } @@ -281,7 +279,8 @@ public function logEvent($description, $type='notice', $subtext=NULL) { */ public function logWarning($description) { - $this->app->logWarning($description); + $logger = Logger::getInstance(); + $logger->addWarning($description); } @@ -292,7 +291,8 @@ public function logWarning($description) { */ public function logError($description) { - $this->app->logError($description); + $logger = Logger::getInstance(); + $logger->addError($description); }