diff --git a/core/bootstrap/autoload.php b/core/bootstrap/autoload.php index 6c1b8ce1ee5..16d70b5770d 100644 --- a/core/bootstrap/autoload.php +++ b/core/bootstrap/autoload.php @@ -56,4 +56,4 @@ | */ -require dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'hubzero' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Base' . DIRECTORY_SEPARATOR . 'helpers.php'; +require dirname(__DIR__) . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'Hubzero' . DIRECTORY_SEPARATOR . 'Base' . DIRECTORY_SEPARATOR . 'helpers.php'; diff --git a/core/composer.json b/core/composer.json old mode 100644 new mode 100755 index dea78a988c9..55cef7798b0 --- a/core/composer.json +++ b/core/composer.json @@ -45,7 +45,6 @@ "pear/net_dns2": "1.4.*", "hubzero/orcid-php": "0.*", "league/flysystem": "1.0.*", - "hubzero/framework": "dev-master", "hubzero/flysystem-github": "dev-develop", "nao-pon/flysystem-google-drive": "~1.1", "php-amqplib/php-amqplib": "2.5.*", @@ -54,12 +53,21 @@ "solarium/solarium" : "3.8.1", "cilogon/oauth2-cilogon": "^1.1", "srmklive/flysystem-dropbox-v2": "^1.0", - "stevenmaguire/oauth2-dropbox": "^2.0.0" + "stevenmaguire/oauth2-dropbox": "^2.0.0", + "composer/composer": "1.6.3", + "phpoffice/phpspreadsheet": "1.4.1" }, "require-dev": { - "mockery/mockery": "dev-master" + "mockery/mockery": "dev-master", + "hubzero/standards": "dev-master", + "jakub-onderka/php-parallel-lint": "0.9.*", + "phpunit/dbunit": "1.4.*", + "phpunit/phpunit": "4.*", + "satooshi/php-coveralls": "1.0.*", + "squizlabs/php_codesniffer": "2.*" }, "autoload": { + "psr-0": { "Hubzero": "libraries/" }, "psr-4": { "Bootstrap\\": "bootstrap/"}, "classmap": [ "libraries/joomla" ] }, diff --git a/core/composer.lock b/core/composer.lock old mode 100644 new mode 100755 index a5c131b0d58..347bab6e98e --- a/core/composer.lock +++ b/core/composer.lock @@ -1107,7 +1107,9 @@ }, "require-dev": { "php": ">=5.5", + "hubzero/standards": "dev-master", "phpunit/phpunit": "^4.7.7", + "phpunit/dbunit": "~1.4", "satooshi/php-coveralls": "^0.6.1", "scrutinizer/ocular": "^1.1", "whatthejeff/nyancat-phpunit-resultprinter": "^1.2" @@ -1159,54 +1161,6 @@ }, "time": "2017-07-24T19:58:42+00:00" }, - { - "name": "hubzero/framework", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/hubzero/framework.git", - "reference": "c31d1e6e44b659b5deab81a268c19cff21262019" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hubzero/framework/zipball/c31d1e6e44b659b5deab81a268c19cff21262019", - "reference": "c31d1e6e44b659b5deab81a268c19cff21262019", - "shasum": "" - }, - "require": { - "composer/composer": "1.6.3", - "php": ">=5.4.0", - "phpoffice/phpspreadsheet": "1.4.1", - "symfony/http-foundation": "2.5.5", - "symfony/yaml": "2.5.*" - }, - "require-dev": { - "hubzero/standards": "dev-master", - "jakub-onderka/php-parallel-lint": "0.9.*", - "mockery/mockery": "0.9.*", - "phpunit/dbunit": "1.4.*", - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "1.0.*", - "squizlabs/php_codesniffer": "2.*" - }, - "type": "library", - "autoload": { - "psr-4": { - "Hubzero\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The HUBzero PHP framework", - "keywords": [ - "framework", - "hubzero", - "php" - ], - "time": "2019-10-22T17:48:23+00:00" - }, { "name": "hubzero/orcid-php", "version": "0.3.1", @@ -4039,7 +3993,6 @@ "minimum-stability": "stable", "stability-flags": { "pelago/emogrifier": 20, - "hubzero/framework": 20, "hubzero/flysystem-github": 20, "mockery/mockery": 20 }, diff --git a/core/libraries/Hubzero/Access/Access.php b/core/libraries/Hubzero/Access/Access.php new file mode 100644 index 00000000000..7d3bcede911 --- /dev/null +++ b/core/libraries/Hubzero/Access/Access.php @@ -0,0 +1,487 @@ +allow($action, $identities); + } + + /** + * Method to check if a group is authorised to perform an action, optionally on an asset. + * + * @param integer $groupId The path to the group for which to check authorisation. + * @param string $action The name of the action to authorise. + * @param mixed $asset Integer asset id or the name of the asset as a string. Defaults to the global asset node. + * @return boolean True if authorised. + */ + public static function checkGroup($groupId, $action, $asset = null) + { + // Sanitize inputs. + $groupId = (int) $groupId; + $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); + $asset = strtolower(preg_replace('#[\s\-]+#', '.', trim($asset))); + + // Get group path for group + $groupPath = self::getGroupPath($groupId); + + // Default to the root asset node. + if (empty($asset)) + { + $rootId = Asset::getRootId(); + } + + // Get the rules for the asset recursively to root if not already retrieved. + if (empty(self::$assetRules[$asset])) + { + self::$assetRules[$asset] = self::getAssetRules($asset, true); + } + + return self::$assetRules[$asset]->allow($action, $groupPath); + } + + /** + * Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree + * (including the leaf group id). + * + * @param mixed $groupId An integer or array of integers representing the identities to check. + * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. + */ + protected static function getGroupPath($groupId) + { + // Preload all groups + if (empty(self::$userGroups)) + { + self::$userGroups = array(); + + $groups = Group::all() + ->order('lft', 'asc') + ->rows(); + foreach ($groups as $group) + { + self::$userGroups[$group->get('id')] = $group; + } + } + + // Make sure groupId is valid + if (!array_key_exists($groupId, self::$userGroups)) + { + return array(); + } + + // Get parent groups and leaf group + if (!isset(self::$userGroupPaths[$groupId])) + { + self::$userGroupPaths[$groupId] = array(); + + foreach (self::$userGroups as $group) + { + if ($group->get('lft') <= self::$userGroups[$groupId]->get('lft') + && $group->get('rgt') >= self::$userGroups[$groupId]->get('rgt')) + { + self::$userGroupPaths[$groupId][] = $group->get('id'); + } + } + } + + return self::$userGroupPaths[$groupId]; + } + + /** + * Method to return the Rules object for an asset. The returned object can optionally hold + * only the rules explicitly set for the asset or the summation of all inherited rules from + * parent assets and explicit rules. + * + * @param mixed $asset Integer asset id or the name of the asset as a string. + * @param boolean $recursive True to return the rules object with inherited rules. + * @return object Rules object for the asset. + */ + public static function getAssetRules($asset, $recursive = false) + { + // Build the database query to get the rules for the asset. + $model = Asset::blank(); + + $db = App::get('db'); + $query = $db->getQuery() + ->select($recursive ? 'b.rules' : 'a.rules'); + + $query->from($model->getTableName(), 'a'); + + // If the asset identifier is numeric assume it is a primary key, else lookup by name. + if (is_numeric($asset)) + { + $query->whereEquals('a.id', (int) $asset); + } + else + { + $query->whereEquals('a.name', $asset); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) + { + $query->joinRaw($model->getTableName() . ' AS b', 'b.lft <= a.lft AND b.rgt >= a.rgt', 'left'); + $query->order('b.lft', 'asc'); + } + + $query->group($recursive ? 'b.id, b.rules, b.lft' : 'a.id, a.rules, a.lft'); + + $db->setQuery($query->toString()); + $result = $db->loadColumn(); + + //$result = $query->rows()->fieldsByKey('rules'); + + // Get the root even if the asset is not found and in recursive mode + if (empty($result) && $recursive) + { + $result = Asset::oneOrFail(Asset::getRootId()); + $result = array($result->get('rules')); + } + + // Instantiate and return the Rules object for the asset rules. + $rules = new Rules; + $rules->mergeCollection($result); + + return $rules; + } + + /** + * Method to return a list of user groups mapped to a user. The returned list can optionally hold + * only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited + * by the user. + * + * @param integer $userId Id of the user for which to get the list of groups. + * @param boolean $recursive True to include inherited user groups. + * @return array List of user group ids to which the user is mapped. + */ + public static function getGroupsByUser($userId, $recursive = true) + { + // Creates a simple unique string for each parameter combination: + $storeId = $userId . ':' . (int) $recursive; + + if (!isset(self::$groupsByUser[$storeId])) + { + // Guest user (if only the actually assigned group is requested) + if (empty($userId) && !$recursive) + { + $result = array(\Component::params('com_members')->get('guest_usergroup', 1)); + } + // Registered user and guest if all groups are requested + else + { + $db = App::get('db'); + + // Build the database query to get the rules for the asset. + $query = $db->getQuery(); + $query->select($recursive ? 'b.id' : 'a.id'); + if (empty($userId)) + { + $query->from('#__usergroups', 'a') + ->whereEquals('a.id', (int) \Component::params('com_members')->get('guest_usergroup', 1)); + } + else + { + $query->from('#__user_usergroup_map', 'map') + ->whereEquals('map.user_id', (int) $userId) + ->join('#__usergroups AS a', 'a.id', 'map.group_id', 'left'); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) + { + $query->joinRaw('#__usergroups AS b', 'b.lft <= a.lft AND b.rgt >= a.rgt', 'left'); + } + + // Execute the query and load the rules from the result. + $db->setQuery($query->toString()); + $result = $db->loadColumn(); + + // Clean up any NULL or duplicate values, just in case + Arr::toInteger($result); + + if (empty($result)) + { + $result = array('1'); + } + else + { + $result = array_unique($result); + } + } + + self::$groupsByUser[$storeId] = $result; + } + + return self::$groupsByUser[$storeId]; + } + + /** + * Method to return a list of user Ids contained in a Group + * + * @param integer $groupId The group Id + * @param boolean $recursive Recursively include all child groups (optional) + * @return array + * @todo This method should move somewhere else + */ + public static function getUsersByGroup($groupId, $recursive = false) + { + $test = $recursive ? '>=' : '='; + + // First find the users contained in the group + $db = App::get('db'); + $query = $db->getQuery() + ->select('DISTINCT(user_id)') + ->from('#__usergroups', 'ug1') + ->joinRaw('#__usergroups AS ug2', 'ug2.lft' . $test . 'ug1.lft AND ug1.rgt' . $test . 'ug2.rgt', 'inner') + ->join('#__user_usergroup_map AS m', 'm.group_id', 'ug2.id', 'inner') + ->whereEquals('ug1.id', $groupId); + + $db->setQuery($query->toString()); + + $result = $db->loadColumn(); + + // Clean up any NULL values, just in case + Arr::toInteger($result); + + return $result; + } + + /** + * Method to return a list of view levels for which the user is authorised. + * + * @param integer $userId Id of the user for which to get the list of authorised view levels. + * @return array List of view levels for which the user is authorised. + */ + public static function getAuthorisedViewLevels($userId) + { + // Get all groups that the user is mapped to recursively. + $groups = self::getGroupsByUser($userId); + + // Only load the view levels once. + if (empty(self::$viewLevels)) + { + // Build the view levels array. + $levels = Viewlevel::all() + ->rows(); + + foreach ($levels as $level) + { + self::$viewLevels[$level->get('id')] = (array) json_decode($level->get('rules')); + } + } + + // Initialise the authorised array. + $authorised = array(1); + + // Find the authorised levels. + foreach (self::$viewLevels as $level => $rule) + { + foreach ($rule as $id) + { + if (($id < 0) && (($id * -1) == $userId)) + { + $authorised[] = $level; + break; + } + // Check to see if the group is mapped to the level. + elseif (($id >= 0) && in_array($id, $groups)) + { + $authorised[] = $level; + break; + } + } + } + + return $authorised; + } + + /** + * Method to return a list of actions from a file for which permissions can be set. + * + * @param string $file The path to the XML file. + * @param string $xpath An optional xpath to search for the fields. + * @return boolean|array False if case of error or the list of actions available. + */ + public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/") + { + if (!is_file($file)) + { + // If unable to find the file return false. + return false; + } + + // Else return the actions from the xml. + return self::getActionsFromData(self::getXml($file, true), $xpath); + } + + /** + * Method to return a list of actions from a string or from an xml for which permissions can be set. + * + * @param string|SimpleXMLElement $data The XML string or an XML element. + * @param string $xpath An optional xpath to search for the fields. + * @return boolean|array False if case of error or the list of actions available. + */ + public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/") + { + // If the data to load isn't already an XML element or string return false. + if (!($data instanceof SimpleXMLElement) && !is_string($data)) + { + return false; + } + + // Attempt to load the XML if a string. + if (is_string($data)) + { + $data = self::getXml($data, false); + + // Make sure the XML loaded correctly. + if (!$data) + { + return false; + } + } + + // Initialise the actions array + $actions = array(); + + // Get the elements from the xpath + $elements = $data->xpath($xpath . 'action[@name][@title][@description]'); + + // If there some elements, analyse them + if (!empty($elements)) + { + foreach ($elements as $action) + { + // Add the action to the actions array + $actions[] = (object) array( + 'name' => (string) $action['name'], + 'title' => (string) $action['title'], + 'description' => (string) $action['description'] + ); + } + } + + // Finally return the actions array + return $actions; + } + + /** + * Reads an XML file or string. + * + * @param string $data Full path and file name. + * @param boolean $isFile true to load a file or false to load a string. + * @return mixed SimpleXMLElement on success or false on error. + * @todo This may go in a separate class - error reporting may be improved. + */ + public static function getXml($data, $isFile = true) + { + // Disable libxml errors and allow to fetch error information as needed + libxml_use_internal_errors(true); + + if ($isFile) + { + // Try to load the XML file + $xml = simplexml_load_file($data); + } + else + { + // Try to load the XML string + $xml = simplexml_load_string($data); + } + + return $xml; + } +} diff --git a/core/libraries/Hubzero/Access/Asset.php b/core/libraries/Hubzero/Access/Asset.php new file mode 100644 index 00000000000..cd7cbeada6f --- /dev/null +++ b/core/libraries/Hubzero/Access/Asset.php @@ -0,0 +1,183 @@ + 'notempty', + 'name' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'parent_id' + ); + + /** + * Automatic fields to populate every time a row is touched + * + * @var array + **/ + public $always = array( + 'rules' + ); + + /** + * Sets up additional custom rules + * + * @return void + */ + public function setup() + { + $this->addRule('parent_id', function($data) + { + /*if (!isset($data['parent_id']) || $data['parent_id'] == 0) + { + return 'Entries must have a parent ID.'; + }*/ + + if (isset($data['parent_id']) && $data['parent_id']) + { + $parent = self::oneOrNew($data['parent_id']); + + return $parent->get('id') ? false : 'The set parent does not exist.'; + } + else + { + return false; + } + }); + } + + /** + * Generates automatic parent_id field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticParentId($data) + { + if (!isset($data['parent_id'])) + { + $data['parent_id'] = 0; + } + + if ((!isset($data['id']) || !$data['id']) + && ($data['parent_id'] == 0)) + { + $data['parent_id'] = self::getRootId(); + } + + return $data['parent_id']; + } + + /** + * Generates automatic rules field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticRules($data) + { + if (!isset($data['rules'])) + { + $data['rules'] = '{}'; + } + + if (!is_string($data['rules'])) + { + $data['rules'] = (string)$data['rules']; + } + + return $data['rules']; + } + + /** + * Method to load an asset by it's name. + * + * @param string $name + * @return object + */ + public static function oneByName($name) + { + $model = self::all() + ->whereEquals('name', $name) + ->row(); + + if (!$model) + { + $model = self::blank(); + } + + return $model; + } + + /** + * Method to load root node ID + * + * @return integer + */ + public static function getRootId() + { + $result = self::all() + ->whereEquals('parent_id', 0) + ->row(); + + if (!$result->get('id')) + { + $result = self::all() + ->whereEquals('lft', 0) + ->row(); + + if (!$result->get('id')) + { + $result = self::all() + ->whereEquals('alias', 'root.1') + ->row(); + } + } + + return $result->get('id'); + } +} diff --git a/core/libraries/Hubzero/Access/Group.php b/core/libraries/Hubzero/Access/Group.php new file mode 100644 index 00000000000..91edb54b5de --- /dev/null +++ b/core/libraries/Hubzero/Access/Group.php @@ -0,0 +1,289 @@ + 'notempty' + ); + + /** + * Sets up additional custom rules + * + * @return void + */ + public function setup() + { + $this->addRule('title', function($data) + { + if (!isset($data['title']) || $data['title'] == '') + { + return 'Title is required.'; + } + + $exist = self::all() + ->whereEquals('title', $data['title']) + ->whereEquals('parent_id', $data['parent_id']) + ->where('id', '<>', $data['id']) + ->count(); + + return $exist ? 'Access group title already exists.' : false; + }); + } + + /** + * Defines a relationship to the User/Group Map + * + * @return object + */ + public function maps() + { + return $this->oneToMany('Map', 'group_id'); + } + + /** + * Get parent + * + * @return object + */ + public function parent() + { + return $this->oneToOne('Group', 'parent'); + } + + /** + * Load a record by title + * + * @param string $title + * @return object + */ + public static function oneByTitle($title) + { + return self::all() + ->whereEquals('title', $title) + ->row(); + } + + /** + * Saves the current model to the database + * + * @return bool + */ + public function save() + { + if ($result = parent::save()) + { + // Rebuild the nested set tree. + $this->rebuild(); + } + + return $result; + } + + /** + * Method to recursively rebuild the nested set tree. + * + * @param integer $parent_id The root of the tree to rebuild. + * @param integer $left The left id to start with in building the tree. + * @return boolean True on success + */ + public function rebuild($parent_id = 0, $left = 0) + { + // get all children of this node + $children = self::all() + ->select('id') + ->whereEquals('parent_id', (int) $parent_id) + ->order('parent_id', 'asc') + ->rows(); + + // the right value of this node is the left value + 1 + $right = $left + 1; + + // execute this function recursively over all children + foreach ($children as $child) + { + // $right is the current right value, which is incremented on recursion return + $right = $this->rebuild($child->get('id'), $right); + + // if there is an update failure, return false to break out of the recursion + if ($right === false) + { + return false; + } + } + + // we've got the left value, and now that we've processed + // the children of this node we also know the right value + $query = $this->getQuery() + ->update($this->getTableName()) + ->set(array( + 'lft' => (int) $left, + 'rgt' => (int) $right + )) + ->whereEquals('id', (int) $parent_id); + + // if there is an update failure, return false to break out of the recursion + if (!$query->execute()) + { + return false; + } + + // return the right value of this node + 1 + return $right + 1; + } + + /** + * Delete this object and its dependencies + * + * @return boolean + */ + public function destroy() + { + if ($this->get('id') == 0) + { + $this->addError('JGLOBAL_CATEGORY_NOT_FOUND'); + return false; + } + + if ($this->get('parent_id') == 0) + { + $this->addError('JLIB_DATABASE_ERROR_DELETE_ROOT'); + return false; + } + + if ($this->get('lft') == 0 or $this->get('rgt') == 0) + { + $this->addError('JLIB_DATABASE_ERROR_DELETE_ROOT'); + return false; + } + + // Select it's children + $children = self::all() + ->where('lft', '>=', (int)$this->get('lft')) + ->where('rgt', '<=', (int)$this->get('rgt')) + ->rows(); + + if (!$children->count()) + { + $this->addError('JLIB_DATABASE_ERROR_DELETE_CATEGORY'); + return false; + } + + // Delete the dependencies + $ids = array(); + + foreach ($children as $child) + { + $ids[] = $child->get('id'); + } + + $query = $this->getQuery() + ->delete($this->getTableName()) + ->whereIn('id', $ids); + + if (!$query->execute()) + { + $this->addError($query->getError()); + return false; + } + + // Delete the usergroup in view levels + $find = array(); + $replace = array(); + foreach ($ids as $id) + { + $find[] = "[$id,"; + $find[] = ",$id,"; + $find[] = ",$id]"; + $find[] = "[$id]"; + + $replace[] = "["; + $replace[] = ","; + $replace[] = "]"; + $replace[] = "[]"; + } + + $rules = Viewlevel::all() + ->rows(); + + foreach ($rules as $rule) + { + foreach ($ids as $id) + { + if (strstr($rule->get('rules'), '[' . $id) + || strstr($rule->get('rules'), ',' . $id) + || strstr($rule->get('rules'), $id . ']')) + { + $rule->set('rules', str_replace($find, $replace, $rule->get('rules'))); + + if (!$rule->save()) + { + $this->addError($rule->getError()); + return false; + } + } + } + } + + // Delete the user to usergroup mappings for the group(s) from the database. + try + { + Map::destroyByGroup($ids); + } + catch (\Exception $e) + { + $this->addError($e->getMessage()); + return false; + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Access/Map.php b/core/libraries/Hubzero/Access/Map.php new file mode 100644 index 00000000000..ecf71d77631 --- /dev/null +++ b/core/libraries/Hubzero/Access/Map.php @@ -0,0 +1,225 @@ + 'positive|nonzero', + 'group_id' => 'positive|nonzero' + ); + + /** + * Defines a relationship to the User/Group Map + * + * @return object + */ + public function user() + { + return $this->belongsToOne('Hubzero\User\User', 'user_id'); + } + + /** + * Defines a relationship to the User/Group Map + * + * @return object + */ + public function group() + { + return $this->belongsToOne('Hubzero\Access\Group', 'group_id'); + } + + /** + * Delete this object and its dependencies + * + * @return boolean + */ + public function destroy() + { + $query = $this->getQuery() + ->delete($this->getTableName()) + ->whereEquals('group_id', $this->get('group_id')) + ->whereEquals('user_id', $this->get('user_id')); + + if (!$query->execute()) + { + $this->addError($query->getError()); + return false; + } + + return true; + } + + /** + * Delete objects of this type by Access Group ID + * + * @param mixed $group_id Integer or array of integers + * @return boolean + */ + public static function destroyByGroup($group_id) + { + $group_id = (is_array($group_id) ? $group_id : array($group_id)); + + $blank = self::blank(); + + $query = $blank->getQuery() + ->delete($blank->getTableName()) + ->whereIn('group_id', $group_id); + + if (!$query->execute()) + { + return false; + } + + return true; + } + + /** + * Delete objects of this type by User ID + * + * @param mixed $user_id Integer or array of integers + * @return boolean + */ + public static function destroyByUser($user_id) + { + $user_id = (is_array($user_id) ? $user_id : array($user_id)); + + $blank = self::blank(); + + $query = $blank->getQuery() + ->delete($blank->getTableName()) + ->whereIn('user_id', $user_id); + + if (!$query->execute()) + { + return false; + } + + return true; + } + + /** + * Add a user to access groups + * + * @param mixed $user_id Integer + * @param mixed $group_id Integer or array of integers + * @return boolean + */ + public static function addUserToGroup($user_id, $group_id) + { + // Get the user's existing entries + $entries = self::all() + ->whereEquals('user_id', $user_id) + ->rows(); + + $existing = array(); + foreach ($entries as $entry) + { + $existing[] = $entry->get('group_id'); + } + + $group_id = (is_array($group_id) ? $group_id : array($group_id)); + + $blank = self::blank(); + + // Loop through groups to be added + foreach ($group_id as $group) + { + $group = intval($group); + + // Is the group already an existing entry? + if (in_array($group, $existing)) + { + // Skip. + continue; + } + + $query = $blank->getQuery() + ->insert($blank->getTableName()) + ->values(array( + 'user_id' => $user_id, + 'group_id' => $group + )); + + if (!$query->execute()) + { + return false; + } + } + + return true; + } + + /** + * Remove a user from an access groups + * + * @param mixed $user_id Integer + * @param mixed $group_id Integer or array of integers + * @return boolean + */ + public static function removeUserFromGroup($user_id, $group_id) + { + $group_id = (is_array($group_id) ? $group_id : array($group_id)); + + $blank = self::blank(); + + $query = $blank->getQuery() + ->delete($blank->getTableName()) + ->whereEquals('user_id', $user_id) + ->whereIn('group_id', $group_id); + + if (!$query->execute()) + { + return false; + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Access/Rule.php b/core/libraries/Hubzero/Access/Rule.php new file mode 100644 index 00000000000..b040bd2d125 --- /dev/null +++ b/core/libraries/Hubzero/Access/Rule.php @@ -0,0 +1,155 @@ + true, 3 => true, 4 => false) + * or an equivalent JSON encoded string. + * + * @param mixed $identities A JSON format string (probably from the database) or a named array. + * @return void + */ + public function __construct($identities) + { + // Convert string input to an array. + if (is_string($identities)) + { + $identities = json_decode($identities, true); + } + + $this->mergeIdentities($identities); + } + + /** + * Get the data for the action. + * + * @return array A named array + */ + public function getData() + { + return $this->data; + } + + /** + * Merges the identities + * + * @param mixed $identities An integer or array of integers representing the identities to check. + * @return void + */ + public function mergeIdentities($identities) + { + if ($identities instanceof Rule) + { + $identities = $identities->getData(); + } + + if (is_array($identities)) + { + foreach ($identities as $identity => $allow) + { + $this->mergeIdentity($identity, $allow); + } + } + } + + /** + * Merges the values for an identity. + * + * @param integer $identity The identity. + * @param boolean $allow The value for the identity (true == allow, false == deny). + * @return void + */ + public function mergeIdentity($identity, $allow) + { + $identity = (int) $identity; + $allow = (int) ((boolean) $allow); + + // Check that the identity exists. + if (isset($this->data[$identity])) + { + // Explicit deny always wins a merge. + if ($this->data[$identity] !== 0) + { + $this->data[$identity] = $allow; + } + } + else + { + $this->data[$identity] = $allow; + } + } + + /** + * Checks that this action can be performed by an identity. + * + * The identity is an integer where +ve represents a user group, + * and -ve represents a user. + * + * @param mixed $identities An integer or array of integers representing the identities to check. + * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. + */ + public function allow($identities) + { + // Implicit deny by default. + $result = null; + + // Check that the inputs are valid. + if (!empty($identities)) + { + if (!is_array($identities)) + { + $identities = array($identities); + } + + foreach ($identities as $identity) + { + // Technically the identity just needs to be unique. + $identity = (int) $identity; + + // Check if the identity is known. + if (isset($this->data[$identity])) + { + $result = (boolean) $this->data[$identity]; + + // An explicit deny wins. + if ($result === false) + { + break; + } + } + + } + } + + return $result; + } + + /** + * Convert this object into a JSON encoded string. + * + * @return string JSON encoded string + */ + public function __toString() + { + return json_encode($this->data); + } +} diff --git a/core/libraries/Hubzero/Access/Rules.php b/core/libraries/Hubzero/Access/Rules.php new file mode 100644 index 00000000000..aab6d8ee576 --- /dev/null +++ b/core/libraries/Hubzero/Access/Rules.php @@ -0,0 +1,196 @@ + array(-42 => true, 3 => true, 4 => false)) + * or an equivalent JSON encoded string, or an object where properties are arrays. + * + * @param mixed $input A JSON format string (probably from the database) or a nested array. + * @return void + */ + public function __construct($input = '') + { + // Convert in input to an array. + if (is_string($input)) + { + $input = json_decode($input, true); + } + elseif (is_object($input)) + { + $input = (array) $input; + } + + if (is_array($input)) + { + // Top level keys represent the actions. + foreach ($input as $action => $identities) + { + $this->mergeAction($action, $identities); + } + } + } + + /** + * Get the data for the action. + * + * @return array A named array of Rule objects. + */ + public function getData() + { + return $this->data; + } + + /** + * Method to merge a collection of Rules. + * + * @param mixed $input Rule or array of Rules + * @return void + */ + public function mergeCollection($input) + { + // Check if the input is an array. + if (is_array($input)) + { + foreach ($input as $actions) + { + $this->merge($actions); + } + } + } + + /** + * Method to merge actions with this object. + * + * @param mixed $actions Rule object, an array of actions or a JSON string array of actions. + * @return void + */ + public function merge($actions) + { + if (is_string($actions)) + { + $actions = json_decode($actions, true); + } + + if (is_array($actions)) + { + foreach ($actions as $action => $identities) + { + $this->mergeAction($action, $identities); + } + } + elseif ($actions instanceof Rules) + { + $data = $actions->getData(); + + foreach ($data as $name => $identities) + { + $this->mergeAction($name, $identities); + } + } + } + + /** + * Merges an array of identities for an action. + * + * @param string $action The name of the action. + * @param array $identities An array of identities + * @return void + */ + public function mergeAction($action, $identities) + { + if (isset($this->data[$action])) + { + // If exists, merge the action. + $this->data[$action]->mergeIdentities($identities); + } + else + { + // If new, add the action. + $this->data[$action] = new Rule($identities); + } + } + + /** + * Checks that an action can be performed by an identity. + * + * The identity is an integer where +ve represents a user group, + * and -ve represents a user. + * + * @param string $action The name of the action. + * @param mixed $identity An integer representing the identity, or an array of identities + * @return mixed Object or null if there is no information about the action. + */ + public function allow($action, $identity) + { + // Check we have information about this action. + if (isset($this->data[$action])) + { + return $this->data[$action]->allow($identity); + } + + return null; + } + + /** + * Get the allowed actions for an identity. + * + * @param mixed $identity An integer representing the identity or an array of identities + * @return object Allowed actions for the identity or identities + */ + public function getAllowed($identity) + { + // Sweep for the allowed actions. + $allowed = new Obj; + + foreach ($this->data as $name => &$action) + { + if ($action->allow($identity)) + { + $allowed->set($name, true); + } + } + + return $allowed; + } + + /** + * Magic method to convert the object to JSON string representation. + * + * @return string JSON representation of the actions array + */ + public function __toString() + { + $temp = array(); + + foreach ($this->data as $name => $rule) + { + // Convert the action to JSON, then back into an array otherwise + // re-encoding will quote the JSON for the identities in the action. + $temp[$name] = json_decode((string) $rule); + } + + return json_encode($temp); + } +} diff --git a/core/libraries/Hubzero/Access/Viewlevel.php b/core/libraries/Hubzero/Access/Viewlevel.php new file mode 100644 index 00000000000..652dc116b91 --- /dev/null +++ b/core/libraries/Hubzero/Access/Viewlevel.php @@ -0,0 +1,92 @@ + 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'ordering' + ); + + /** + * Generates automatic ordering field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticOrdering($data) + { + if (!isset($data['ordering'])) + { + $last = self::all() + ->select('ordering') + ->order('ordering', 'desc') + ->row(); + + $data['ordering'] = $last->ordering + 1; + } + + return $data['ordering']; + } + + /** + * Saves the current model to the database + * + * @return bool + */ + public function save() + { + // Bind the rules as appropriate. + if (is_array($this->get('rules'))) + { + $this->set('rules', json_encode($this->get('rules'))); + } + + return parent::save(); + } +} diff --git a/core/libraries/Hubzero/Activity/Digest.php b/core/libraries/Hubzero/Activity/Digest.php new file mode 100644 index 00000000000..bbe7ad676e2 --- /dev/null +++ b/core/libraries/Hubzero/Activity/Digest.php @@ -0,0 +1,62 @@ + 'notempty', + 'scope_id' => 'positive|nonzero' + ); + + /** + * Load a record by scope and scope ID + * + * @param integer $scope_id + * @param string $scope + * @return object + */ + public static function oneByScope($scope_id, $scope) + { + return self::all() + ->whereEquals('scope_id', (int)$scope_id) + ->whereEquals('scope', (string)$scope) + ->row(); + } +} diff --git a/core/libraries/Hubzero/Activity/Log.php b/core/libraries/Hubzero/Activity/Log.php new file mode 100644 index 00000000000..25a3b57d679 --- /dev/null +++ b/core/libraries/Hubzero/Activity/Log.php @@ -0,0 +1,394 @@ + 'notempty', + 'scope' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'created', + 'created_by'//, + //'uuid' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $always = array( + 'scope' + ); + + /** + * Container for details + * + * @var object + */ + protected $entryDetails = null; + + /** + * Generate a UUID + * + * @param array $data + * @return string + */ + public function automaticUuid($data) + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } + + /** + * Generates automatic scope field value + * + * @param array $data + * @return string + */ + public function automaticScope($data) + { + if (!isset($data['scope'])) + { + $data['scope'] = ''; + } + return strtolower(preg_replace("/[^a-zA-Z0-9\-_\.]/", '', trim($data['scope']))); + } + + /** + * Get recipients + * + * @return object + */ + public function recipients() + { + return $this->oneToMany('Recipient', 'log_id'); + } + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function creator() + { + return $this->belongsToOne('Hubzero\User\User', 'created_by'); + } + + /** + * Defines a belongs to one relationship between entry and another entry + * + * @return object + */ + public function parent() + { + return $this->belongsToOne('Hubzero\Activity\Log', 'parent'); + } + + /** + * Get children + * + * @return object + */ + public function children() + { + return $this->oneToMany('Hubzero\Activity\Log', 'parent'); + } + + /** + * Delete the record and all associated data + * + * @return boolean False if error, True on success + */ + public function destroy() + { + foreach ($this->recipients()->rows() as $recipient) + { + if (!$recipient->destroy()) + { + $this->addError($recipient->getError()); + return false; + } + } + + foreach ($this->children()->rows() as $child) + { + if (!$child->destroy()) + { + $this->addError($child->getError()); + return false; + } + } + + $result = parent::destroy(); + + if ($result) + { + Event::trigger('activity.onLogDelete', [$this]); + } + + return $result; + } + + /** + * Delete the record and all associated data + * + * @return boolean False if error, True on success + */ + public function save() + { + $data = $this->get('details'); + + if ($data instanceof Registry) + { + $this->set('details', $data->toString()); + } + else if (!is_string($data)) + { + $this->set('details', json_encode($data)); + } + + $isNew = $this->isNew(); + $result = parent::save(); + + if ($result) + { + Event::trigger('activity.onLogSave', [$this, $isNew]); + } + + return $result; + } + + /** + * Transform details into object + * + * @return object Hubzero\Config\Registry + */ + public function transformDetails() + { + if (!isset($this->entryDetails)) + { + $this->entryDetails = new Registry($this->get('details')); + } + + return $this->entryDetails; + } + + /** + * Send an activity to recipients + * + * @param array $recipients + * @return bool + */ + public function broadcast($recipients = array()) + { + // Get everyone subscribed + $subscriptions = Subscription::all() + ->whereEquals('scope', $this->get('scope')) + ->whereEquals('scope_id', $this->get('scope_id')) + ->rows(); + + foreach ($subscriptions as $subscription) + { + $recipients[] = array( + 'scope' => 'user', + 'scope_id' => $subscription->get('user_id') + ); + } + + $sent = array(); + + // Do we have any recipients? + foreach ($recipients as $receiver) + { + // Default to type 'user' + if (!is_array($receiver)) + { + $receiver = array( + 'scope' => 'user', + 'scope_id' => $receiver + ); + } + + // Make sure we have expected data + if (!isset($receiver['scope']) + || !isset($receiver['scope_id'])) + { + $receiver = array_values($receiver); + + $receiver['scope'] = $receiver[0]; + $receiver['scope_id'] = $receiver[1]; + } + + $key = $receiver['scope'] . '.' . $receiver['scope_id']; + + // No duplicate sendings + if (in_array($key, $sent)) + { + continue; + } + + // Create a recipient object that ties a user to an activity + $recipient = Recipient::blank()->set([ + 'scope' => $receiver['scope'], + 'scope_id' => $receiver['scope_id'], + 'log_id' => $this->get('id'), + 'state' => Recipient::STATE_PUBLISHED + ]); + + if (!$recipient->save()) + { + return false; + } + + $sent[] = $key; + } + + return true; + } + + /** + * Create an activity log entry and broadcast it. + * + * @param mixed $data + * @param array $recipients + * @return boolean + */ + public static function log($data = array(), $recipients = array()) + { + if (is_object($data)) + { + $data = (array) $data; + } + + if (is_string($data)) + { + $data = array('description' => $data); + + $data['action'] = 'create'; + + if (substr(strtolower($data['description']), 0, 6) == 'update') + { + $data['action'] = 'update'; + } + + if (substr(strtolower($data['description']), 0, 6) == 'delete') + { + $data['action'] = 'delete'; + } + } + + try + { + $activity = self::blank()->set($data); + + if (!$activity->save()) + { + return false; + } + + if (!$activity->broadcast($recipients)) + { + return false; + } + } + catch (Exception $e) + { + return false; + } + + return true; + } + + /** + * Modify query to only return published entries + * + * @return object + */ + public function wherePublished() + { + $this->whereEquals(Recipient::blank()->getTableName() . '.state', Recipient::STATE_PUBLISHED); + return $this; + } + + /** + * Get all logs for a recipient + * + * @param string $scope + * @param integer $scope_id + * @return boolean + */ + public static function allForRecipient($scope, $scope_id = 0) + { + if (!is_array($scope)) + { + $scope = array($scope); + } + + $logs = self::all(); + + $r = Recipient::blank()->getTableName(); + $l = $logs->getTableName(); + + $logs + ->select($l . '.*') + ->join($r, $l . '.id', $r . '.log_id') + ->whereIn($r . '.scope', $scope) + ->whereEquals($r . '.scope_id', $scope_id); + + return $logs; + } +} diff --git a/core/libraries/Hubzero/Activity/Recipient.php b/core/libraries/Hubzero/Activity/Recipient.php new file mode 100644 index 00000000000..968cb64f47c --- /dev/null +++ b/core/libraries/Hubzero/Activity/Recipient.php @@ -0,0 +1,198 @@ + 'positive|nonzero', + 'scope' => 'notempty', + 'scope_id' => 'positive|nonzero' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'created' + ); + + /** + * Defines a belongs to one relationship between recipient and log + * + * @return object + */ + public function log() + { + return $this->belongsToOne('Hubzero\Activity\Log', 'log_id'); + } + + /** + * Check if entry has been viewed or not + * + * @return boolean + */ + public function wasViewed() + { + if ($this->get('viewed') + && $this->get('viewed') != '0000-00-00 00:00:00') + { + return true; + } + + return false; + } + + /** + * Mark entry as having been viewed + * + * @return boolean + */ + public function markAsViewed() + { + $dt = new Date('now'); + + $this->set('viewed', $dt->toSql()); + + return $this->save(); + } + + /** + * Mark entry as NOT having been viewed + * + * @return boolean + */ + public function markAsNotViewed() + { + $this->set('viewed', null); + + return $this->save(); + } + + /** + * Mark entry as being published + * + * @return boolean + */ + public function markAsPublished() + { + $this->set('state', self::STATE_PUBLISHED); + + return $this->save(); + } + + /** + * Mark entry as being unpublished + * + * @return boolean + */ + public function markAsUnpublished() + { + $this->set('state', self::STATE_UNPUBLISHED); + + return $this->save(); + } + + /** + * Mark entry as starred + * + * @return boolean + */ + public function markAsStarred() + { + $this->set('starred', 1); + + return $this->save(); + } + + /** + * Mark entry as not starred + * + * @return boolean + */ + public function markAsNotStarred() + { + $this->set('starred', 0); + + return $this->save(); + } + + /** + * Modify query to only return published entries + * + * @return object + */ + public function wherePublished() + { + $this->whereEquals($this->getTableName() . '.state', self::STATE_PUBLISHED); + return $this; + } + + /** + * Get all entries for a scope + * + * @param string $scope + * @param integer $scope_id + * @return boolean + */ + public static function allForScope($scope, $scope_id = 0) + { + if (!is_array($scope)) + { + $scope = array($scope); + } + + $recipient = self::all(); + + $r = $recipient->getTableName(); + $l = \Hubzero\Activity\Log::blank()->getTableName(); + + $recipient + ->select($r . '.*') + ->including('log') + ->join($l, $l . '.id', $r . '.log_id') + ->whereIn($r . '.scope', $scope) + ->whereEquals($r . '.scope_id', $scope_id); + + return $recipient; + } +} diff --git a/core/libraries/Hubzero/Activity/Subscription.php b/core/libraries/Hubzero/Activity/Subscription.php new file mode 100644 index 00000000000..822507c647d --- /dev/null +++ b/core/libraries/Hubzero/Activity/Subscription.php @@ -0,0 +1,79 @@ + 'positive|nonzero', + 'scope' => 'notempty', + 'scope_id' => 'positive|nonzero' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'created' + ); + + /** + * Load a record by scope and scope ID + * + * @param integer $scope_id + * @param string $scope + * @param integer $user_id + * @return object + */ + public static function oneByScope($scope_id, $scope, $user_id = 0) + { + $model = self::all() + ->whereEquals('scope_id', (int)$scope_id) + ->whereEquals('scope', (string)$scope); + + if ($user_id) + { + $model->whereEquals('user_id', (int)$user_id); + } + + return $model->row(); + } +} diff --git a/core/libraries/Hubzero/Api/AuthServiceProvider.php b/core/libraries/Hubzero/Api/AuthServiceProvider.php new file mode 100644 index 00000000000..53c0cc24628 --- /dev/null +++ b/core/libraries/Hubzero/Api/AuthServiceProvider.php @@ -0,0 +1,92 @@ +app['auth'] = function($app) + { + return new Guard($app); + }; + } + + /** + * Handle request in HTTP stack + * + * @param object $request HTTP Request + * @return mixed + */ + public function handle(Request $request) + { + $response = $this->next($request); + + // If CLI then we have to gather all query, post and header values + // into params for Oauth_Provider's constructor. + $params = array(); + + if ($this->app->runningInConsole()) + { + $queryvars = $this->app['request']->get('queryvars'); + $postvars = $this->app['request']->get('postdata'); + + if (!empty($queryvars)) + { + foreach ($queryvars as $key => $value) + { + if (isset($queryvars[$key])) + { + $params[$key] = $queryvars[$key]; + } + else if (isset($postvars[$key])) + { + $params[$key] = $postvars[$key]; + } + } + } + + if (!empty($postvars)) + { + foreach ($postvars as $key => $value) + { + if (isset($queryvars[$key])) + { + $params[$key] = $queryvars[$key]; + } + else if (isset($postvars[$key])) + { + $params[$key] = $postvars[$key]; + } + } + } + + if (empty($params)) + { + return false; + } + } + + $this->app['authn'] = $this->app['auth']->authenticate($params); + + $this->app['request']->setVar('validApiKey', !empty($this->app['authn']['consumer_key'])); + + return $response; + } +} diff --git a/core/libraries/Hubzero/Api/Component/Loader.php b/core/libraries/Hubzero/Api/Component/Loader.php new file mode 100644 index 00000000000..1029ac3eb9c --- /dev/null +++ b/core/libraries/Hubzero/Api/Component/Loader.php @@ -0,0 +1,133 @@ +app['language']; + + if (empty($option)) + { + // Throw 404 if no component + $this->app->abort(404, $lang->translate('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND')); + } + + $option = $this->canonical($option); + + // Record the scope + $scope = $this->app->has('scope') ? $this->app->get('scope') : null; + + // Set scope to component name + $this->app->set('scope', $option); + + // Build the component path. + $client = (isset($this->app['client']->alias) ? $this->app['client']->alias : $this->app['client']->name); + $found = false; + + $version = $this->app['request']->getVar('version'); + $controller = $this->app['request']->getCmd('controller', $this->app['request']->segment(2, 'api')); + $controllerClass = '\\Hubzero\\Component\\ApiController'; + + // Make sure the component is enabled + if ($this->isEnabled($option) && is_dir($this->path($option))) + { + // Set path and constants + define('PATH_COMPONENT', $this->path($option) . DIRECTORY_SEPARATOR . $client); + define('PATH_COMPONENT_SITE', $this->path($option) . DIRECTORY_SEPARATOR . 'site'); + define('PATH_COMPONENT_ADMINISTRATOR', $this->path($option) . DIRECTORY_SEPARATOR . 'admin'); + + // Legacy compatibility + // @TODO: Deprecate this! + define('JPATH_COMPONENT', PATH_COMPONENT); + define('JPATH_COMPONENT_SITE', PATH_COMPONENT_SITE); + define('JPATH_COMPONENT_ADMINISTRATOR', PATH_COMPONENT_ADMINISTRATOR); + + if (is_dir(PATH_COMPONENT)) + { + // If no version is specified, try to determine the most + // recent version from the available controllers + if (!$version) + { + $files = glob(PATH_COMPONENT . DIRECTORY_SEPARATOR . 'controllers' . DIRECTORY_SEPARATOR . $controller . 'v*.php'); + + if (!empty($files)) + { + natsort($files); + + $file = end($files); + $controller = basename($file, '.php'); + } + } + else + { + $controller .= 'v' . str_replace('.', '_', $version); + } + + $path = PATH_COMPONENT . DIRECTORY_SEPARATOR . 'controllers' . DIRECTORY_SEPARATOR . $controller . '.php'; + $controllerClass = '\\Components\\' . ucfirst(substr($option, 4)) . '\\Api\\Controllers\\' . ucfirst($controller); + + // Include the file + if (file_exists($path)) + { + require_once $path; + } + } + + // Check to see if the class exists + if ($controllerClass && class_exists($controllerClass)) + { + $found = true; + + $lang->load($option, PATH_COMPONENT, null, false, true); + } + } + + if (!$found) + { + $this->app->abort(404, $lang->translate('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND')); + } + + // Handle template preview outlining. + $action = new $controllerClass($this->app->get('response'), array( + 'name' => substr($option, 4), + 'controller' => $controller + )); + $action->execute(); + + // Revert the scope + $this->app->forget('scope'); + $this->app->set('scope', $scope); + + return true; + } + + /** + * Execute the component. + * + * @param string $path The component path. + * @return string The component output + */ + protected function execute($path) + { + return ''; + } +} diff --git a/core/libraries/Hubzero/Api/ComponentServiceProvider.php b/core/libraries/Hubzero/Api/ComponentServiceProvider.php new file mode 100644 index 00000000000..5f778cc8bf2 --- /dev/null +++ b/core/libraries/Hubzero/Api/ComponentServiceProvider.php @@ -0,0 +1,68 @@ +app->has('component')) + { + $this->app->forget('component'); + } + + $this->app['component'] = function($app) + { + return new Loader($app); + }; + } + + /** + * Handle request in HTTP stack + * + * @param object $request HTTP Request + * @return mixed + */ + public function handle(Request $request) + { + $response = $this->next($request); + + if (!$this->app->runningInConsole()) + { + $component = $request->getCmd('option'); + + if (!$component) + { + $this->app->abort(404); + } + + $contents = $this->app['component']->render($component); + + $this->app['dispatcher']->trigger('system.onAfterDispatch'); + + if ($this->app->has('profiler')) + { + $this->app['profiler'] ? $this->app['profiler']->mark('afterDispatch') : null; + } + } + + return $response; + } +} diff --git a/core/libraries/Hubzero/Api/Doc/Generator.php b/core/libraries/Hubzero/Api/Doc/Generator.php new file mode 100644 index 00000000000..780ba3cf008 --- /dev/null +++ b/core/libraries/Hubzero/Api/Doc/Generator.php @@ -0,0 +1,412 @@ +cache = (bool) $cache; + + // create all needed keys in output + $this->output = array( + 'sections' => array(), + 'versions' => array( + 'max' => '', + 'available' => array() + ), + 'errors' => array(), + 'files' => array() + ); + } + + /** + * Return documentation + * + * @param string $format Output format + * @param bool $format Force new version + * @return string + */ + public function output($format = 'json', $force = false) + { + // generate output + if ($force || !$this->cache()) + { + $this->generate(); + } + + // option to switch formats + switch ($format) + { + case 'array': + break; + case 'php': + $this->output = serialize($this->output); + break; + case 'json': + default: + $this->output = json_encode($this->output); + } + + return $this->output; + } + + /** + * Load from cache + * + * @return boolean + */ + private function cache() + { + if (!$this->cache) + { + return false; + } + + // get developer params to get cache expiration + if (App::has('component')) + { + $developerParams = App::get('component')->params('com_developer'); + } + else + { + $developerParams = new \Hubzero\Config\Registry(); + } + $cacheExpiration = $developerParams->get('doc_expiration', 720); + + // cache file + $cacheFile = PATH_APP . DS . 'cache' . DS . 'api' . DS . 'documentation.json'; + + // check if we have a cache file + if (file_exists($cacheFile)) + { + // check if its still valid + $cacheMakeTime = @filemtime($cacheFile); + if (time() - $cacheExpiration < $cacheMakeTime) + { + $this->output = json_decode(file_get_contents($cacheFile), true); + return true; + } + } + + return false; + } + + /** + * Generate Doc + * + * @return void + */ + private function generate() + { + // only load sections if we dont have a cache + $this->discoverComponentSections(); + + // generate output by processing sections + $this->output['sections'] = $this->processComponentSections($this->sections); + + // remove duplicate available versions & order + $this->output['versions']['available'] = array_unique($this->output['versions']['available']); + + // get the highest version available + $this->output['versions']['max'] = end($this->output['versions']['available']); + + // create cache folder + $cacheFile = PATH_APP . DS . 'cache' . DS . 'api' . DS . 'documentation.json'; + + if (App::has('filesystem') && !App::get('filesystem')->exists(dirname($cacheFile))) + { + App::get('filesystem')->makeDirectory(dirname($cacheFile)); + } + + // save cache file + file_put_contents($cacheFile, json_encode($this->output)); + } + + /** + * Load api controller files and group by component + * + * @return void + */ + private function discoverComponentSections() + { + $loader = null; + if (App::has('component')) + { + $loader = App::get('component'); + } + + $roots = array(PATH_CORE, PATH_APP); + + // group by component + foreach ($roots as $base) + { + foreach (glob($base . DS . 'components' . DS . 'com_*' . DS . 'api') as $path) + { + // get component + $pieces = explode(DS, $path); + array_pop($pieces); + $component = str_replace('com_', '', array_pop($pieces)); + + if ($loader && !$loader->isEnabled('com_' . $component)) + { + continue; + } + + // add all matching files to section + $this->sections[$component] = glob($path . DS . 'controllers' . DS . '*.php'); + } + } + } + + /** + * Process sections + * + * @param array $sections All the component api controllers grouped by component + * @return array + */ + private function processComponentSections($sections) + { + // var to hold output + $output = array(); + + // loop through each component grouping + foreach ($sections as $component => $files) + { + // if we dont have an array for that component let's create it + if (!isset($output[$component])) + { + $output[$component] = []; + } + + // loop through each file + foreach ($files as $file) + { + if (!preg_match('/(.*)v[0-9]+_[0-9]+.php$/', $file)) + { + continue; + } + $output[$component] = array_merge($output[$component], $this->processFile($file)); + } + } + + // return output + return $output; + } + + /** + * Process an individual file + * + * @param string $file File path + * @return array Processed endpoints + */ + private function processFile($file) + { + // var to hold output + $output = array(); + + require_once $file; + + $className = $this->parseClassFromFile($file); + $component = $this->parseClassFromFile($file, true)['component']; + $version = $this->parseClassFromFile($file, true)['version']; + + // Push file to files array + $this->output['files'][] = $file; + + // Push version to versions array + $this->output['versions']['available'][] = $version; + + if (!class_exists($className)) + { + return $output; + } + + $classReflector = new ReflectionClass($className); + + foreach ($classReflector->getMethods() as $method) + { + // Create docblock object & make sure we have something + $phpdoc = new DocBlock($method); + + // Skip methods we don't want processed + if (substr($method->getName(), -4) != 'Task' || in_array($method->getName(), array('registerTask', 'unregisterTask', 'indexTask'))) + { + continue; + } + + // Skip method in the parent class (already processed), + if ($className != $method->getDeclaringClass()->getName()) + { + //continue; + } + + // Skip if we dont have a short desc + // but put in error + if (!$phpdoc->getShortDescription()) + { + $this->output['errors'][] = sprintf('Missing docblock for method "%s" in "%s"', $method->getName(), $file); + continue; + } + + $controller = basename($file); + $controller = preg_replace('#\.[^.]*$#', '', $controller); + $parts = explode('v', $controller); + $v = array_pop($parts); + $controller = implode('v', $parts); + + // Create endpoint data array + $endpoint = array( + //'name' => substr($method->getName(), 0, -4), + //'description' => preg_replace('/\s+/', ' ', $phpdoc->getShortDescription()), // $phpdoc->getLongDescription()->getContents() + 'name' => $phpdoc->getShortDescription(), + 'description' => $phpdoc->getLongDescription()->getContents(), + 'method' => '', + 'uri' => '', + 'parameters' => array(), + '_metadata' => array( + 'controller' => $controller, + 'component' => $component, + 'version' => $version, + 'method' => $method->getName() + ) + ); + + // Loop through each tag + foreach ($phpdoc->getTags() as $tag) + { + $name = strtolower(str_replace('api', '', $tag->getName())); + $content = $tag->getContent(); + + // Handle parameters separately + // json decode param input + if ($name == 'parameter') + { + $parameter = json_decode($content); + + if (json_last_error() != JSON_ERROR_NONE) + { + $this->output['errors'][] = sprintf('Unable to parse parameter info for method "%s" in "%s"', $method->getName(), $file); + continue; + } + + $endpoint['parameters'][] = (array) $parameter; + continue; + } + + if ($name == 'uri') + { + $content = str_replace(['{component}', '{controller}'], [$component, $controller], $content); + + if ($controller == $component) + { + $content = str_replace($component . '/' . $controller, $component, $content); + } + } + + if ($name == 'uri' && $method->getName() == 'indexTask') + { + $content .= $component; + } + + // Add data to endpoint data + $endpoint[$name] = $content; + } + + // Add endpoint to output + // We always want indexTask to be first in the list + if ($method->getName() == 'indexTask') + { + array_unshift($output, $endpoint); + } + else + { + $output[] = $endpoint; + } + } + + return $output; + } + + /** + * Get class name based on file + * + * @param string $file File path + * @param bool $returnAsParts Return as parts? + * @return mixed + */ + private function parseClassFromFile($file, $returnAsParts = false) + { + // replace some values in file path to get what we need + $file = str_replace( + array( + PATH_CORE . DS . 'components' . DS . 'com_', + PATH_APP . DS . 'components' . DS . 'com_', + '.php' + ), + array('', '', ''), + $file + ); + + // split by "/" + $parts = explode(DS, $file); + array_unshift($parts, 'components'); + + // do we want to return as parts? + if ($returnAsParts) + { + $parts['namespace'] = $parts[0]; + $parts['component'] = $parts[1]; + $parts['client'] = $parts[2]; + $parts['controller'] = $parts[4]; + $b = explode('v', $parts[4]); + $parts['version'] = end($b);//$parts[4]; + return $parts; + } + + // capitalize first letter + $parts = array_map('ucfirst', $parts); + + // put all the pieces back together + return str_replace('.', '_', implode('\\', $parts)); + } +} diff --git a/core/libraries/Hubzero/Api/Guard.php b/core/libraries/Hubzero/Api/Guard.php new file mode 100644 index 00000000000..563ff896e4d --- /dev/null +++ b/core/libraries/Hubzero/Api/Guard.php @@ -0,0 +1,101 @@ +app = $app; + } + + /** + * Grabs and returns the oauth token data + * + * @return array + */ + public function token() + { + return $this->token; + } + + /** + * Validates incoming request via OAuth2 specification + * + * @param array $params Oauth server request parameters + * @param array $options OAuth server configuration options + * @return array + */ + public function authenticate($params = array(), $options = array()) + { + // Placeholder response + $response = ['user_id' => null]; + + // Fire before auth event + Event::trigger('before_auth'); + + // Load oauth server + $oauthServer = new Server(new MysqlStorage, $options); + $oauthRequest = \OAuth2\Request::createFromGlobals(); + $oauthResponse = new \OAuth2\Response(); + + // Validate request via oauth + $oauthServer->verifyResourceRequest($oauthRequest, $oauthResponse); + + // Store our token locally + $this->token = $oauthServer->getAccessTokenData($oauthRequest); + + // See if we have a valid user + if (isset($this->token['uidNumber'])) + { + $response['user_id'] = $this->token['uidNumber']; + $user = User::oneOrNew($response['user_id']); + if ($user->get('id')) + { + $user->set('guest', false); + } + $this->app['session']->set('user', $user); + } + + // Fire after auth event + Event::trigger('after_auth'); + + // Return the response + return $response; + } +} diff --git a/core/libraries/Hubzero/Api/RateLimit/RateLimitService.php b/core/libraries/Hubzero/Api/RateLimit/RateLimitService.php new file mode 100644 index 00000000000..f00fe4a250d --- /dev/null +++ b/core/libraries/Hubzero/Api/RateLimit/RateLimitService.php @@ -0,0 +1,87 @@ +app['ratelimiter'] = function($app) + { + // creat new storage object + $storage = new Storage\Database($app['db']); + + // Get rate limit config (JSON encode/decode to get as array) + $config = json_decode(json_encode($app['config']->get('rate_limit')), true); + $config = (is_array($config)) ? $config : []; + + // Create and return new rate limiter + return new RateLimiter($storage, $config); + }; + } + + /** + * Handle request in HTTP stack + * + * @param object $request HTTP Request + * @return mixed + */ + public function handle(Request $request) + { + // Get response + $response = $this->next($request); + + // Get authentication + $token = $this->app['auth']->token(); + + // Rate limit application/user id and get data + $rateLimitData = $this->app['ratelimiter']->rateLimit($token['application_id'], $token['uidNumber']); + + // Calculate header values + $limit = $rateLimitData->limit_short; + $remaining = $rateLimitData->limit_short - $rateLimitData->count_short; + $reset = with(new Date($rateLimitData->expires_short))->toUnix(); + + // If we exceeded out rate limit lets respond accordingly + if ($rateLimitData->exceeded_long || $rateLimitData->exceeded_short) + { + throw new \Exception('You have exceeded your rate limit allowance. Please see rate limit headers for details.', 429); + + // Use different values for long + if ($rateLimitData->exceeded_long) + { + $limit = $rateLimitData->limit_long; + $reset = with(new Date($rateLimitData->expires_long))->toUnix(); + } + + // Always 0 if exceeded + $remaining = 0; + } + + // Add rate limit headers + $response->headers->set('X-RateLimit-Limit', $limit); + $response->headers->set('X-RateLimit-Remaining', $remaining); + $response->headers->set('X-RateLimit-Reset', $reset); + + // Return response + return $response; + } +} diff --git a/core/libraries/Hubzero/Api/RateLimit/RateLimiter.php b/core/libraries/Hubzero/Api/RateLimit/RateLimiter.php new file mode 100644 index 00000000000..8ce79416f5f --- /dev/null +++ b/core/libraries/Hubzero/Api/RateLimit/RateLimiter.php @@ -0,0 +1,150 @@ +storage = $storage; + $this->config = array_merge([ + 'short' => [ + 'period' => 1, // 1 minute + 'limit' => 120 // 120 requests + ], + 'long' => [ + 'period' => 1440, // 1 day (in minutes) + 'limit' => 10000 // 10,000 requests + ] + ], $config); + } + + /** + * Rate limit for application & user + * + * @param int $applicationId Application identifier + * @param int $userId User identifier + * @return array Array of rate limit data + */ + public function rateLimit($applicationId, $userId) + { + // load limit data, creating initial record if doesnt exist + if (!$data = $this->storage->getRateLimitData($applicationId, $userId)) + { + $data = $this->createRateLimitData($applicationId, $userId); + } + + // check if we can reset short expiration + if (time() > with(new Date($data->expires_short))->toUnix()) + { + $newShortDate = $this->getNewExpiresDateString('short'); + $this->storage->resetShort($data->id, 0, $newShortDate); + } + + // check if we can reset long expiration + if (time() > with(new Date($data->expires_long))->toUnix()) + { + $newLongDate = $this->getNewExpiresDateString('long'); + $this->storage->resetLong($data->id, 0, $newLongDate); + } + + // increment data then refetch + $this->storage->incrementRateLimitData($data->id); + + // refetch record after incrementing + $data = $this->storage->getRateLimitData($applicationId, $userId); + + // check to see if were over short or long limits + $data->exceeded_short = false; + $data->exceeded_long = false; + if ($data->count_short >= $data->limit_short) + { + $data->exceeded_short = true; + } + if ($data->count_long >= $data->limit_long) + { + $data->exceeded_long = true; + } + + // return data + return $data; + } + + /** + * Create initial limit data + * + * @param int $applicationId Application identifier + * @param int $userId User identifier + * @return array Array of rate limit data + */ + private function createRateLimitData($applicationId, $userId) + { + // data needed to create record + $ipAddress = \Request::ip(); + $countShort = 0; + $countLong = 0; + $limitShort = $this->config['short']['limit']; + $limitLong = $this->config['long']['limit']; + $created = with(new Date('now'))->toSql(); + $expiresShort = $this->getNewExpiresDateString('short'); + $expiresLong = $this->getNewExpiresDateString('long'); + + // create initial limit record + return $this->storage->createRateLimitData( + $applicationId, + $userId, + $ipAddress, + $limitShort, + $limitLong, + $countShort, + $countLong, + $expiresShort, + $expiresLong, + $created + ); + } + + /** + * Get new expires date string + * + * @param string $type Short or long period + * @return string Date string + */ + private function getNewExpiresDateString($type = 'short') + { + $modifier = $this->config[$type]['period']; + return with(new Date('now'))->modify('+' . $modifier . ' MINUTES')->toSql(); + } +} diff --git a/core/libraries/Hubzero/Api/RateLimit/Storage/Database.php b/core/libraries/Hubzero/Api/RateLimit/Storage/Database.php new file mode 100644 index 00000000000..9167a0d47bc --- /dev/null +++ b/core/libraries/Hubzero/Api/RateLimit/Storage/Database.php @@ -0,0 +1,127 @@ +db = $db; + } + + /** + * Get record by application & user is + * + * @param int $applicationId Application id + * @param int $userId User identifier + * @return void + */ + public function getRateLimitData($applicationId, $userId) + { + $sql = "SELECT * FROM `#__developer_rate_limit` + WHERE `application_id` = " . $this->db->quote($applicationId) . " + AND `uidNumber` = " . $this->db->quote($userId) . " + ORDER BY `created` LIMIT 1"; + $this->db->setQuery($sql); + return $this->db->loadObject(); + } + + /** + * Create initial rate limit record + * + * @param int $applicationId Application id + * @param int $userId User identifier + * @param string $ip IP address + * @param int $limitShort Short limit + * @param int $limitLong Long limit + * @param int $countShort Short count + * @param int $countLong Long count + * @param string $expiresShort Short expiration date string + * @param string $expiresLong Long expiration date string + * @param string $created Created date string + * @return void + */ + public function createRateLimitData($applicationId, $userId, $ip, $limitShort, $limitLong, $countShort, $countLong, $expiresShort, $expiresLong, $created) + { + $sql = "INSERT INTO `#__developer_rate_limit` (`application_id`, `uidNumber`, `ip`, `limit_short`, `limit_long`, `count_short`, `count_long`, `expires_short`, `expires_long`, `created`) + VALUES (" . $this->db->quote($applicationId) . ", " . $this->db->quote($userId) . ", " . $this->db->quote($ip) . ", " . $this->db->quote($limitShort) . ", " . $this->db->quote($limitLong) . ", " . $this->db->quote($countShort) . ", " . $this->db->quote($countLong) . ", " . $this->db->quote($expiresShort) . ", " . $this->db->quote($expiresLong) . ", " . $this->db->quote($created) . ")"; + $this->db->setQuery($sql); + $this->db->query(); + + return $this->getRateLimitData($applicationId, $userId); + } + + /** + * Increment rate limit record + * + * @param int $id Rate limit record id + * @param int $increment Increment amount + * @return void + */ + public function incrementRateLimitData($id, $increment = 1) + { + $sql = "UPDATE `#__developer_rate_limit` + SET `count_short` = `count_short` + " . $this->db->quote($increment) . ", + `count_long` = `count_long` + " . $this->db->quote($increment) . " + WHERE `id` = " . $this->db->quote($id); + $this->db->setQuery($sql); + return $this->db->query(); + } + + /** + * Reset short count & expiration + * + * @param int $id Rate limit record id + * @param int $toCount Reset count + * @param string $toDate Reset date string + * @return void + */ + public function resetShort($id, $toCount, $toDate) + { + $sql = "UPDATE `#__developer_rate_limit` + SET `count_short` = " . $this->db->quote($toCount) . ", + `expires_short` = " . $this->db->quote($toDate) . " + WHERE `id` = " . $this->db->quote($id); + $this->db->setQuery($sql); + return $this->db->query(); + } + + /** + * Reset long count & expiration + * + * @param int $id Rate limit record id + * @param int $toCount Reset count + * @param string $toDate Reset date string + * @return void + */ + public function resetLong($id, $toCount, $toDate) + { + $sql = "UPDATE `#__developer_rate_limit` + SET `count_long` = " . $this->db->quote($toCount) . ", + `expires_long` = " . $this->db->quote($toDate) . " + WHERE `id` = " . $this->db->quote($id); + $this->db->setQuery($sql); + return $this->db->query(); + } +} diff --git a/core/libraries/Hubzero/Api/RateLimit/Storage/StorageInterface.php b/core/libraries/Hubzero/Api/RateLimit/Storage/StorageInterface.php new file mode 100644 index 00000000000..d1adde06be9 --- /dev/null +++ b/core/libraries/Hubzero/Api/RateLimit/Storage/StorageInterface.php @@ -0,0 +1,69 @@ +accepts; + } + + $key = 'HTTP_' . strtoupper($header); + + if (isset($_SERVER[$key])) + { + return $SERVER[$key]; + } + + return null; + } + + /** + * Short description for '__construct' + * + * Long description (if any) ... + * + * @return void + */ + function __construct($options = array()) + { + if (($options == '_SERVER') || (in_array('_SERVER', $options))) + { + $this->set('request', '_SERVER'); + + if (isset($_GET['format'])) + { + $this->accepts = $this->_parse_accept($_GET['format']); + } + else if (isset($_POST['format'])) + { + $this->accepts = $this->_parse_accept($_POST['format']); + } + else if (isset($_SERVER['HTTP_ACCEPT'])) + { + $this->accepts = $_SERVER['HTTP_ACCEPT']; + } + + if (empty($this->accepts)) + { + $format = strrchr($_SERVER['REQUEST_URI'], '.'); + + if (strchr($format, '/') === false) + { + $this->accepts = $this->_parse_accept(substr($format, 1)); + } + } + + if (isset($_GET['suppress_response_codes'])) + { + $this->suppress_response_codes = true; + } + + if (isset($_POST['suppress_response_codes'])) + { + $this->suppress_response_codes = true; + } + + if (isset($_SERVER['HTTP_X_HTTP_SUPPRESS_RESPONSE_CODES'])) + { + $this->suppress_response_codes = true; + } + + if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) + { + $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + } + else + { + if (isset($_SERVER['REQUEST_METHOD'])) + { + $this->method = $_SERVER['REQUEST_METHOD']; + } + else + { + $this->method = ''; + } + } + } + } + + function import($what = array('all'), $where = '_SYSTEM') + { + if ($where != '_SYSTEM') + { + return false; + } + + $what = (array) $what; + + if (in_array('all', $what)) + { + $what = array_merge($what, array('method', 'request', 'version', 'headers', 'body', 'hostname', 'scheme', 'postdata')); + } + + foreach ($what as $item) + { + switch ($item) + { + case 'method': + $this->set('method', $_SERVER['REQUEST_METHOD']); + break; + case 'request': + $this->set('request', $_SERVER['REQUEST_URI']); + break; + case 'version': + $this->set('version', $_SERVER['SERVER_PROTOCOL']); + break; + case 'headers': + $this->set('header', null); + + foreach ($_SERVER as $key => $value) + { + if (strncmp($key, 'HTTP_', 5) == 0) + { + $header = explode('_', strtolower($key)); + array_shift($header); + $header = array_map('ucfirst', $header); + $header = implode('-', $header); + $this->headers[$header] = $value; + } + } + break; + case 'body': + $this->set('body', 'php://input'); + break; + case 'hostname': + $this->set('hostname', $_SERVER['HTTP_HOST']); + break; + case 'scheme': + if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS']) + { + $this->set('scheme', 'https'); + } + else + { + $this->set('scheme', 'http'); + } + case 'postdata': + $this->set('postdata', $_POST); + break; + } + } + + } + + function export($what = 'all', $where = '_SYSTEM') + { + if ($where != '_SYSTEM') + { + return false; + } + + $what = (array) $what; + + if (in_array('all', $what)) + { + $what = array_merge($what, array('uri', 'get', 'method', 'version', 'headers', 'postdata')); + } + + // uri.... fill _SERVER: SCRIPT_*, QUERY_STRING + // method... fill _SERVER: REQUEST_METHOD + // headers... fill _SERVER HTTP_* + // get... fill _GET + // post.... fill _POST + // cookies.. fill _COOKIE + // recompute _REQUEST if _GET or _POST changed + + foreach ($what as $item) + { + switch ($item) + { + case 'version': + $_SERVER['SERVER_PROTOCOL'] = $this->version; + break; + + case 'uri': + $_SERVER['REQUEST_URI'] = '/' . $this->path; + $_SERVER['SCRIPT_NAME'] = '/' . $this->path; + $_SERVER['PHP_SELF'] = '/' . $this->path; + $_SERVER['SCRIPT_URL'] = '/' . $this->path; + + $request = ''; + + if ($this->scheme) + { + $request .= $this->scheme . "://"; + } + + if ($this->username && $this->password) + { + $request .= $this->username . ":" . $this->password . '@'; + } + + $request .= $this->hostname; + + if ($this->port) + { + $request .= ":" . $this->port . "/"; + } + + $request .= $this->path; + + $_SERVER['SCRIPT_URI'] = $request; + + if (!empty($this->query)) + { + $_SERVER['REQUEST_URI'] .= '?'.rawurldecode($this->get('query')); + } + + if ($this->get('scheme') == 'https') + { + $_SERVER['HTTPS'] = 'on'; + } + else + { + unset($_SERVER['HTTPS']); + } + + $_SERVER['QUERY_STRING'] = rawurldecode($this->get('query')); + + break; + case 'method': + $_SERVER['REQUEST_METHOD'] = $this->get('method'); + break; + case 'get': + { + // can add variables to scope, so keep these braces to block out scope + parse_str($this->get('query'), $_GET); + parse_str($this->get('query'), $_REQUEST); // @FIXME: quick hack, will break when _POST support added + } + break; + case 'postdata': + $_POST = $this->get('postdata'); + break; + case 'headers': + foreach ($this->headers as $key => $value) + { + $key = str_replace('-', '_', $key); + $key = "HTTP_" . strtoupper($key); + $_SERVER[$key] = $value; + } + if (!isset($_SERVER['HTTP_HOST'])) + { + $_SERVER['HTTP_HOST'] = $this->get('hostname'); + } + break; + } + } + + $order = ini_get('request_order'); + + if (empty($order)) + { + $order = ini_get('variables_order'); + } + + if (empty($order)) + { + $order = "GP"; + } + + $g = stripos($order, 'g'); + $p = stripos($order, 'p'); + + if ($g < $p) + { + $_REQUEST = $_GET; + + if (!empty($_POST)) + { + foreach ($_POST as $k => $v) + { + $_REQUEST[$k] = $v; + } + } + } + else + { + $_REQUEST = $_POST; + + if (!empty($_GET)) + { + foreach ($_GET as $k => $v) + { + $_REQUEST[$k] = $v; + } + } + } + + $GLOBALS['_REQUEST'] = array(); + } + + /** + * Short description for 'getMethod' + * + * Long description (if any) ... + * + * @return string Return description (if any) ... + */ + function getMethod() + { + return $this->method; + } + + /** + * Short description for 'getSuppressResponseCodes' + * + * Long description (if any) ... + * + * @return boolean Return description (if any) ... + */ + function getSuppressResponseCodes() + { + return $this->suppress_response_codes; + } + + /** + * Short description for '_parse_accept' + * + * Long description (if any) ... + * + * @param unknown $input Parameter description (if any) ... + * @return mixed Return description (if any) ... + */ + function _parse_accept($input) + { + static $_types = array( + 'xml' => 'application/xml', + 'html' => 'text/html', + 'xhtml' => 'application/xhtml+xml', + 'json' => 'application/json', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'plain' => 'text/plain', + 'php_serialized' => 'application/vnd.php.serialized', + 'php' => 'application/php', + ); + + if (isset($_types[$input])) + { + return $_types[$input]; + } + + return ''; + } + + public function get($key, $default = '') + { + switch ($key) + { + case 'version': + return isset($this->version) ? $this->version : $default; + case 'method': + return isset($this->method) ? $this->method : $default; + case 'scheme': + return isset($this->scheme) ? $this->scheme: $default; + case 'username': + return isset($this->username) ? $this->username: $default; + case 'password': + return isset($this->password) ? $this->password: $default; + case 'hostname': + return isset($this->hostname) ? $this->hostname: $default; + case 'port': + return isset($this->port) ? $this->port: $default; + case 'path': + return isset($this->path) ? $this->path: $default; + case 'query': + return isset($this->query) ? $this->query: $default; + case 'fragment': + return isset($this->fragment) ? $this->fragment: $default; + case 'request': // @FIXME: this work should probably be cached + + $request = ''; + + if ($this->scheme) + { + $request .= $this->scheme . "://"; + } + + if ($this->username && $this->password) + { + $request .= $this->username . ":" . $this->password . '@'; + } + + $request .= $this->hostname; + + if ($this->port) + { + $request .= ":" . $this->port . "/"; + } + + $request .= $this->path; + + if ($this->query) + { + $request .= "?" . $this->query; + } + + if ($this->fragment) + { + $request .= "#" . $this->fragment; + } + + return !empty($request) ? $request : $default; + + case 'queryvars': // @FIXME: this work should be cached + { + // can add variables to scope, so keep these braces to block out scope + parse_str($this->get('query'), $queryvars); + + return $queryvars; + } + case 'postdata': + return $this->_post; + case 'sbs': + if (empty($this->method)) + { + return false; + } + + $sbs = oauth_get_sbs($this->method, $this->get('request'), $this->_post); + return $sbs; + + default: + break; + + } + } + + public function setHeader($key, $value) + { + if (empty($value)) + { + unset($this->headers[$key]); + } + else + { + $this->headers[$key] = $value; + } + } + + public function getHeader($key, $default = '') + { + if (isset($this->headers[$key])) + { + return $this->headers[$key]; + } + + return $default; + } + + public function add($property, $key, $value) + { + switch ($property) + { + case 'query': + + if (!empty($this->query)) + { + $this->query .= '&'; + } + + $this->query .= $key . '=' . rawurlencode($value); + + break; + + case 'postdata': + if ($value === null) + { + unset($this->_post[$key]); + } + else + { + $this->_post[$key] = $value; + } + } + } + + public function set($key, $value = null) + { + switch ($key) + { + case 'method': + $this->method = $value; + break; + case 'scheme': + $this->scheme = $value; + break; + case 'username': + $this->username = $value; + break; + case 'password': + $this->password = $value; + break; + case 'hostname': + $this->hostname = $value; + break; + case 'port': + $this->port = $value; + break; + case 'path': + $this->path = $value; + break; + case 'query': + if (is_array($value)) + { + $this->query = ''; + + foreach ($value as $key => $value) + { + if (!empty($this->query)) + { + $this->query .= '&'; + } + + $this->query .= $key . '=' . rawurlencode($value); + } + } + else + { + $this->query = $value; + } + break; + case 'fragment': + $this->fragment = $value; + break; + case 'version': + $this->version = $value; + break; + case 'headers': + $this->headers = $value; + break; + case 'body'; + $this->body = $value; + break; + case 'postdata': + $this->_post = $value; + break; + case 'request': + { + if (is_string($value)) + { + if ($value == '_SERVER') + { + $u = $_SERVER['SCRIPT_URI']; + } + else + { + $u = parse_url($value); + } + + $this->scheme = (isset($u['scheme'])) ? $u['scheme'] : ''; + $this->username = (isset($u['username'])) ? $u['username'] : ''; + $this->password = (isset($u['password'])) ? $u['password'] : ''; + $this->hostname = (isset($u['hostname'])) ? $u['hostname'] : ''; + $this->port = (isset($u['port'])) ? $u['port'] : ''; + $this->path = (isset($u['path'])) ? $u['path'] : ''; + $this->query = (isset($u['query'])) ? $u['query'] : ''; + $this->fragment = (isset($u['fragment'])) ? $u['fragment'] : ''; + } + else + { + $this->scheme = ''; + $this->username = ''; + $this->password = ''; + $this->hostname = ''; + $this->port = ''; + $this->path = ''; + $this->query = ''; + $this->fragment = ''; + } + } + } + } + + public function sign($type = 'oauth', $key = '', $secret1 = '', $secret2 = '', $method = '') + { + if (empty($method)) + { + $method = $this->get('method'); + + if ($method == 'GET') + { + $qkey = 'query'; + } + else + { + $qkey = 'postdata'; + } + } + //$qkey = 'query'; + switch ($type) + { + case 'oauth': + $queryvars = $this->get('queryvars'); + $postvars = $this->get('postdata'); + + if (!isset($queryvars['oauth_nonce']) && !isset($postvars['oauth_nonce'])) + { + $this->add($qkey, 'oauth_nonce', uniqid()); + } + if (!isset($queryvars['oauth_timestamp']) && !isset($postvars['oauth_timestamp'])) + { + $this->add($qkey, 'oauth_timestamp', time()); + } + if (!isset($queryvars['oauth_token']) && !isset($postvars['oauth_token'])) + { + $this->add($qkey, 'oauth_token', ''); + } + if (!isset($queryvars['oauth_consumer_key']) && !isset($postvars['oauth_consumer_key'])) + { + $this->add($qkey, 'oauth_consumer_key', oauth_urlencode($key)); + } + if (!isset($queryvars['oauth_signature_method']) && !isset($postvars['oauth_signature_method'])) + { + $this->add($qkey, 'oauth_signature_method', 'HMAC-SHA1'); + } + if (!isset($queryvars['oauth_version']) && !isset($postvars['oauth_version'])) + { + $this->add($qkey, 'oauth_version', '1.0'); + } + if (isset($queryvars['oauth_signature']) || isset($postvars['oauth_signature'])) + { + return false; + } + + $sbs = $this->get('sbs'); + + $secret = (!empty($secret1)) ? oauth_urlencode($secret1) : ''; + $token_secret = (!empty($secret2)) ? oauth_urlencode($secret2) : ''; + $secret = $secret . '&' . $token_secret; + + $signature = base64_encode( hash_hmac('sha1', $sbs, $secret, true) ); + + $this->add($qkey, 'oauth_signature', $signature); + + break; + + default: + return false; + } + } +} diff --git a/core/libraries/Hubzero/Api/Response.php b/core/libraries/Hubzero/Api/Response.php new file mode 100644 index 00000000000..39cb27f0240 --- /dev/null +++ b/core/libraries/Hubzero/Api/Response.php @@ -0,0 +1,169 @@ +original = $content; + + $status = $this->getStatusCode(); + $reason = ''; + $output = ''; + + switch ($this->headers->get('content-type')) + { + case 'text/plain': + /*if ($suppress_response_codes) + { + $output .= "Status: $status\n"; + $output .= "Reason: $reason\n"; + $output .= "\n"; + }*/ + + if (!is_object($content) && !is_array($content)) + { + $output .= $content; + } + else + { + $output .= json_encode($content); + } + break; + + case 'text/html': + $reason = htmlspecialchars($reason); + $content = htmlspecialchars($content); + + $output .= "\n"; + $output .= "\n"; + $output .= "\n"; + $output .= "\n"; + $output .= "$status $reason\n"; + $output .= "\n"; + $output .= "\n"; + $output .= '
' . "\n"; + + $output .= '

' . $reason . "

\n"; + + if ($suppress_response_codes) + { + $output .= '

' . htmlspecialchars($status) . "

\n"; + } + + if (!is_object($content) && !is_array($content)) + { + $output .= '

' . $content . "

\n"; + } + else + { + $output .= '

' . json_encode($content) . "

\n"; + } + + $output .= "
\n"; + $output .= "\n"; + $output .= ""; + break; + case 'application/xhtml+xml': + $reason = htmlspecialchars($reason); + $content = htmlspecialchars($content); + + $output .= '' . "\n"; + $output .= '' . "\n"; + $output .= '' . "\n"; + $output .= "\n"; + $output .= "$status $reason\n"; + $output .= "\n"; + $output .= "\n"; + $output .= '
' . "\n"; + + $output .= '

' . $reason . "

\n"; + + if ($suppress_response_codes) + { + $output .= '

' . htmlspecialchars($status) . "

\n"; + } + if (!is_object($content) && !is_array($content)) + { + $output .= '

' . $content . "

\n"; + } + else + { + $output .= '

' . json_encode($content) . "

\n"; + } + + $output .= "
\n"; + $output .= "\n"; + $output .= ""; + break; + case "application/xml": + $output .= Xml::encode($content); + break; + case 'application/json': + $output .= json_encode($content); + break; + case 'application/javascript': + $output .= $content; + break; + case 'application/vnd.php.serialized': + $output .= serialize($content); + break; + case 'application/php': + $output .= var_export($content, true); + break; + case 'application/x-www-form-urlencoded': + if (!is_object($content)) + { + $output .= $content; + } + else + { + $output .= json_encode($content); + } + break; + } + + return parent::setContent($output); + } + + /** + * Sends HTTP headers and content. + * + * @param boolean $flush + * @return object Response + */ + public function send($flush = false) + { + if (!$this->getContent() && $this->original && $this->headers->get('Content-Type')) + { + $this->setContent($this->original); + } + + return parent::send($flush); + } +} diff --git a/core/libraries/Hubzero/Api/Response/DateFormatter.php b/core/libraries/Hubzero/Api/Response/DateFormatter.php new file mode 100644 index 00000000000..ac56c089e72 --- /dev/null +++ b/core/libraries/Hubzero/Api/Response/DateFormatter.php @@ -0,0 +1,100 @@ +next($request); + + // only do on json data + if (!$response->isJson()) + { + return $response; + } + + // get the response content and json decode + $content = json_decode($response->getContent()); + + // get date keys from response options + $this->dateKeys = array_merge( + $this->dateKeys, + $response->getTransformKeys('dates') + ); + + // make sure to handle array different then a single object + if (is_array($content)) + { + // loop through each item in array and covert dates + foreach ($content as $key => $value) + { + $content[$key] = $this->convertDateKeysInObjects($value); + } + } + else + { + // convert single object dates + $content = $this->convertDateKeysInObjects($content); + } + + // set the response content to modified content + $response->setContent(json_encode($content)); + + // return response + return $response; + } + + /** + * Convert dates from SQL format to ISO 8601 + * + * @param mixed $object Convert date keys + * @return mixed Converted object + */ + private function convertDateKeysInObjects($object) + { + // only hanlde objects + if (!is_object($object)) + { + return $object; + } + + // spin over each key replacing the date with new format + foreach (array_keys(get_object_vars($object)) as $key) + { + if (in_array($key, $this->dateKeys)) + { + $object->$key = with(new Date($object->$key))->format('Y-m-d\TH:i:s\Z'); + } + } + + // return object + return $object; + } +} diff --git a/core/libraries/Hubzero/Api/Response/JsonpCallable.php b/core/libraries/Hubzero/Api/Response/JsonpCallable.php new file mode 100644 index 00000000000..019049c1611 --- /dev/null +++ b/core/libraries/Hubzero/Api/Response/JsonpCallable.php @@ -0,0 +1,41 @@ +next($request); + + // check for presence of callback param + // if we have one lets replace response content with a function executing the + // current response content + if ($callback = $request->getVar('callback', null)) + { + $response->headers->set('content-type', 'application/javascript'); + $response->setContent(sprintf('%s(%s);', $callback, $response->getContent())); + } + + // return response + return $response; + } +} diff --git a/core/libraries/Hubzero/Api/Response/ObjectExpander.php b/core/libraries/Hubzero/Api/Response/ObjectExpander.php new file mode 100644 index 00000000000..49d4b46a134 --- /dev/null +++ b/core/libraries/Hubzero/Api/Response/ObjectExpander.php @@ -0,0 +1,207 @@ + array( + 'action_by', + 'actor_id', + 'addedBy', + 'approved_by', + 'assigned', + 'assigned_to', + 'author', + 'authorid', + 'checked_out', + 'closed_by', + 'commenter', + 'commenter_id', + 'comment_by', + 'created_by', + 'created_by_user', + 'created_user_id', + 'creator_id', + 'editedBy', + 'follower_id', + 'following_id', + 'foreign_key', + 'granted_by', + 'modified_by', + 'modified_user_id', + 'object_id', + 'owned_by_user', + 'posted_by', + 'proposed_by', + 'ran_by', + 'redeemed_by', + 'reviewed_by', + 'sent_by', + 'taggerid', + 'uid', + 'uidNumber', + 'uploaded_by', + 'userid', + 'user_id', + 'user_id_to', + 'user_id_from', + 'voter' + ), + 'group' => array( + 'groupid', + 'group_id', + 'gidNumber' + ) + ); + + /** + * Handle request in HTTP stack + * + * @param objct $request HTTP Request + * @return mixes + */ + public function handle(Request $request) + { + // execute response + $response = $this->next($request); + + // only do this if user wants it expanded + if (!$expand = $request->getVar('expand', null)) + { + return $response; + } + + // normalize keys + $expandKeys = $this->normalizeExpandKeys($expand); + + // only do on json data + if (!$response->isJson()) + { + return $response; + } + + // get the response content and json decode + $content = json_decode($response->getContent()); + + // make sure to handle array different then a single object + if (is_array($content)) + { + // loop through each item in array and covert dates + foreach ($content as $key => $value) + { + $content[$key] = $this->convertExpandKeysInObjects($expandKeys, $value); + } + } + else + { + // convert single object dates + $content = $this->convertExpandKeysInObjects($expandKeys, $content); + } + + // set the response content to modified content + $response->setContent(json_encode($content)); + + // return response + return $response; + } + + /** + * Normalize expand keys + * + * @param array $expandKeys Raw expand keys + * @return array Normalized expand keys + */ + private function normalizeExpandKeys($expandKeys) + { + // clean up expand keys + $expandKeys = array_map('trim', explode(',', $expandKeys)); + $normalized = []; + + foreach ($this->acceptedKeys as $type => $acceptedKeys) + { + foreach ($expandKeys as $expandKey) + { + if (in_array($expandKey, $acceptedKeys)) + { + $normalized[$expandKey] = $type; + } + } + } + + return $normalized; + } + + /** + * Convert keys in object + * + * @param array $expandKeys + * @param mixed $object + * @return object + */ + private function convertExpandKeysInObjects($expandKeys, $object) + { + // only hanlde objects + if (!is_object($object)) + { + return $object; + } + + // spin over each key replacing the date with new format + foreach (array_keys(get_object_vars($object)) as $key) + { + if (array_key_exists($key, $expandKeys)) + { + $func = $expandKeys[$key] . 'Expander'; + if (method_exists($this, $func)) + { + $object->$key = $this->$func($object->$key); + } + } + } + + // return object + return $object; + } + + /** + * Function to return profile object + * + * @param integer $user_id User identifier + * @return object User object + */ + private function profileExpander($user_id) + { + return User::oneOrNew($user_id); + } + + /** + * Function to return group object + * + * @param integer $gidNumber Group identifier + * @return object Group object + */ + private function groupExpander($gidNumber) + { + return Group::getInstance($gidNumber); + } +} diff --git a/core/libraries/Hubzero/Api/Response/UriBase.php b/core/libraries/Hubzero/Api/Response/UriBase.php new file mode 100644 index 00000000000..9212a690a54 --- /dev/null +++ b/core/libraries/Hubzero/Api/Response/UriBase.php @@ -0,0 +1,103 @@ +next($request); + + if ($content = $response->original) + { + $content = $this->traverse($request, $content); + + $response->setContent($content); + } + + return $response; + } + + /** + * Look for keys in data and convert found values + * + * @param object $request + * @param mixed $data + * @return mixed + */ + private function traverse($request, $data) + { + if (is_array($data)) + { + foreach ($data as $key => $value) + { + $data[$key] = $this->convert($request, $key, $value); + } + } + else if (is_object($data)) + { + foreach (array_keys(get_object_vars($data)) as $key) + { + $data->$key = $this->convert($request, $key, $data->$key); + } + } + + return $data; + } + + /** + * Convert a URI + * + * @param object $request + * @param string $key + * @param mixed $value + * @return mixed + */ + private function convert($request, $key, $value) + { + if (is_array($value) || is_object($value)) + { + return $this->traverse($request, $value); + } + + if (!in_array($key, $this->keys, true)) + { + return $value; + } + + if (substr($value, 0, 4) == 'http') + { + return $value; + } + + return rtrim(str_replace('/api', '', $request->root()), '/') . '/' . ltrim($value, '/'); + } +} diff --git a/core/libraries/Hubzero/Api/Response/Xml.php b/core/libraries/Hubzero/Api/Response/Xml.php new file mode 100644 index 00000000000..f3e8ffaa3e6 --- /dev/null +++ b/core/libraries/Hubzero/Api/Response/Xml.php @@ -0,0 +1,373 @@ +'; + + $indent = ($depth > 1) ? str_repeat("\t", $depth-1) : ''; + + $xml = ($show_declaration) ? $indent . $declaration . "\n" : ''; + + $element_type = gettype($mixed); + + if (is_array($mixed)) + { + $i = 0; + + foreach ($mixed as $key => $value) + { + if ($key !== $i) + { + $element_type = 'a-array'; + break; + } + + ++$i; + } + } + + $sp = (!empty($attributes)) ? ' ' : ''; + + if (in_array($element_type, array('object', 'a-array'))) + { + $xml_element_type = 'object'; + } + else if (in_array($element_type, array('double', 'integer'))) + { + $xml_element_type = 'number'; + } + else if (in_array($element_type, array('array', 'boolean', 'string'))) + { + $xml_element_type = $element_type; + } + else if ($element_type == 'NULL') + { + $xml_element_type = 'null'; + } + else + { + $xml_element_type = ''; + } + + if (!empty($xml_element_type)) + { + $attributes .= $sp . 'type="' . $xml_element_type . '"'; + } + + $sp = (!empty($attributes)) ? ' ' : ''; + + $xml .= $indent . '<' . $tag . $sp . $attributes . '>'; + + if (is_array($mixed) || is_object($mixed)) + { + foreach ($mixed as $key => $value) + { + if (!isset($first)) + { + $xml .= "\n"; + $first = true; + } + + $value_type = gettype($value); + + if (is_array($value)) + { + $i = 0; + + foreach ($value as $vkey => $vvalue) + { + if ($vkey !== $i) + { + $value_type = 'a-array'; + break; + } + + ++$i; + } + } + + $vattributes = ''; + + if ($element_type == 'array') + { + $vname = 'item'; + } + else if ($element_type == 'a-array') + { + $vname = 'a:item'; + $vattributes = 'xmlns:a="item" item="' . $key . '"'; + } + else + { + $vname = $key; + } + + $xml .= self::encode($value, $vname, $vattributes, $depth+1, false); + } + + $xml .= $indent; + } + else + { + if (is_bool($mixed)) + { + $xml .= ($mixed) ? 'true' : 'false'; + } + else if (is_null($mixed)) + { + $xml .= ''; + } + else + { + $xml .= htmlspecialchars($mixed, ENT_QUOTES); + } + } + + $xml .= ""; + + if ($depth > 1) + { + $xml .= "\n"; + } + + self::last_error(self::XML_ERROR_NONE); + + return $xml; + } + + /** + * Set the last error + * + * @param integer $id Error number + * @return integer + */ + public static function last_error($id) + { + static $last_error = 0; + + if (isset($id)) + { + $last_error = $id; + } + + return $last_error; + } + + /** + * Parse XML and return as array or object + * + * @param string $xml XML + * @return mixed Array or object + */ + public static function decode($xml) + { + $p = xml_parser_create(); + + xml_parser_set_option($p, self::XML_OPTION_CASE_FOLDING, 0); + xml_parser_set_option($p, self::XML_OPTION_SKIP_WHITE, 1); + xml_parse_into_struct($p, $xml, $vals, $index); + xml_parser_free($p); + + $count = count($vals); + + $stack = array(); + + $obj = null; + + for ($i = 0; $i < $count; $i++) + { + $v = $vals[$i]; + + if ($v['type'] == 'open') + { + if ($v['attributes']['type'] == 'object') + { + if (is_null($obj)) + { + array_push($stack, array(null,$obj)); + } + else if (is_array($obj) || is_object($obj)) + { + array_push($stack, array($v['tag'],$obj)); + } + else + { + if ($fatal) + { + die('invalid container'); + } + else + { + return false; + } + } + + $obj = new \stdClass(); + } + else if ($v['attributes']['type'] == 'array') + { + if (is_null($obj)) + { + array_push($stack, array(null,$obj)); + } + else if (is_array($obj) || is_object($obj)) + { + array_push($stack, array($v['tag'],$obj)); + } + else + { + die('invalid container'); + } + + $obj = array(); + } + else + { + die('invalid element'); + } + } + else if ($v['type'] == 'complete') + { + if ($v['attributes']['type'] == 'string') + { + $value = $v['value']; + } + else if ($v['attributes']['type'] == 'boolean') + { + $value = $v['value'] == 'true'; + } + else if ($v['attributes']['type'] == 'number') + { + $value = $v['value']; + + if (!is_numeric($value)) + { + die('invalid numeric value'); + } + + $value = $value + 0; // converts to type int or float as required + } + else if ($v['attributes']['type'] == 'null') + { + $value = null; + } + else if ($v['attributes']['type'] == 'array') + { + $value = array(); + } + else if ($v['attributes']['type'] == 'object') + { + $value = new \stdClass(); + } + else + { + die('invalid value type'); + } + + if (is_null($obj)) + { + $obj = $value; + } + else if (is_array($obj)) + { + $obj[] = $value; + } + else if (is_object($obj)) + { + if (isset($v['attributes']['item'])) + { + $obj->{$v['attributes']['item']} = $value; + } + else + { + $obj->{$v['tag']} = $value; + } + } + else + { + die('invalid container 2'); + } + } + else if ($v['type'] == 'close') + { + $prev = array_pop($stack); + + if (is_object($prev[1])) + { + $prev[1]->{$prev[0]} = $obj; + } + else if (is_array($prev[1])) + { + $prev[1][$prev[0]] = $obj; + } + else if (is_null($prev[1])) + { + break; + } + else + { + die('invalid container in stack'); + } + + $obj = $prev[1]; + } + else + { + die('unknown parse part'); + } + + } + + self::last_error(self::XML_ERROR_NONE); + + return $obj; + } +} diff --git a/core/libraries/Hubzero/Api/ResponseServiceProvider.php b/core/libraries/Hubzero/Api/ResponseServiceProvider.php new file mode 100644 index 00000000000..7d22448e27d --- /dev/null +++ b/core/libraries/Hubzero/Api/ResponseServiceProvider.php @@ -0,0 +1,70 @@ +app->forget('response'); + + $this->app['response'] = function($app) + { + return new Response(); + }; + } + + /** + * Force debugging off. + * + * @return void + */ + public function boot() + { + if (function_exists('xdebug_disable')) + { + xdebug_disable(); + } + ini_set('zlib.output_compression', '0'); + ini_set('output_handler', ''); + ini_set('implicit_flush', '0'); + + $this->app['config']->set('debug', 0); + $this->app['config']->set('debug_lang', 0); + + static $types = array( + 'xml' => 'application/xml', + 'html' => 'text/html', + 'xhtml' => 'application/xhtml+xml', + 'json' => 'application/json', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'plain' => 'text/plain', + 'php' => 'application/php', + 'php_serialized' => 'application/vnd.php.serialized' + ); + + $format = $this->app['request']->getWord('format', 'json'); + $format = (isset($types[$format]) ? $format : 'json'); + + $this->app['response']->setStatusCode(404); + $this->app['response']->headers->addCacheControlDirective('no-store', true); + $this->app['response']->headers->addCacheControlDirective('must-revalidate', true); + $this->app['response']->headers->set('Content-Type', $types[$format]); + } +} diff --git a/core/libraries/Hubzero/Auth/Domain.php b/core/libraries/Hubzero/Auth/Domain.php new file mode 100644 index 00000000000..178efab2e7f --- /dev/null +++ b/core/libraries/Hubzero/Auth/Domain.php @@ -0,0 +1,289 @@ + 'notempty' + ); + + /** + * Automatically fillable fields + * + * @var array + **/ + public $always = array( + 'type' + ); + + /** + * Generates automatic authenticator field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticAuthenticator($data) + { + $alias = $data['authenticator']; + $alias = strip_tags($alias); + $alias = trim($alias); + if (strlen($alias) > 255) + { + $alias = substr($alias . ' ', 0, 255); + $alias = substr($alias, 0, strrpos($alias, ' ')); + } + + return preg_replace("/[^a-zA-Z0-9]/", '', strtolower($alias)); + } + + /** + * Generates automatic modified field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticType($data) + { + return (isset($data['type']) && $data['type'] ? $data['type'] : 'authentication'); + } + + /** + * Get associated links + * + * @return object + */ + public function links() + { + return $this->oneToMany(__NAMESPACE__ . '\\Link', 'auth_domain_id'); + } + + /** + * Read a record + * + * @return boolean True on success, False on failure + */ + public function read() + { + if ($this->get('id')) + { + $row = self::oneOrNew($this->get('id')); + } + else + { + $row = self::all() + ->whereEquals('type', $this->get('type')) + ->whereEquals('authenticator', $this->get('authenticator')) + ->whereEquals('domain', $this->get('domain')) + ->row(); + } + + if (!$row || !$row->get('id')) + { + return false; + } + + foreach (array_keys($this->getAttributes()) as $key) + { + $this->set($key, $row->get($key)); + } + + return true; + } + + /** + * Create a record + * + * @return boolean True on success, False on failure + */ + public function create() + { + return $this->save(); + } + + /** + * Update a record + * + * @param boolean $all Update all properties? + * @return boolean + */ + public function update($all = false) + { + return $this->save(); + } + + /** + * Delete a record + * + * @return boolean + */ + public function delete() + { + return $this->destroy(); + } + + /** + * Get a Domain instance + * + * @param string $type + * @param string $authenticator + * @param string $domain + * @return mixed Object on success, False on failure + */ + public static function getInstance($type, $authenticator, $domain) + { + $query = self::all() + ->whereEquals('type', $type) + ->whereEquals('authenticator', $authenticator); + if ($domain) + { + $query->whereEquals('domain', $domain); + } + $row = $query->row(); + + if (!$row || !$row->get('id')) + { + return false; + } + + return $row; + } + + /** + * Create a new instance and return it + * + * @param string $type + * @param string $authenticator + * @param string $domain + * @return mixed + */ + public static function createInstance($type, $authenticator, $domain = null) + { + if (empty($type) || empty($authenticator)) + { + return false; + } + + $row = self::blank(); + $row->set('type', $type); + $row->set('authenticator', $authenticator); + if ($domain) + { + $row->set('domain', $domain); + } + $row->save(); + + if (!$row->get('id')) + { + return false; + } + + return $row; + } + + /** + * Find a record by ID + * + * @param integer $id + * @return mixed Object on success, False on failure + */ + public static function find_by_id($id) + { + $hzad = self::oneOrNew($id); + + if (!$hzad->get('authenticator')) + { + return false; + } + + return $hzad; + } + + /** + * Fine a specific record, or create it + * if not found + * + * @param string $type + * @param string $authenticator + * @param string $domain + * @return mixed + */ + public static function find_or_create($type, $authenticator, $domain=null) + { + $query = self::all() + ->whereEquals('type', $type) + ->whereEquals('authenticator', $authenticator); + if ($domain) + { + $query->whereEquals('domain', $domain); + } + $row = $query->row(); + + if (!$row || !$row->get('id')) + { + $row = self::blank(); + $row->set('type', $type); + $row->set('authenticator', $authenticator); + if ($domain) + { + $row->set('domain', $domain); + } + $row->save(); + } + + if (!$row->get('id')) + { + return false; + } + + return $row; + } +} diff --git a/core/libraries/Hubzero/Auth/Exception.php b/core/libraries/Hubzero/Auth/Exception.php new file mode 100644 index 00000000000..71380c24b1b --- /dev/null +++ b/core/libraries/Hubzero/Auth/Exception.php @@ -0,0 +1,180 @@ +level = $level; + $this->code = $code; + $this->message = $msg; + + if ($info != null) + { + $this->info = $info; + } + + if ($backtrace && function_exists('debug_backtrace')) + { + $this->backtrace = debug_backtrace(); + + for ($i = count($this->backtrace) - 1; $i >= 0; --$i) + { + ++$i; + if (isset($this->backtrace[$i]['file'])) + { + $this->file = $this->backtrace[$i]['file']; + } + if (isset($this->backtrace[$i]['line'])) + { + $this->line = $this->backtrace[$i]['line']; + } + if (isset($this->backtrace[$i]['class'])) + { + $this->class = $this->backtrace[$i]['class']; + } + if (isset($this->backtrace[$i]['function'])) + { + $this->function = $this->backtrace[$i]['function']; + } + if (isset($this->backtrace[$i]['type'])) + { + $this->type = $this->backtrace[$i]['type']; + } + + $this->args = false; + if (isset($this->backtrace[$i]['args'])) + { + $this->args = $this->backtrace[$i]['args']; + } + break; + } + } + + parent::__construct($msg, (int) $code); + } + + /** + * Returns to error message + * + * @return string Error message + */ + public function __toString() + { + return $this->message; + } + + /** + * Returns to error message + * + * @return string Error message + */ + public function toString() + { + return (string) $this; + } + + /** + * Returns a property of the object or the default value if the property is not set. + * + * @param string $property The name of the property + * @param mixed $default The default value + * @return mixed The value of the property or null + */ + public function get($property, $default = null) + { + if (isset($this->$property)) + { + return $this->$property; + } + + return $default; + } + + /** + * Modifies a property of the object, creating it if it does not already exist. + * + * @param string $property The name of the property + * @param mixed $value The value of the property to set + * @return object + */ + public function set($property, $value = null) + { + $this->$property = $value; + + return $this; + } +} diff --git a/core/libraries/Hubzero/Auth/Factor.php b/core/libraries/Hubzero/Auth/Factor.php new file mode 100644 index 00000000000..9dae8428926 --- /dev/null +++ b/core/libraries/Hubzero/Auth/Factor.php @@ -0,0 +1,40 @@ +whereEquals('user_id', User::get('id')) + ->whereEquals('domain', $domain) + ->row(); + + return ($factor->isNew()) ? false : $factor; + } +} diff --git a/core/libraries/Hubzero/Auth/Guard.php b/core/libraries/Hubzero/Auth/Guard.php new file mode 100644 index 00000000000..e9a324e3b60 --- /dev/null +++ b/core/libraries/Hubzero/Auth/Guard.php @@ -0,0 +1,285 @@ +app = $app; + + $isLoaded = $this->app['plugin']->import('authentication'); + + if (!$isLoaded) + { + $this->app['log.debug']->error($this->app['language']->txt('JLIB_USER_ERROR_AUTHENTICATION_LIBRARIES')); + } + } + + /** + * Get the state of the object + * + * @return mixed The state of the object. + */ + public function getState() + { + return $this->_state; + } + + /** + * Attach an observer object + * + * [!] Based on Joomla's event dispatcher + * This is here purely for compatibility. + * + * @param object $observer An observer object to attach + * @return void + * @todo Update plugins to not need this and remove method + */ + public function attach($observer) + { + if (is_array($observer)) + { + if (!isset($observer['handler']) || !isset($observer['event']) || !is_callable($observer['handler'])) + { + return; + } + + // Make sure we haven't already attached this array as an observer + foreach ($this->_observers as $check) + { + if (is_array($check) && $check['event'] == $observer['event'] && $check['handler'] == $observer['handler']) + { + return; + } + } + + $this->_observers[] = $observer; + end($this->_observers); + $methods = array($observer['event']); + } + else + { + if (!($observer instanceof Guard)) + { + return; + } + + // Make sure we haven't already attached this object as an observer + $class = get_class($observer); + + foreach ($this->_observers as $check) + { + if ($check instanceof $class) + { + return; + } + } + + $this->_observers[] = $observer; + $methods = array_diff(get_class_methods($observer), get_class_methods('Hubzero\\Plugin\\Plugin')); + } + + $key = key($this->_observers); + + foreach ($methods as $method) + { + $method = strtolower($method); + + if (!isset($this->_methods[$method])) + { + $this->_methods[$method] = array(); + } + + $this->_methods[$method][] = $key; + } + } + + /** + * Detach an observer object + * + * [!] Based on Joomla's event dispatcher + * This is here purely for compatibility. + * + * @param object $observer An observer object to detach. + * @return boolean True if the observer object was detached. + * @todo Update plugins to not need this and remove method + */ + public function detach($observer) + { + // Initialise variables. + $retval = false; + + $key = array_search($observer, $this->_observers); + + if ($key !== false) + { + unset($this->_observers[$key]); + $retval = true; + + foreach ($this->_methods as &$method) + { + $k = array_search($key, $method); + + if ($k !== false) + { + unset($method[$k]); + } + } + } + + return $retval; + } + + /** + * Finds out if a set of login credentials are valid by asking all observing + * objects to run their respective authentication routines. + * + * @param array $credentials Array holding the user credentials. + * @param array $options Array holding user options. + * @return object Response object with status variable filled in for last plugin or first successful plugin. + */ + public function authenticate($credentials, $options = array()) + { + // Get plugins + $plugins = $this->app['plugin']->byType('authentication'); + + // Create authentication response + $response = new Response; + + // Track whether or not we have a valid plugin matching the requested auth type + $match = false; + + // Loop through the plugins and check of the credentials can be used to authenticate + // the user + // + // Any errors raised in the plugin should be returned via the Response + // and handled appropriately. + foreach ($plugins as $plugin) + { + if (!empty($options['authenticator']) && ($plugin->name != $options['authenticator'])) + { + continue; + } + + $className = 'plg' . $plugin->type . $plugin->name; + if (class_exists($className)) + { + $plugin = new $className($this, (array) $plugin); + } + else + { + // Bail here if the plugin can't be created + $this->app['log.debug']->error($this->app['language']->txts('JLIB_USER_ERROR_AUTHENTICATION_FAILED_LOAD_PLUGIN', $className)); + continue; + } + + $client = $this->app['client']->alias . '_login'; + + // Make sure plugin is enabled for a given client + if (!$plugin->params->get($client, false)) + { + continue; + } + + // At this point, we'll consider this a match + $match = true; + + // Try to authenticate + $plugin->onUserAuthenticate($credentials, $options, $response); + + // If authentication is successful break out of the loop + if ($response->status === Status::SUCCESS) + { + if (empty($response->type)) + { + $response->type = isset($plugin->_name) ? $plugin->_name : $plugin->name; + } + break; + } + } + + // If we didn't get a match at all, set a somewhat meaningful error + if (!$match) + { + $response->error_message = 'Invalid authenticator'; + } + + if (empty($response->username)) + { + $response->username = $credentials['username']; + } + + if (empty($response->fullname)) + { + $response->fullname = $credentials['username']; + } + + if (empty($response->password)) + { + $response->password = $credentials['password']; + } + + return $response; + } + + /** + * Authorises that a particular user should be able to login + * + * @param object $response response including username of the user to authorise + * @param array $options list of options + * @return array Results of authorisation + */ + public function authorise($response, $options = array()) + { + // Get plugins in case they haven't been loaded already + $this->app['plugin']->byType('user'); + $this->app['plugin']->byType('authentication'); + + return $this->app['dispatcher']->trigger('onUserAuthorisation', array($response, $options)); + } +} diff --git a/core/libraries/Hubzero/Auth/Link.php b/core/libraries/Hubzero/Auth/Link.php new file mode 100644 index 00000000000..d9829992752 --- /dev/null +++ b/core/libraries/Hubzero/Auth/Link.php @@ -0,0 +1,438 @@ + 'positive|nonzero', + 'username' => 'notempty' + ); + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function user() + { + return $this->belongsToOne('Hubzero\User\User', 'user_id'); + } + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function domain() + { + return $this->belongsToOne(__NAMESPACE__ . '\\Domain', 'auth_domain_id'); + } + + /** + * Get associated data + * + * @return object + */ + public function data() + { + return $this->oneToMany(__NAMESPACE__ . '\\Link\\Data', 'link_id'); + } + + /** + * Read a record + * + * @return boolean True on success, False on failure + */ + public function read() + { + if ($this->get('id')) + { + $row = self::oneOrNew($this->get('id')); + } + elseif ($this->get('user_id')) + { + $row = self::all() + ->whereEquals('auth_domain_id', $this->get('auth_domain_id')) + ->whereEquals('user_id', $this->get('user_id')) + ->row(); + } + elseif ($this->get('username')) + { + $row = self::all() + ->whereEquals('auth_domain_id', $this->get('auth_domain_id')) + ->whereEquals('username', $this->get('username')) + ->row(); + } + + if (!$row || !$row->get('id')) + { + return false; + } + + foreach (array_keys($this->getAttributes()) as $key) + { + $this->set($key, $row->get($key)); + } + + return true; + } + + /** + * Create a record + * + * @return boolean True on success, False on failure + */ + public function create() + { + return $this->save(); + } + + /** + * Update a record + * + * @param boolean $all Update all properties? + * @return boolean + */ + public function update($all = false) + { + return $this->save(); + } + + /** + * Delete a record + * + * @return boolean + */ + public function delete() + { + return $this->destroy(); + } + + /** + * Get an instance of a record + * + * @param integer $auth_domain_id + * @param string $username + * @return mixed Object on success, False on failure + */ + public static function getInstance($auth_domain_id, $username) + { + $row = self::all() + ->whereEquals('auth_domain_id', $auth_domain_id) + ->whereEquals('username', $username) + ->row(); + + if (!$row || !$row->get('id')) + { + return false; + } + + return $row; + } + + /** + * Create a new instance and return it + * + * @param integer $auth_domain_id + * @param string $username + * @return mixed + */ + public static function createInstance($auth_domain_id, $username) + { + if (empty($auth_domain_id) || empty($username)) + { + return false; + } + + $row = self::blank(); + $row->set('auth_domain_id', $auth_domain_id); + $row->set('username', $username); + $row->save(); + + if (!$row->get('id')) + { + return false; + } + + return $row; + } + + /** + * Find existing auth_link entry, return false if none exists + * + * @param string $type + * @param string $authenticator + * @param string $domain + * @param string $username + * @return mixed object on success and false on failure + */ + public static function find($type, $authenticator, $domain, $username) + { + $hzad = Domain::find_or_create($type, $authenticator, $domain); + + if (!is_object($hzad)) + { + return false; + } + + if (empty($username)) + { + return false; + } + + $row = self::all() + ->whereEquals('auth_domain_id', $hzad->get('id')) + ->whereEquals('username', $username) + ->row(); + + if (!$row || !$row->get('id')) + { + return false; + } + + return $row; + } + + /** + * Find a record by ID + * + * @param integer $id + * @return mixed Object on success, False on failure + */ + public static function find_by_id($id) + { + $row = self::oneOrNew($id); + + if (!$row->get('id')) + { + return false; + } + + return $row; + } + + /** + * Find a record, creating it if not found. + * + * @param string $type + * @param string $authenticator + * @param string $domain + * @param string $username + * @return mixed Object on success, False on failure + */ + public static function find_or_create($type, $authenticator, $domain, $username) + { + $hzad = Domain::find_or_create($type, $authenticator, $domain); + + if (!$hzad) + { + return false; + } + + if (empty($username)) + { + return false; + } + + $row = self::all() + ->whereEquals('auth_domain_id', $hzad->get('id')) + ->whereEquals('username', $username) + ->row(); + + if (!$row || !$row->get('id')) + { + $row = self::blank(); + $row->set('auth_domain_id', $hzad->get('id')); + $row->set('username', $username); + $row->save(); + } + + if (!$row->get('id')) + { + return false; + } + + return $row; + } + + /** + * Return array of linked accounts associated with a given user id + * Also include auth domain name for easy display of domain name + * + * @param integer $user_id ID of user to return accounts for + * @return array Array of auth link entries for the given user_id + */ + public static function find_by_user_id($user_id = null) + { + if (empty($user_id)) + { + return false; + } + + $l = self::blank()->getTableName(); + $d = Domain::blank()->getTableName(); + + $results = self::all() + ->select($l . '.*') + ->select($d . '.authenticator', 'auth_domain_name') + ->join($d, $d . '.id', $l . '.auth_domain_id', 'inner') + ->whereEquals($l . '.user_id', $user_id) + ->rows(); + + if (empty($results)) + { + return false; + } + + return $results->toArray(); + } + + /** + * Find trusted emails by User ID + * + * @param integer $user_id USer ID + * @return mixed + */ + public static function find_trusted_emails($user_id) + { + if (empty($user_id) || !is_numeric($user_id)) + { + return false; + } + + $results = self::all() + ->whereEquals('user_id', $user_id) + ->rows() + ->fieldsByKey('email'); + + if (empty($results)) + { + return false; + } + + return $results; + } + + /** + * Delete a record by User ID + * + * @param integer $user_id User ID + * @return boolean + */ + public static function delete_by_user_id($user_id = null) + { + if (empty($user_id)) + { + return true; + } + + $results = self::all() + ->whereEquals('user_id', $user_id) + ->rows(); + + foreach ($results as $result) + { + if (!$result->destroy()) + { + return false; + } + } + + return true; + } + + /** + * Return array of linked accounts associated with a given email address + * Also include auth domain name for easy display of domain name + * + * @param string $email + * @param array $exclude + * @return mixed + */ + public static function find_by_email($email, $exclude = array()) + { + if (empty($email)) + { + return false; + } + + $query = self::all() + ->whereEquals('email', $email); + + if (!empty($exclude[0])) + { + foreach ($exclude as $e) + { + $query->where('auth_domain_id', '!=', $e); + } + } + + $rows = $query->rows(); + + $results = array(); + + foreach ($rows as $row) + { + $result = $row->toArray(); + $result['auth_domain_name'] = $row->domain->get('authenticator'); + + $results[] = $result; + } + + if (empty($results)) + { + return false; + } + + return $results; + } +} diff --git a/core/libraries/Hubzero/Auth/Link/Data.php b/core/libraries/Hubzero/Auth/Link/Data.php new file mode 100644 index 00000000000..ed5262049fd --- /dev/null +++ b/core/libraries/Hubzero/Auth/Link/Data.php @@ -0,0 +1,111 @@ + 'positive|nonzero', + 'domain_key' => 'notempty' + ); + + /** + * Automatically fillable fields + * + * @var array + **/ + public $always = array( + 'modified' + ); + + /** + * Generates automatic modified field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticModified($data) + { + return (isset($data['modified']) && $data['modified'] ? $data['modified'] : Date::of('now')->toSql()); + } + + /** + * Defines a belongs to one relationship between entry and Link + * + * @return object + */ + public function link() + { + return $this->belongsToOne('Hubzero\Auth\Link', 'link_id'); + } + + /** + * Get an instance of a record + * + * @param integer $link_id + * @param string $domain_key + * @return mixed Object on success, False on failure + */ + public static function oneByLinkAndKey($link_id, $domain_key) + { + $row = self::all() + ->whereEquals('link_id', $link_id) + ->whereEquals('domain_key', $domain_key) + ->row(); + + if (!$row || !$row->get('id')) + { + $row = self::blank(); + } + + return $row; + } +} diff --git a/core/libraries/Hubzero/Auth/Manager.php b/core/libraries/Hubzero/Auth/Manager.php new file mode 100644 index 00000000000..b9df09a570b --- /dev/null +++ b/core/libraries/Hubzero/Auth/Manager.php @@ -0,0 +1,196 @@ +app = $app; + } + + /** + * Login authentication function. + * + * Username and encoded password are passed the onUserLogin event which + * is responsible for the user validation. A successful validation updates + * the current session record with the user's details. + * + * Username and encoded password are sent as credentials (along with other + * possibilities) to each observer (authentication plugin) for user + * validation. Successful validation will update the current session with + * the user details. + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * @return boolean True on success. + */ + public function login($credentials, $options = array()) + { + $guard = new Guard($this->app); + + $response = $guard->authenticate($credentials, $options); + + if ($response->status === Status::SUCCESS) + { + // validate that the user should be able to login (different to being authenticated) + // this permits authentication plugins blocking the user + $authorisations = $guard->authorise($response, $options); + + $denied_states = array( + Status::EXPIRED, + Status::DENIED + ); + + foreach ($authorisations as $authorisation) + { + if (in_array($authorisation->status, $denied_states)) + { + // Trigger onUserAuthorisationFailure Event. + $this->app['dispatcher']->trigger('user.onUserAuthorisationFailure', array((array) $authorisation)); + + // If silent is set, just return false. + if (isset($options['silent']) && $options['silent']) + { + return false; + } + + // Return the error. + switch ($authorisation->status) + { + case Status::EXPIRED: + return new Exception($this->app['language']->txt('JLIB_LOGIN_EXPIRED'), 102002, E_WARNING); + break; + case Status::DENIED: + return new Exception($this->app['language']->txt('JLIB_LOGIN_DENIED'), 102003, E_WARNING); + break; + default: + return new Exception($this->app['language']->txt('JLIB_LOGIN_AUTHORISATION'), 102004, E_WARNING); + break; + } + } + } + + // OK, the credentials are authenticated and user is authorised. Lets fire the onLogin event. + $results = $this->app['dispatcher']->trigger('user.onUserLogin', array((array) $response, $options)); + + // If any of the user plugins did not successfully complete the login routine + // then the whole method fails. + // + // Any errors raised should be done in the plugin as this provides the ability + // to provide much more information about why the routine may have failed. + if (!in_array(false, $results, true)) + { + // Set the remember me cookie if enabled. + if (isset($options['remember']) && $options['remember']) + { + // Create the encryption key, apply extra hardening using the user agent string. + $privateKey = $this->app->hash(@$_SERVER['HTTP_USER_AGENT']); + $crypt = new \Hubzero\Encryption\Encrypter( + new \Hubzero\Encryption\Cipher\Simple, + new \Hubzero\Encryption\Key('simple', $privateKey, $privateKey) + ); + $rcookie = $crypt->encrypt(json_encode($credentials)); + $lifetime = time() + 365 * 24 * 60 * 60; + + // Use domain and path set in config for cookie if it exists. + $cookie_domain = $this->app['config']->get('cookie_domain', ''); + $cookie_path = $this->app['config']->get('cookie_path', '/'); + + // Check for SSL connection + $secure = ((isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on')) || getenv('SSL_PROTOCOL_VERSION')); + + setcookie($this->app->hash('JLOGIN_REMEMBER'), $rcookie, $lifetime, $cookie_path, $cookie_domain, $secure, true); + } + + return true; + } + } + + // Trigger onUserLoginFailure Event. + $this->app['dispatcher']->trigger('user.onUserLoginFailure', array((array) $response)); + + // If silent is set, just return false. + if (isset($options['silent']) && $options['silent']) + { + return false; + } + + // If status is success, any error will have been raised by the user plugin + if ($response->status !== Status::SUCCESS) + { + return new Exception($response->error_message, 102001, E_WARNING); + } + + return false; + } + + /** + * Logout a user + * + * @param integer $userid + * @param array $options + * @return boolean + */ + public function logout($userid = null, $options = array()) + { + // Get a user object + $user = ($userid ? \User::getInstance($userid) : \User::getInstance()); + + // Build the credentials array. + $parameters['username'] = $user->get('username'); + $parameters['id'] = $user->get('id'); + + // Set clientid in the options array if it hasn't been set already. + if (!isset($options['clientid'])) + { + $options['clientid'] = $this->app['client']->id; + } + + // OK, the credentials are built. Lets fire the onLogout event. + $results = $this->app['dispatcher']->trigger('user.onUserLogout', array($parameters, $options)); + + // Check if any of the plugins failed. If none did, success. + if (!in_array(false, $results, true)) + { + // Use domain and path set in config for cookie if it exists. + $cookie_domain = $this->app['config']->get('cookie_domain', ''); + $cookie_path = $this->app['config']->get('cookie_path', '/'); + + setcookie($this->app->hash('JLOGIN_REMEMBER'), false, time() - 86400, $cookie_path, $cookie_domain); + + return true; + } + + // Trigger onUserLoginFailure Event. + $this->app['dispatcher']->trigger('user.onUserLogoutFailure', array($parameters)); + + return false; + } +} diff --git a/core/libraries/Hubzero/Auth/Response.php b/core/libraries/Hubzero/Auth/Response.php new file mode 100644 index 00000000000..92a543d74be --- /dev/null +++ b/core/libraries/Hubzero/Auth/Response.php @@ -0,0 +1,123 @@ +request_type = $request_type; + } + + /** + * Set credentials + * + * @param object $passportCredentials + * @return void + */ + public function setCredentials($passportCredentials) + { + $this->credentials = $passportCredentials; + + $this->request = new \OAuth($this->credentials->client_id, $this->credentials->client_secret, OAUTH_SIG_METHOD_HMACSHA1, OAUTH_AUTH_TYPE_FORM); + + $params['username'] = $this->credentials->username; + $params['password'] = $this->credentials->password; + $params['client_id'] = $this->credentials->client_id; + $params['client_secret'] = $this->credentials->client_secret; + $params['grant_type'] = 'password'; + $userAgent = $_SERVER['HTTP_USER_AGENT']; + + $this->request->fetch('https://www.openpassport.org/oauth/token', $params, OAUTH_HTTP_METHOD_POST, array('user-agent' => $userAgent)); + + $access = json_decode($this->request->getLastResponse()); + $this->credentials->access_token = $access->access_token; + } + + /** + * Create a new badge + * + * @param array $data badge info. Must have the following: + * $data['Name'] = 'Badge name'; + * $data['Description'] = 'Badge description'; + * $data['CriteriaUrl'] = 'Badge criteria URL'; + * $data['Version'] = 'Version'; + * $data['BadgeImageUrl'] = 'URL of the badge image: square at least 450px x 450px'; + * @return integer Freshly created badge ID + */ + public function createBadge($data) + { + if (!$this->credentialsSet()) + { + throw new Exception('You need to set the credentials first.'); + } + + $data['IssuerId'] = $this->credentials->issuerId; + $data = json_encode($data); + $accessToken = $this->credentials->access_token; + $userAgent = $_SERVER['HTTP_USER_AGENT']; + + $headers = [ + 'Cache-Control: no-cache', + 'Content-Type: application/json', + "Authorization: Bearer $accessToken", + "user-agent: $userAgent" + ]; + + $request = curl_init(); + curl_setopt($request, CURLOPT_HTTPHEADER, $headers); + curl_setopt($request, CURLOPT_URL, 'https://www.openpassport.org/1.0.0/badges'); + curl_setopt($request, CURLOPT_POSTFIELDS, $data); + curl_setopt($request, CURLOPT_POST, 1); + curl_setopt($request, CURLOPT_RETURNTRANSFER, true); + curl_setopt($request, CURLOPT_VERBOSE, true); + + $response = curl_exec($request); + $badge = json_decode($response); + + if (empty($badge->Id) || !$badge->Id) + { + throw new Exception($badge->message); + } + + return $badge->Id; + } + + /** + * Grant badges to users + * + * @param object $badge Badge info: ID, Evidence URL + * @param mixed $users String (for single user) or array (for multiple users) of user email addresses + * @return void + */ + public function grantBadge($badge, $users) + { + if (!$this->credentialsSet()) + { + throw new Exception('You need to set the credentials first.'); + } + + if (!is_array($users)) + { + $users = array($users); + } + + $assertions = array(); + + foreach ($users as $user) + { + $data = array(); + + $data['BadgeId'] = $badge->id; + $data['EvidenceUrl'] = $badge->evidenceUrl; + $data['EmailAddress'] = $user; + $data['ClientId'] = $this->credentials->clientId; + + $assertions[] = $data; + unset($data); + } + + $assertionsData = json_encode($assertions); + + if ($this->request_type == 'oauth' && is_a($this->request, 'oauth')) + { + $this->request->setAuthType(OAUTH_AUTH_TYPE_AUTHORIZATION); + try + { + $this->request->fetch(self::PASSPORT_API_ENDPOINT . "assertions/", $assertionsData, OAUTH_HTTP_METHOD_POST, array('Content-Type' => 'application/json')); + } + catch (Exception $e) + { + throw new Exception('Badge grant request failed.'); + } + + $assertion = json_decode($this->request->getLastResponse()); + } + else if ($this->request_type == 'curl' && get_resource_type($this->request) == 'curl') + { + curl_setopt($this->request, CURLOPT_URL, self::PASSPORT_API_ENDPOINT . "assertions/"); + curl_setopt($this->request, CURLOPT_POSTFIELDS, $assertionsData); + curl_setopt($this->request, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($this->request); + $assertion = json_decode($response); + } + else + { + throw new Exception('Unsupported request type'); + } + + foreach ($assertion as $ass) + { + if (empty($ass->Id) || !$ass->Id) + { + throw new Exception($ass->message); + } + } + } + + /** + * Check if credentials are set + * + * @return bool + */ + private function credentialsSet() + { + if (empty($this->credentials)) + { + return false; + } + + return true; + } + + /** + * Return a URL + * + * @param string $type + * @return bool + */ + public function getUrl($type = 'Claim') + { + switch ($type) + { + case 'Denied': + return self::PASSPORT_DENIED_URL; + break; + + case 'Badges': + return self::PASSPORT_BADGES_URL; + break; + + default: + return self::PASSPORT_CLAIM_URL; + break; + } + } + + /** + * Get assertions by email address + * + * @param mixed $emailAddresses String (for single user) or array (for multiple users) of user email addresses + * @return array + */ + public function getAssertionsByEmailAddress($emailAddresses) + { + if (!$this->credentialsSet()) + { + throw new Exception('You need to set the credentials first.'); + } + + if (!is_array($emailAddresses)) + { + $emailAddresses = array($emailAddresses); + } + + $query_params = implode('%20', $emailAddresses); + $url = self::PASSPORT_API_ENDPOINT . "assertions?emailAddresses=" . $query_params; + + if ($this->request_type == 'oauth' && is_a($this->request, 'oauth')) + { + $this->request->setAuthType(OAUTH_AUTH_TYPE_URI); + try + { + $this->request->fetch($url, null, OAUTH_HTTP_METHOD_GET, array('Content-Type' => 'application/json')); + } + catch (Exception $e) + { + throw new Exception('Assertations by email request failed.'); + } + + $response = json_decode($this->request->getLastResponse()); + } + else if ($this->request_type == 'curl' && get_resource_type($this->request) == 'curl') + { + curl_setopt($this->request, CURLOPT_POST, false); + curl_setopt($this->request, CURLOPT_URL, $url); + curl_setopt($this->request, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($this->request); + $response = json_decode($response); + } + else + { + throw new Exception('Unsupported request type'); + } + + return $response; + } +} diff --git a/core/libraries/Hubzero/Badges/Provider/ProviderInterface.php b/core/libraries/Hubzero/Badges/Provider/ProviderInterface.php new file mode 100644 index 00000000000..521d65a12fd --- /dev/null +++ b/core/libraries/Hubzero/Badges/Provider/ProviderInterface.php @@ -0,0 +1,36 @@ +_provider = new $cls($requestType); + + if (!($this->_provider instanceof ProviderInterface)) + { + throw new InvalidProviderException(\Lang::txt('Invalid badges provider of "%s". Provider must implement ProviderInterface', $provider)); + } + } + + /** + * Get badges provider instance + * + * @return object + */ + public function getProvider() + { + return $this->_provider; + } +} diff --git a/core/libraries/Hubzero/Bank/Account.php b/core/libraries/Hubzero/Bank/Account.php new file mode 100644 index 00000000000..0d65f02f9ef --- /dev/null +++ b/core/libraries/Hubzero/Bank/Account.php @@ -0,0 +1,70 @@ + 'positive|nonzero' + ); + + /** + * Load a record by user ID + * + * @param integer $user_id User ID + * @return object + */ + public static function oneByUserId($user_id) + { + return self::all() + ->whereEquals('uid', (int)$user_id) + ->row(); + } +} diff --git a/core/libraries/Hubzero/Bank/Config.php b/core/libraries/Hubzero/Bank/Config.php new file mode 100644 index 00000000000..249566f86e4 --- /dev/null +++ b/core/libraries/Hubzero/Bank/Config.php @@ -0,0 +1,69 @@ +rows(); + + $config = new Obj; + + foreach ($pc as $p) + { + $config->set($p->get('alias'), $p->get('points')); + } + + return $config; + } +} diff --git a/core/libraries/Hubzero/Bank/MarketHistory.php b/core/libraries/Hubzero/Bank/MarketHistory.php new file mode 100644 index 00000000000..ffc76d2c3e9 --- /dev/null +++ b/core/libraries/Hubzero/Bank/MarketHistory.php @@ -0,0 +1,130 @@ + 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'date' + ); + + /** + * Generates automatic date value + * + * @param array $data the data being saved + * @return string + */ + public function automaticDate($data) + { + if (!isset($data['date'])) + { + $dt = new \Hubzero\Utility\Date('now'); + + $data['date'] = $dt->toSql(); + } + + return $data['date']; + } + + /** + * Get the ID of a record matching the data passed + * + * @param mixed $itemid Integer + * @param string $action Transaction type + * @param string $category Transaction category + * @param string $created Transaction date + * @param string $log Transaction log + * @return integer + */ + public static function getRecord($itemid=0, $action='', $category='', $created='', $log = '') + { + $model = self::all() + ->select('id'); + + if ($category) + { + $model->whereEquals('category', $category); + } + + if ($itemid) + { + $model->whereEquals('itemid', $itemid); + } + + if ($action) + { + $model->whereEquals('action', $action); + } + + if ($created) + { + $model->whereLike('date', $created . '%'); + } + + if ($log) + { + $model->whereEquals('log', $log); + } + + $row = $model->row(); + + return $row->get('id'); + } +} diff --git a/core/libraries/Hubzero/Bank/Teller.php b/core/libraries/Hubzero/Bank/Teller.php new file mode 100644 index 00000000000..1a17cb37fa9 --- /dev/null +++ b/core/libraries/Hubzero/Bank/Teller.php @@ -0,0 +1,355 @@ +uid = $user_id; + $this->balance = 0; + $this->earnings = 0; + $this->credit = 0; + + $BA = Account::oneByUserId($this->uid); + + if ($BA->get('id')) + { + $this->balance = $BA->get('balance'); + $this->earnings = $BA->get('earnings'); + $this->credit = $BA->get('credit'); + } + else + { + // no points are given initially + $this->_saveBalance('creation'); + } + } + + /** + * Get the current balance + * + * @return integer + */ + public function summary() + { + return $this->balance; + } + + /** + * Get the current credit balance + * + * @return integer + */ + public function credit_summary() + { + return $this->credit; + } + + /** + * Add points + * + * @param integer $amount Amount to deposit + * @param string $desc Transaction description + * @param string $cat Transaction category + * @param integer $ref ID of item transaction references + * @return void + */ + public function deposit($amount, $desc='Deposit', $cat, $ref) + { + $amount = $this->_amountCheck($amount); + + if ($this->getError()) + { + echo $this->getError(); + return; + } + + $this->balance += $amount; + $this->earnings += $amount; + + if (!$this->_save('deposit', $amount, $desc, $cat, $ref)) + { + echo $this->getError(); + } + } + + /** + * Withdraw (spend) points + * + * @param number $amount Amount to withdraw + * @param string $desc Transaction description + * @param string $cat Transaction category + * @param integer $ref ID of item transaction references + * @return void + */ + public function withdraw($amount, $desc='Withdraw', $cat, $ref) + { + $amount = $this->_amountCheck($amount); + + if ($this->getError()) + { + echo $this->getError(); + return; + } + + if ($this->_creditCheck($amount)) + { + $this->balance -= $amount; + + if (!$this->_save('withdraw', $amount, $desc, $cat, $ref)) + { + echo $this->getError(); + } + } + else + { + echo $this->getError(); + } + } + + /** + * Set points aside (credit) + * + * @param integer $amount Amount to put on hold + * @param string $desc Transaction description + * @param string $cat Transaction category + * @param integer $ref ID of item transaction references + * @return void + */ + public function hold($amount, $desc='Hold', $cat, $ref) + { + $amount = $this->_amountCheck($amount); + + if ($this->getError()) + { + echo $this->getError(); + return; + } + + // Current order processing workflow (which requires manual order fulfillment on the backend) prevents race + // condition with the check and update below from corrupting user point balance, but if + // but if workflow is ever changed, a table/row level lock would need to be added to this function + // and error code added to deal with multiple orders with insufficient balances. + // + // See https://hubzero.org/support/ticket/234 for details + + if ($this->_creditCheck($amount)) + { + $this->credit += $amount; + + if (!$this->_save('hold', $amount, $desc, $cat, $ref)) + { + echo $this->getError(); + } + } + else + { + echo $this->getError(); + } + } + + /** + * Make credit adjustment + * + * @param integer $amount Amount to credit + * @return void + */ + public function credit_adjustment($amount) + { + $amount = intval($amount); + + $this->credit = ($amount > 0 ? $amount : 0); + + $this->_saveBalance('update'); + } + + /** + * Get a history of transactions + * + * @param integer $limit Number of records to return + * @return array + */ + public function history($limit=20) + { + return Transaction::history($limit, $this->uid); + } + + /** + * Check that they have enough in their account to perform the transaction. + * + * @param number $amount Amount to subtract from balance + * @return boolean True if they have enough credit + */ + public function _creditCheck($amount) + { + $b = $this->balance; + $b -= $amount; + $c = $this->credit; + $ccheck = $b - $c; + + if ($b >= 0 && $ccheck >= 0) + { + return true; + } + + $this->setError('Not enough points in user account to process transaction.'); + return false; + } + + /** + * Check if an amount is greater than 0 + * + * @param integer $amount Amount to check + * @return integer + */ + public function _amountCheck($amount) + { + $amount = intval($amount); + + if ($amount == 0) + { + $this->setError('Cannot process transaction with 0 points.'); + } + + return $amount; + } + + /** + * Save a record + * + * @param string $type Record type (inserting or updating) + * @param integer $amount Amount to process + * @param string $desc Transaction description + * @param string $cat Transaction category + * @param integer $ref ID of item transaction references + * @return boolean True on success + */ + public function _save($type, $amount, $desc, $cat, $ref) + { + if (!$this->_saveBalance($type)) + { + return false; + } + + if (!$this->_saveTransaction($type, $amount, $desc, $cat, $ref)) + { + return false; + } + + return true; + } + + /** + * Save the current balance + * + * @param string $type Record type (inserting or updating) + * @return boolean True on success + */ + public function _saveBalance($type) + { + if ($type == 'creation') + { + $model = Account::blank(); + } + else + { + $model = Account::oneByUserId($this->uid); + } + + $model->set([ + 'uid' => $this->uid, + 'balance' => $this->balance, + 'earnings' => $this->earnings, + 'credit' => $this->credit + ]); + + if (!$model->save()) + { + $this->setError($model->getError()); + + return false; + } + + return true; + } + + /** + * Record the transaction + * + * @param string $type Record type (inserting or updating) + * @param integer $amount Amount to process + * @param string $desc Transaction description + * @param string $cat Transaction category + * @param integer $ref ID of item transaction references + * @return boolean True on success + */ + public function _saveTransaction($type, $amount, $desc, $cat, $ref) + { + $transaction = Transaction::blank()->set(array( + 'uid' => $this->uid, + 'type' => $type, + 'amount' => $amount, + 'description' => $desc, + 'category' => $cat, + 'referenceid' => $ref, + 'balance' => $this->balance + )); + + if (!$transaction->save()) + { + $this->setError($transaction->getError()); + + return false; + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Bank/Transaction.php b/core/libraries/Hubzero/Bank/Transaction.php new file mode 100644 index 00000000000..0e60efd07c2 --- /dev/null +++ b/core/libraries/Hubzero/Bank/Transaction.php @@ -0,0 +1,262 @@ + 'positive|nonzero', + 'type' => 'notempty', + 'category' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'created' + ); + + /** + * Get a history of transactions for a user + * + * @param integer $limit Number of records to return + * @param integer $uid User ID + * @return mixed False if errors, array on success + */ + public static function history($limit=50, $uid=null) + { + $model = self::all(); + + if ($limit) + { + $model->limit((int)$limit); + } + + if ($uid) + { + $model->whereEquals('uid', $uid); + } + + return $model->order('created', 'desc')->rows(); + } + + /** + * Delete records for a given category, type, and reference combination + * + * @param string $category Transaction category (royalties, etc) + * @param string $type Transaction type (deposit, withdraw, etc) + * @param integer $referenceid Reference ID (resource ID, etc) + * @param integer $uid User ID + * @return boolean False if errors, True on success + */ + public static function deleteRecords($category=null, $type=null, $referenceid=null, $uid=null) + { + $model = self::all(); + + if ($category) + { + $model->whereEquals('category', $category); + } + + if ($type) + { + $model->whereEquals('type', $type); + } + + if ($referenceid) + { + $model->whereEquals('referenceid', $referenceid); + } + + if ($uid) + { + $model->whereEquals('uid', $uid); + } + + foreach ($model->rows() as $row) + { + if (!$row->destroy()) + { + return false; + } + } + + return true; + } + + /** + * Get get the transaction amount for a category, type, reference item and, optionally, user + * + * @param string $category Transaction category (royalties, etc) + * @param string $type Transaction type (deposit, withdraw, etc) + * @param integer $referenceid Reference ID (resource ID, etc) + * @param integer $uid User ID + * @return integer + */ + public static function getAmount($category=null, $type=null, $referenceid=null, $uid=null) + { + $model = self::all() + ->select('amount'); + + if ($category) + { + $model->whereEquals('category', $category); + } + + if ($type) + { + $model->whereEquals('type', $type); + } + + if ($referenceid) + { + $model->whereEquals('referenceid', $referenceid); + } + + if ($uid) + { + $model->whereEquals('uid', $uid); + } + + $row = $model->row(); + + return $row->amount; + } + + /** + * Get a point total/average for a combination of category, type, user, etc. + * + * @param string $category Transaction category (royalties, etc) + * @param string $type Transaction type (deposit, withdraw, etc) + * @param integer $referenceid Reference ID (resource ID, etc) + * @param integer $royalty If getting royalties + * @param string $action Action to filter by (asked, answered, misc) + * @param integer $uid User ID + * @param integer $allusers Get total for all users? + * @param string $when Datetime filter + * @param integer $calc How total is calculated (record sum, avg, record count) + * @return integer + */ + public static function getTotals($category=null, $type=null, $referenceid=null, $royalty=0, $action=null, $uid=null, $allusers = 0, $when=null, $calc=0) + { + $model = self::all(); + + if ($calc == 0) + { + $model->select("SUM(amount) AS total"); + } + else if ($calc == 1) + { + // average + $model->select("AVG(amount) AS total"); + } + else if ($calc == 2) + { + // num of transactions + $model->select("COUNT(*) AS total"); + } + + if ($category) + { + $model->whereEquals('category', $category); + } + + if ($type) + { + $model->whereEquals('type', $type); + } + + if ($referenceid) + { + $model->whereEquals('referenceid', $referenceid); + } + + if ($royalty) + { + $model->whereLike('description', 'Royalty payment%'); + } + + if ($action == 'asked') + { + $model->whereLike('description', '%posting question%'); + } + else if ($action == 'answered') + { + $model->whereLike('description', '%answering question%');// OR description like 'Answer for question%' OR description like 'Answered question%') "; + } + else if ($action == 'misc') + { + $model->where('description', 'NOT LIKE', '%posting question%'); + $model->where('description', 'NOT LIKE', '%answering question%'); + $model->where('description', 'NOT LIKE', 'Answer for question%'); + $model->where('description', 'NOT LIKE', 'Answered question%'); + } + + if (!$allusers) + { + if ($uid) + { + $model->whereEquals('uid', $uid); + } + } + + if ($when) + { + $model->whereLike('created', $when . '%'); + } + + $row = $model->row(); + + return $row->get('total', 0); + } +} diff --git a/core/libraries/Hubzero/Base/Application.php b/core/libraries/Hubzero/Base/Application.php new file mode 100644 index 00000000000..9f6eb510a0b --- /dev/null +++ b/core/libraries/Hubzero/Base/Application.php @@ -0,0 +1,434 @@ +name; + if (isset($this['client']->alias)) + { + $name = $this['client']->alias; + } + return ($name == $client); + } + + throw new RuntimeException(sprintf('Method [%s] not found.', $method)); + } + + /** + * Get the version number of the application. + * + * @return string + */ + public function version() + { + if (!defined('HVERSION')) + { + return static::VERSION; + } + else + { + return HVERSION; + } + } + + /** + * Register facades with the autoloader + * + * @param array $aliases + * @return void + */ + public function registerFacades($aliases = array()) + { + // Set the application to resolve Facades + Facade::setApplication($this); + + // Create aliaes for runtime + Facade::createAliases((array) $aliases); + } + + /** + * Register a service provider with the application. + * + * @param mixed $provider \Hubzero\Base\ServiceProvider|string + * @param array $options + * @param bool $force + * @return object + */ + public function register($provider, $options = array()) //, $force = false) + { + /*if ($registered = $this->getRegistered($provider) && !$force) + { + return $registered; + }*/ + + // If the given "provider" is a string, we will resolve it, passing in the + // application instance automatically for the developer. This is simply + // a more convenient way of specifying your service provider classes. + if (is_string($provider)) + { + $provider = $this->resolveProviderClass($provider); + } + + $provider->register(); + + // Once we have registered the service we will iterate through the options + // and set each of them on the application so they will be available on + // the actual loading of the service objects and for developer usage. + foreach ($options as $key => $value) + { + $this[$key] = $value; + } + + // Since service providers can do more than just register callbacks, + // we need to track the loaded providers for futher use later in the + // application. + $this->markAsRegistered($provider); + + // If the application has already booted, we will call this boot method on + // the provider class so it has an opportunity to do its boot logic and + // will be ready for any usage by the developer's application logics. + if ($this->booted) + { + $this->bootProvider($provider); + } + + return $this; + } + + /** + * Get the registered service provider instance if it exists. + * + * @param mixed $provider \Hubzero\Base\ServiceProvider|string + * @return mixed \Hubzero\Base\ServiceProvider|null + */ + /*public function getRegistered($provider) + { + $name = is_string($provider) ? $provider : get_class($provider); + + if (array_key_exists($name, $this->serviceProviders)) + { + return $this->serviceProviders[$name]; + } + + return null; + }*/ + + /** + * Resolve a service provider instance from the class name. + * + * @param string $provider + * @return object \Hubzero\Base\ServiceProvider + */ + protected function resolveProviderClass($provider) + { + return new $provider($this); + } + + /** + * Mark the given provider as registered. + * + * @param object \Hubzero\Base\ServiceProvider + * @return void + */ + protected function markAsRegistered($provider) + { + $class = get_class($provider); + + $this->serviceProviders[$class] = $provider; + } + + /** + * Detect the application's current environment. + * + * @param array|string $clients + * @return string + */ + public function detectClient($clients) + { + $args = isset($_SERVER['argv']) ? $_SERVER['argv'] : null; + + return $this['client'] = with(new ClientDetector($this['request']))->detect($clients, $args); + } + + /** + * Determine if we are running in the console. + * + * @return bool + */ + public function runningInConsole() + { + return php_sapi_name() == 'cli'; + } + + /** + * Abort + * + * @param integer $code Error code + * @param string $message Error message + * @return void + */ + public function abort($code, $message='') + { + switch ($code) + { + case 404: + throw new NotFoundException($message, $code); + break; + + case 403: + throw new NotAuthorizedException($message, $code); + break; + + default: + throw new RuntimeException($message, $code); + break; + } + } + + /** + * Redirect current request to new request (sub requests) + * + * @param string $url Url to redirect to + * @param string $message Message to display on redirect. + * @param array $type Message type. + * @return void + */ + public function redirect($url, $message = null, $type = 'success') + { + $redirect = new RedirectResponse($url); + $redirect->setRequest($this['request']); + + if ($message && $this->has('notification')) + { + $this['notification']->message($message, $type); + } + + $redirect->send(); + + $this->close(); + } + + /** + * Terminate the application + * + * @return void + */ + public function close() + { + exit(); + } + + /** + * Provides a secure hash based on a seed + * + * @param string $seed Seed string. + * @return string A secure hash + */ + public function hash($seed) + { + return md5($this['config']->get('secret') . $seed); + } + + /** + * Boot the application's service providers. + * + * @return void + */ + public function boot() + { + if ($this->booted) + { + return; + } + + array_walk($this->serviceProviders, function($p) + { + $this->bootProvider($p); + }); + + $this->booted = true; + } + + /** + * Boot the given service provider. + * + * @param object $provider + * @return void + */ + protected function bootProvider(ServiceProvider $provider) + { + if (method_exists($provider, 'boot')) + { + return $provider->boot(); + } + } + + /** + * Get only runnable services + * + * @param array $layers Unfiltered services + * @return array Filtered runnable services + */ + protected function middleware($services) + { + return array_filter($services, function($service) + { + return $service instanceof Middleware; + }); + } + + /** + * Application layer is responsible for dispatching request + * + * @param object $request Request object + * @return object Response object + */ + public function handle(Request $request) + { + return $this['response']->compress($this['config']->get('gzip', false)); + } + + /** + * Run the application and send the response. + * + * @return void + */ + public function run() + { + // Start handling errors before doing anything else + if ($this->has('error')) + { + array_walk($this->serviceProviders, function($p) + { + if (method_exists($p, 'startHandling')) + { + return $p->startHandling(); + } + }); + } + + // Boot the application + // + // This allows service providers to finish performing any + // needed setup. + $this->boot(); + + // Initialise + if (!$this->runningInConsole() && $this->has('dispatcher')) + { + $this['dispatcher']->trigger('system.onAfterInitialise'); + + if ($this->has('profiler') && $this->get('profiler')) + { + $this['profiler']->mark('afterInitialise'); + } + } + + // Create a new stack and bind to application then + $this['stack'] = new Stack($this); + + // Send request throught stack and finally send response + $this['stack'] + ->send($this['request']) + ->through($this->middleware($this->serviceProviders)) + ->then(function($request, $response) + { + $response->prepare($request); + $response->send(); + }); + } +} diff --git a/core/libraries/Hubzero/Base/ClassLoader.php b/core/libraries/Hubzero/Base/ClassLoader.php new file mode 100755 index 00000000000..6e79d94f17c --- /dev/null +++ b/core/libraries/Hubzero/Base/ClassLoader.php @@ -0,0 +1,134 @@ + /components/example/models/entry.php + * + * Inspired by Laravel 4's autoloader + */ +class ClassLoader +{ + /** + * The registered directories. + * + * @var array + */ + protected static $directories = array(); + + /** + * Indicates if a ClassLoader has been registered. + * + * @var bool + */ + protected static $registered = false; + + /** + * Load the given class file. + * + * @param string $class + * @return bool + */ + public static function load($class) + { + $class = static::normalizeClass($class); + + foreach (static::$directories as $directory) + { + if (file_exists($path = $directory . DIRECTORY_SEPARATOR . $class)) + { + require_once $path; + + return true; + } + + if (file_exists($path = $directory . DIRECTORY_SEPARATOR . strtolower($class))) + { + require_once $path; + + return true; + } + } + + return false; + } + + /** + * Get the normal file name for a class. + * + * @param string $class + * @return string + */ + public static function normalizeClass($class) + { + if ($class[0] == '\\') + { + $class = substr($class, 1); + } + + return str_replace(array('\\', '_'), DIRECTORY_SEPARATOR, $class) . '.php'; + } + + /** + * Register the given class loader on the auto-loader stack. + * + * @return void + */ + public static function register() + { + if (!static::$registered) + { + static::$registered = spl_autoload_register(array('\Hubzero\Base\ClassLoader', 'load')); + } + } + + /** + * Add directories to the class loader. + * + * @param string|array $directories + * @return void + */ + public static function addDirectories($directories) + { + static::$directories = array_unique(array_merge(static::$directories, (array) $directories)); + } + + /** + * Remove directories from the class loader. + * + * @param string|array $directories + * @return void + */ + public static function removeDirectories($directories = null) + { + if (is_null($directories)) + { + static::$directories = array(); + } + else + { + static::$directories = array_diff(static::$directories, (array) $directories); + } + } + + /** + * Gets all the directories registered with the loader. + * + * @return array + */ + public static function getDirectories() + { + return static::$directories; + } +} diff --git a/core/libraries/Hubzero/Base/Client/Administrator.php b/core/libraries/Hubzero/Base/Client/Administrator.php new file mode 100644 index 00000000000..40322f4a0cc --- /dev/null +++ b/core/libraries/Hubzero/Base/Client/Administrator.php @@ -0,0 +1,42 @@ +error('You\'ve attempted to enter an infinite loop. We\'ve stopped you. You\'re welcome.'); + } + + // If task is help, set the output to our output class with extra methods for rendering help doc + if ($task == 'help') + { + $output = $output->getHelpOutput(); + } + + $command = new $class($output, $arguments); + + $command->{$task}(); + } +} diff --git a/core/libraries/Hubzero/Base/Client/ClientInterface.php b/core/libraries/Hubzero/Base/Client/ClientInterface.php new file mode 100644 index 00000000000..860e15a65bc --- /dev/null +++ b/core/libraries/Hubzero/Base/Client/ClientInterface.php @@ -0,0 +1,15 @@ +request = $request; + } + + /** + * Detect the application's current client. + * + * @param array $environments + * @return object + */ + public function detect($environments) + { + if ($this->detectConsoleClient($environments)) + { + return ClientManager::client('cli', true); + } + + return $this->detectWebClient($environments); + } + + /** + * Determine client for a web request. + * + * @param array $environments + * @return object + */ + protected function detectWebClient($environments) + { + $default = ClientManager::client('site', true); + + // To determine the current client, we'll simply iterate through the possible + // clients and look for the one that matches the path for the request we + // are currently processing here, then return back that client. + foreach ($environments as $environment => $url) + { + if ($client = ClientManager::client($environment, true)) + { + if ($client->name == 'cli') + { + continue; + } + + // Legacy check based on file path + // Ex: JPATH_API would be set from ROOT/api/index.php + // @TODO: Remove need for this code + $const = 'JPATH_' . strtoupper($environment); + + if (defined($const) + && defined('JPATH_BASE') + && JPATH_BASE == constant($const)) + { + return $client; + } + + // Check based on request path + // Ex: http://somehub.org/api + if ($this->request->segment(1) == $url + || $this->request->segment(1) == $client->name + || $this->request->segment(1) == $client->url) + { + return $client; + } + } + } + + return $default; + } + + /** + * Determine if the client is command-line + * + * @param array $environments + * @return bool + */ + protected function detectConsoleClient($environments) + { + return (php_sapi_name() == 'cli'); + } +} diff --git a/core/libraries/Hubzero/Base/ClientManager.php b/core/libraries/Hubzero/Base/ClientManager.php new file mode 100644 index 00000000000..9c235e80203 --- /dev/null +++ b/core/libraries/Hubzero/Base/ClientManager.php @@ -0,0 +1,168 @@ +isDot() || $file->isDir()) + { + continue; + } + + $client = preg_replace('#\.[^.]*$#', '', $file->getFilename()); + if ($client == 'ClientInterface') + { + continue; + } + + $cls = __NAMESPACE__ . '\\Client\\' . ucfirst(strtolower($client)); + + if (!class_exists($cls)) + { + include_once $file->getPathname(); + + if (!class_exists($cls)) + { + throw new \InvalidArgumentException(sprintf('Invalid client type of "%s".', $client)); + } + } + + $obj = new $cls; + + self::$_clients[$obj->id] = $obj; + } + ksort(self::$_clients); + } + + // If no client id has been passed return the whole array + if (is_null($id)) + { + return self::all(); + } + + // Are we looking for client information by id or by name? + if (!$byName) + { + if (isset(self::$_clients[$id])) + { + return self::$_clients[$id]; + } + } + else + { + foreach (self::$_clients as $client) + { + if ($client->name == strtolower($id)) + { + return $client; + } + } + } + + return null; + } + + /** + * Modify information on a client. + * + * @param integer $client A client identifier + * @param string $property Property to set + * @param mixed $value Value to set + * @return void + */ + public static function modify($client, $property, $value) + { + if ($cl = self::client($client)) + { + $cl->$property = $value; + } + } + + /** + * Adds information for a client. + * + * @param mixed $client A client identifier either an array or object + * @return boolean True if the information is added. False on error + */ + public static function append($client) + { + if (is_array($client)) + { + $client = (object) $client; + } + + if (!is_object($client)) + { + return false; + } + + $info = self::client(); + + if (!isset($client->id)) + { + $client->id = count($info); + } + + self::$_clients[$client->id] = clone $client; + + return true; + } + + /** + * Get all client data + * + * @return mixed + */ + public static function all() + { + return self::$_clients; + } + + /** + * Reset the client list + * + * @return void + */ + public static function reset() + { + self::$_clients = null; + } +} diff --git a/core/libraries/Hubzero/Base/ItemList.php b/core/libraries/Hubzero/Base/ItemList.php new file mode 100644 index 00000000000..7b2e02752fb --- /dev/null +++ b/core/libraries/Hubzero/Base/ItemList.php @@ -0,0 +1,344 @@ +_data = $data; + } + $this->_total = count($this->_data); + } + + /** + * Add item to the array + * + * @param mixed $value + * @return void + */ + public function add($value) + { + return $this->offsetSet(null, $value); + } + + /** + * Remove item from the array + * + * @param mixed $offset + * @return void + */ + public function remove($offset) + { + return $this->offsetUnset($offset); + } + + /** + * Reset cursor to starting point + * + * @return void + */ + public function rewind() + { + $this->_pos = 0; + } + + /** + * Reset cursor to starting point and reset list (in case we unset some) + * + * @return void + */ + public function reset() + { + $this->_pos = 0; + $this->_data = array_values($this->_data); + } + + /** + * Is the current position the first one? + * + * @return boolean + */ + public function isFirst() + { + return !isset($this->_data[$this->_pos - 1]); + } + + /** + * Is the current position the last one? + * + * @return boolean + */ + public function isLast() + { + return !isset($this->_data[$this->_pos + 1]); + } + + /** + * Seek to an absolute position + * + * @param integer $index + * @throws OutOfBoundsException When the seek position is invalid + * @return void + */ + public function seek($index) + { + $this->rewind(); + + while ($this->_pos < $index && $this->valid()) + { + $this->next(); + } + + if (!$this->valid()) + { + throw new \OutOfBoundsException(\Lang::txt('Invalid seek position')); + } + } + + /** + * Return the current array value if the cursor is at + * a valid index + * + * @return mixed + */ + public function current() + { + if ($this->valid()) + { + return $this->_data[$this->_pos]; + } + return null; + } + + /** + * Return the array count + * + * @return integer + */ + public function total() + { + return $this->count(); + } + + /** + * Return the array count + * + * @return integer + */ + public function count() + { + return $this->_total; + } + + /** + * Return the first array value + * + * @return mixed + */ + public function first() + { + $this->rewind(); + return $this->current(); + } + + /** + * Return the last array value + * + * @return mixed + */ + public function last() + { + $this->_pos = ($this->_total - 1); + return $this->current(); + } + + /** + * Return the key for the current cursor position + * + * @return mixed + */ + public function key() + { + return $this->_pos; + } + + /** + * Set cursor position to previous position and return array value + * + * @return mixed + */ + public function prev() + { + --$this->_pos; + return $this->current(); + } + + /** + * Set cursor position to next position and return array value + * + * @return mixed + */ + public function next() + { + ++$this->_pos; + return $this->current(); + } + + /** + * Check if the current cursor position is valid + * + * @return mixed + */ + public function valid() + { + return isset($this->_data[$this->_pos]); + } + + /** + * Check if an offset exists + * + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->_data); + } + + /** + * Get the value of an offset + * + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return isset($this->_data[$offset]) ? $this->_data[$offset] : null; + } + + /** + * Append a new item + * + * @param mixed $offset + * @param mixed $item + * @return void + */ + public function offsetSet($offset, $item) + { + if ($offset === null) + { + $this->_data[] = $item; + $this->_total = count($this->_data); + } + else + { + $this->_data[$offset] = $item; + } + } + + /** + * Unset an item + * + * @param mixed $offset + * @return void + */ + public function offsetUnset($offset) + { + unset($this->_data[$offset]); + $this->_total = count($this->_data); + } + + /** + * Run a map over each of the items + * + * @param object $callback Closure + * @return array + */ + public function map(Closure $callback) + { + return (array_map($callback, $this->_data)); + } + + /** + * Run a filter over each of the items + * + * @param object $callback Closure + * @return array + */ + public function filter(Closure $callback) + { + return new static(array_filter($this->_data, $callback)); + } + + /** + * Merge Item Lists + * + * @param object $data ItemList + * @return object ItemList + */ + public function merge() + { + foreach (func_get_args() as $list) + { + if ($list instanceof self) + { + $this->_data = array_merge($this->_data, $list->_data); + } + } + + return new static($this->_data); + } + + /** + * Reverse data order. + * + * @return object ItemList + */ + public function reverse() + { + return new static(array_reverse($this->_data)); + } +} diff --git a/core/libraries/Hubzero/Base/Middleware.php b/core/libraries/Hubzero/Base/Middleware.php new file mode 100644 index 00000000000..70665f7a39d --- /dev/null +++ b/core/libraries/Hubzero/Base/Middleware.php @@ -0,0 +1,48 @@ +app['stack']->next($request); + } + + /** + * Handle request object + * + * Each runnable service must implement this method and do what it wants + * and then MUST pass the request along to the next service after its done. + * + * @param object $request Request object + * @return mixed Result + */ + abstract public function handle(Request $request); +} diff --git a/core/libraries/Hubzero/Base/Model.php b/core/libraries/Hubzero/Base/Model.php new file mode 100644 index 00000000000..98c185371ef --- /dev/null +++ b/core/libraries/Hubzero/Base/Model.php @@ -0,0 +1,616 @@ +_db = $this->initDbo(); + + if ($this->_tbl_name) + { + $cls = $this->_tbl_name; + $this->_tbl = new $cls($this->_db); + + if (!($this->_tbl instanceof \JTable) && !($this->_tbl instanceof \Hubzero\Database\Table)) + { + $this->_logError( + __CLASS__ . '::' . __FUNCTION__ . '(); ' . \Lang::txt('Table class must be an instance of JTable.') + ); + throw new \LogicException(\Lang::txt('Table class must be an instance of JTable.')); + } + + if (is_numeric($oid) || is_string($oid)) + { + // Make sure $oid isn't empty + // This saves a database call + if ($oid) + { + $this->_tbl->load($oid); + } + } + else if (is_object($oid) || is_array($oid)) + { + $this->bind($oid); + } + } + } + + /** + * Returns a property of the object or the default value if the property is not set. + * + * @param string $property The name of the property + * @param mixed $default The default value + * @return mixed The value of the property + */ + public function get($property, $default=null) + { + if (isset($this->_tbl->$property)) + { + return $this->_tbl->$property; + } + else if (isset($this->_tbl->{'__' . $property})) + { + return $this->_tbl->{'__' . $property}; + } + return $default; + } + + /** + * Modifies a property of the object, creating it if it does not already exist. + * + * @param string $property The name of the property + * @param mixed $value The value of the property to set + * @return object This current model + */ + public function set($property, $value = null) + { + if (!array_key_exists($property, $this->_tbl->getProperties())) + { + $property = '__' . $property; + } + $this->_tbl->$property = $value; + return $this; + } + + /** + * Method to get the database connection. + * + * If detected that the code is being run in a super group + * component, it will return the super group DB connection + * instead of the site connection. + * + * @return object Database + */ + public function initDbo() + { + if (defined('JPATH_GROUPCOMPONENT')) + { + $r = new \ReflectionClass($this); + if (substr($r->getFileName(), 0, strlen(JPATH_GROUPCOMPONENT)) == JPATH_GROUPCOMPONENT) + { + return \Hubzero\User\Group\Helper::getDbo(); + } + } + return \App::get('db'); + } + + /** + * Method to get the Database connector object. + * + * @return object The internal database connector object. + */ + public function getDbo() + { + return $this->_db; + } + + /** + * Method to set the database connector object. + * + * @param object &$db A database connector object to be used by the table object. + * @return boolean True on success. + */ + public function setDbo(&$db) + { + if (!($db instanceof \Hubzero\Database\Driver)) + { + return false; + } + + $this->_db = $db; + $this->_tbl->setDBO($this->_db); + + return true; + } + + /** + * Check if the entry exists (i.e., has a database record) + * + * @return boolean True if record exists, False if not + */ + public function exists() + { + if (!array_key_exists('id', $this->_tbl->getFields())) + { + return true; + } + if ($this->get('id') && (int) $this->get('id') > 0) + { + return true; + } + return false; + } + + /** + * Has the offering started? + * + * @return boolean + */ + public function isPublished() + { + if (!array_key_exists('state', $this->_tbl->getFields())) + { + return true; + } + if ($this->get('state') == self::APP_STATE_PUBLISHED) + { + return true; + } + return false; + } + + /** + * Has the offering started? + * + * @return boolean + */ + public function isUnpublished() + { + if (!array_key_exists('state', $this->_tbl->getFields())) + { + return false; + } + if ($this->get('state') == self::APP_STATE_UNPUBLISHED) + { + return true; + } + return false; + } + + /** + * Has the offering started? + * + * @return boolean + */ + public function isDeleted() + { + if (!array_key_exists('state', $this->_tbl->getFields())) + { + return false; + } + if ($this->get('state') == self::APP_STATE_DELETED) + { + return true; + } + return false; + } + + /** + * Bind data to the model + * + * @param mixed $data Object or array + * @return boolean True on success, False on error + */ + public function bind($data=null) + { + if (is_object($data)) + { + $res = $this->_tbl->bind($data); + + if ($res) + { + $properties = $this->_tbl->getProperties(); + foreach (get_object_vars($data) as $key => $property) + { + if (!array_key_exists($key, $properties)) + { + $this->_tbl->set('__' . $key, $property); + } + } + } + } + else if (is_array($data)) + { + $res = $this->_tbl->bind($data); + + if ($res) + { + $properties = $this->_tbl->getProperties(); + foreach (array_keys($data) as $key) + { + if (!array_key_exists($key, $properties)) + { + $this->_tbl->set('__' . $key, $data[$key]); + } + } + } + } + else + { + $this->_logError( + __CLASS__ . '::' . __FUNCTION__ . '(); ' . \Lang::txt('Data must be of type object or array. Type given was %s', gettype($data)) + ); + throw new \InvalidArgumentException(\Lang::txt('Data must be of type object or array. Type given was %s', gettype($data))); + } + + return $res; + } + + /** + * Log an error message + * + * @param string $message Message to log + * @return void + */ + protected function _logError($message) + { + return $this->_log('error', $message); + } + + /** + * Log an error message + * + * @param string $message Message to log + * @return void + */ + protected function _logDebug($message) + { + return $this->_log('debug', $message); + } + + /** + * Log an error message + * + * @param string $message Message type to log + * @param string $message Message to log + * @return void + */ + protected function _log($type='error', $message) + { + if (!$message) + { + return; + } + + if (\App::get('config')->get('debug')) + { + $message = '[' . \App::get('request')->getVar('REQUEST_URI', '', 'server') . '] [' . $message . ']'; + } + + $type = strtolower($type); + if (!in_array($type, array('error', 'debug', 'critical', 'warning', 'notice', 'alert', 'emergency', 'info'))) + { + return; + } + + $logger = \Log::getRoot(); + $logger->$type($message); + } + + /** + * Perform data validation + * + * @return boolean False if error, True on success + */ + public function check() + { + // Is data valid? + if (!$this->_tbl->check()) + { + $this->_errors = $this->_tbl->getErrors(); + return false; + } + return true; + } + + /** + * Store changes to this database entry + * + * @param boolean $check Perform data validation check? + * @return boolean False if error, True on success + */ + public function store($check=true) + { + // Validate data? + if ($check) + { + // Is data valid? + if (!$this->check()) + { + return false; + } + + if ($this->_context) + { + $results = \Event::trigger('content.onContentBeforeSave', array( + $this->_context, + &$this, + $this->exists() + )); + foreach ($results as $result) + { + if ($result === false) + { + $this->setError(\App::get('language')->txt('Content failed validation.')); + return false; + } + } + } + } + + // Attempt to store data + if (!$this->_tbl->store()) + { + $this->setError($this->_tbl->getError()); + return false; + } + + return true; + } + + /** + * Delete a record + * + * @return boolean True on success, false on error + */ + public function delete() + { + // Can't delete what doesn't exist + if (!$this->exists()) + { + return true; + } + + // Remove record from the database + if (!$this->_tbl->delete()) + { + $this->setError($this->_tbl->getError()); + return false; + } + + // Hey, no errors! + return true; + } + + /** + * Import a set of plugins + * + * @return object + */ + public function importPlugin($type='') + { + \Plugin::import($type); + + return $this; + } + + /** + * Import a set of plugins + * + * @return object + */ + public function trigger($event='', $params=array()) + { + return \Event::trigger($event, $params); + } + + /** + * Turn the object into a string + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Turn the object into a string + * + * @return string + */ + public function toString($ignore=array('_db')) + { + return $this->_print_r($this, $ignore); + } + + + /** + * Special print_r to strip out any vars passed in $ignore + * + * @param object $subject Object to print_r + * @param array $ignore Property names to ignore + * @param integer $depth Recursion depth + * @param array $refChain Reference chain + * @return string + */ + private function _print_r($subject, $ignore = array(), $depth = 1, $refChain = array()) + { + $str = ''; + + if ($depth > 20) + { + return $str; + } + + if (is_object($subject)) + { + foreach ($refChain as $refVal) + { + if ($refVal === $subject) + { + $str .= "*RECURSION*\n"; + return $str; + } + } + + array_push($refChain, $subject); + + $str .= get_class($subject) . " Object ( \n"; + $subject = (array) $subject; + foreach ($subject as $key => $val) + { + if (is_array($ignore) && !in_array($key, $ignore, 1)) + { + if ($key{0} == "\0") + { + $keyParts = explode("\0", $key); + if (is_array($ignore) && in_array($keyParts[2], $ignore, 1)) + { + continue; + } + $str .= str_repeat(" ", $depth * 4) . '['; + $str .= $keyParts[2] . (($keyParts[1] == '*') ? ':protected' : ':private'); + } + else + { + $str .= str_repeat(" ", $depth * 4) . '['; + $str .= $key; + } + $str .= '] => '; + $str .= $this->_print_r($val, $ignore, $depth + 1, $refChain); + } + } + $str .= str_repeat(" ", ($depth - 1) * 4) . ")\n"; + + array_pop($refChain); + } + elseif (is_array($subject)) + { + $str .= "Array ( \n"; + foreach ($subject as $key => $val) + { + if (is_array($ignore) && !in_array($key, $ignore, 1)) + { + $str .= str_repeat(" ", $depth * 4) . '[' . $key . '] => '; + $str .= $this->_print_r($val, $ignore, $depth + 1, $refChain); + } + } + $str .= str_repeat(" ", ($depth - 1) * 4) . ")\n"; + } + else + { + $str .= $subject . "\n"; + } + return $str; + } + + /** + * Method to return model values in array format + * + * @param boolean $verbose Include prefixed "__" vars in output + * @return array Array of model values. + */ + public function toArray($verbose = false) + { + $output = $this->_tbl->getProperties(); + + if ($verbose) + { + foreach ($this->_tbl as $key => $value) + { + if (substr($key, 0, 2) == '__') + { + $output[substr($key, 2)] = $value; + } + } + } + + return $output; + } + + /** + * Dynamically handle error additions. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + throw new \BadMethodCallException(sprintf(__CLASS__ . '; Method [%s] does not exist.', $method)); + } +} diff --git a/core/libraries/Hubzero/Base/Model/ItemList.php b/core/libraries/Hubzero/Base/Model/ItemList.php new file mode 100644 index 00000000000..cba371de2c2 --- /dev/null +++ b/core/libraries/Hubzero/Base/Model/ItemList.php @@ -0,0 +1,50 @@ +_data as $data) + { + if ($data->get($key) == $value) + { + return $data; + } + } + return null; + } + + /** + * Lists a specific key from the item list + * + * @param string $key Key to grab from item + * @param string $default Default value if key is empty + * @return array Array of keys + */ + public function lists($key, $default = null) + { + $results = array(); + foreach ($this->_data as $data) + { + array_push($results, $data->get($key)); + } + return $results; + } +} diff --git a/core/libraries/Hubzero/Base/Obj.php b/core/libraries/Hubzero/Base/Obj.php new file mode 100644 index 00000000000..95d48d871b8 --- /dev/null +++ b/core/libraries/Hubzero/Base/Obj.php @@ -0,0 +1,146 @@ +setProperties($properties); + } + } + + /** + * Magic method to convert the object to a string gracefully. + * + * @return string The classname. + */ + public function __toString() + { + return get_class($this); + } + + /** + * Sets a default value if not alreay assigned + * + * @param string $property The name of the property. + * @param mixed $default The default value. + * @return mixed + */ + public function def($property, $default = null) + { + $value = $this->get($property, $default); + return $this->set($property, $value); + } + + /** + * Returns a property of the object or the default value if the property is not set. + * + * @param string $property The name of the property. + * @param mixed $default The default value. + * @return mixed The value of the property. + */ + public function get($property, $default = null) + { + if (isset($this->$property)) + { + return $this->$property; + } + return $default; + } + + /** + * Returns an associative array of object properties. + * + * @param boolean $public If true, returns only the public properties. + * @return array + */ + public function getProperties($public = true) + { + $vars = get_object_vars($this); + + if ($public) + { + foreach ($vars as $key => $value) + { + if ('_' == substr($key, 0, 1)) + { + unset($vars[$key]); + } + } + } + + return $vars; + } + + /** + * Modifies a property of the object, creating it if it does not already exist. + * Returns $this so set() can be chained + * + * $object->set('foo', $bar) + * ->set('bar', $foo) + * ->doSomething(); + * + * @param string $property The name of the property. + * @param mixed $value The value of the property to set. + * @return object + */ + public function set($property, $value) + { + $this->$property = $value; + return $this; // So we can do method chaining! + } + + /** + * Set the object properties based on a named array/hash. + * + * @param mixed $properties Either an associative array or another object. + * @return boolean + */ + public function setProperties($properties) + { + if (is_array($properties) || is_object($properties)) + { + // PHP changed the object-to-array casting algorithm with version 5.3.0 + // So we need to use get_object_vars() instead + if (is_object($properties)) + { + $properties = get_object_vars($properties); + } + + foreach ((array) $properties as $k => $v) + { + // Use the set function which might be overridden. + $this->set($k, $v); + } + + return true; + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Base/Object.php b/core/libraries/Hubzero/Base/Object.php new file mode 100644 index 00000000000..b4d26cd5b36 --- /dev/null +++ b/core/libraries/Hubzero/Base/Object.php @@ -0,0 +1,17 @@ +app = $app; + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + } +} diff --git a/core/libraries/Hubzero/Base/Stack.php b/core/libraries/Hubzero/Base/Stack.php new file mode 100644 index 00000000000..e8f24f95edf --- /dev/null +++ b/core/libraries/Hubzero/Base/Stack.php @@ -0,0 +1,128 @@ +position = 0; + $this->layers = array($core); + } + + /** + * Send request through stack + * + * @param object $request Request object + * @return object + */ + public function send(Request $request) + { + $this->request = $request; + + return $this; + } + + /** + * Set layers on stack + * + * @param array $layers Array of services + * @return object + */ + public function through($layers) + { + // Merge existing layers (core) + $this->layers = array_merge( + $this->layers, $layers + ); + + // Put the layers in reverse + $this->layers = array_values(array_reverse($this->layers)); + + return $this; + } + + /** + * Add something to the stack + * + * @param mixed $layer + * @return object + */ + public function push($layer) + { + array_push($this->layers, $layer); + + return $this; + } + + /** + * Final callback + * + * @param object $callback Callback after stack is run + * @return void Result of callback + */ + public function then(\Closure $callback) + { + $response = $this->layers[0]->handle($this->request); + + return call_user_func($callback, $this->request, $response); + } + + /** + * Call next layer in stack + * + * @param object $request Request object + * @return object + */ + public function next(Request $request) + { + // Update the stack position + $this->position++; + + // Get the next layer + $layer = $this->layers[$this->position]; + + // Call handle on next layer + return $layer->handle($request); + } +} diff --git a/core/libraries/Hubzero/Base/Tests/ClientManagerTest.php b/core/libraries/Hubzero/Base/Tests/ClientManagerTest.php new file mode 100644 index 00000000000..c6144654ae2 --- /dev/null +++ b/core/libraries/Hubzero/Base/Tests/ClientManagerTest.php @@ -0,0 +1,147 @@ + 'for the money', + 'two' => 'for the show', + 'three' => 'to get ready', + 'four' => 'to go' + ); + + /** + * Test reset() and all() + * + * @covers \Hubzero\Base\ClientManager::reset + * @covers \Hubzero\Base\ClientManager::all + * @return void + **/ + public function testReset() + { + ClientManager::reset(); + + $all = ClientManager::all(); + + $this->assertEquals($all, null); + } + + /** + * Test client() + * + * @covers \Hubzero\Base\ClientManager::client + * @return void + **/ + public function testClient() + { + $clients = ClientManager::client(); + + $this->assertCount(7, $clients); + + $client = ClientManager::client(1); + + $this->assertTrue(is_object($client)); + $this->assertEquals($client->name, 'administrator'); + + $client = ClientManager::client('api', true); + + $this->assertTrue(is_object($client)); + $this->assertEquals($client->name, 'api'); + $this->assertEquals($client->id, 4); + + $client = ClientManager::client('site'); + + $this->assertFalse(is_object($client)); + } + + /** + * Test modify() + * + * @covers \Hubzero\Base\ClientManager::modify + * @return void + **/ + public function testModify() + { + ClientManager::modify(1, 'name', 'adminstuff'); + + $client = ClientManager::client(1); + + $this->assertTrue(is_object($client)); + $this->assertEquals($client->name, 'adminstuff'); + + ClientManager::modify(1, 'name', 'administrator'); + } + + /** + * Test append() + * + * @covers \Hubzero\Base\ClientManager::append + * @return void + **/ + public function testAppend() + { + $clients = ClientManager::client(); + + $foo = array( + 'id' => 9, + 'name' => 'foo', + 'url' => 'foo' + ); + + $bar = new \stdClass; + $bar->id = 10; + $bar->name = 'bar'; + $bar->url = 'bar'; + + $tur = new \stdClass; + $tur->name = 'tur'; + $tur->url = 'tur'; + + $glu = 'foobar'; + + ClientManager::append($tur); + ClientManager::append($foo); + ClientManager::append($bar); + + $this->assertFalse(ClientManager::append($glu)); + + $client = ClientManager::client('tur', true); + + $this->assertTrue(is_object($client)); + $this->assertEquals($client->name, 'tur'); + $this->assertEquals($client->id, count($clients)); + + $client = ClientManager::client(9); + + $this->assertTrue(is_object($client)); + $this->assertEquals($client->name, 'foo'); + + $client = ClientManager::client(10); + + $this->assertTrue(is_object($client)); + $this->assertEquals($client->name, 'bar'); + + $client = ClientManager::client('foo', true); + + $this->assertTrue(is_object($client)); + $this->assertEquals($client->name, 'foo'); + $this->assertEquals($client->id, 9); + } +} diff --git a/core/libraries/Hubzero/Base/Tests/ErrorBagTraitTest.php b/core/libraries/Hubzero/Base/Tests/ErrorBagTraitTest.php new file mode 100644 index 00000000000..b50f1933140 --- /dev/null +++ b/core/libraries/Hubzero/Base/Tests/ErrorBagTraitTest.php @@ -0,0 +1,92 @@ +obj = $this->getObjectForTrait('Hubzero\Base\Traits\ErrorBag'); + + parent::setUp(); + } + + /** + * Test ErrorBag methods + * + * @covers \Hubzero\Base\Obj::setError + * @covers \Hubzero\Base\Obj::setErrors + * @covers \Hubzero\Base\Obj::getError + * @covers \Hubzero\Base\Obj::getErrors + * @return void + **/ + public function testErrorBag() + { + // Test that an array is returned + $errors = $this->obj->getErrors(); + + // Test that the array is empty + $this->assertTrue(is_array($errors)); + $this->assertCount(0, $errors); + + // Set some errors + $this->obj->setError('Donec sed odio dui.'); + $this->obj->setError(new Exception('Aenean lacinia bibendum.')); + $this->obj->setError('Nulla sed consectetur.'); + + // Get the list of set errors + $errors = $this->obj->getErrors(); + + // Make sure: + // - the list of errors matches the number of errors set + // - getError() returns the last error set + // - getError($index) returns the correct item + // - getError($index, false) returns Exception object instead of string + $this->assertCount(3, $errors); + $this->assertEquals($this->obj->getError(), 'Nulla sed consectetur.'); + $this->assertTrue(is_string($this->obj->getError(1))); + $this->assertFalse($this->obj->getError(5)); + $this->assertInstanceOf('Exception', $this->obj->getError(1, false)); + + // Test overwriting an existing entry + $this->obj->setError('Aenean lacinia bibendum.', 0); + $err = $this->obj->getErrors(); + + $this->assertEquals($this->obj->getError(0), 'Aenean lacinia bibendum.'); + + // Test setting the entire list + $newerrors = array( + 'Integer posuere erat', + 'Ante venenatis dapibus', + 'Posuere velit aliquet.' + ); + + $this->obj->setErrors($newerrors); + + $this->assertEquals($this->obj->getErrors(), $newerrors); + } +} diff --git a/core/libraries/Hubzero/Base/Tests/HelpersTest.php b/core/libraries/Hubzero/Base/Tests/HelpersTest.php new file mode 100644 index 00000000000..c68a8388b0a --- /dev/null +++ b/core/libraries/Hubzero/Base/Tests/HelpersTest.php @@ -0,0 +1,85 @@ +assertInstanceOf('Hubzero\\Base\\Application', $app); + + $config = app('config'); + + $this->assertInstanceOf('Hubzero\\Config\\Repository', $config); + } + + /** + * Test config() + * + * @covers \config() + * @return void + **/ + public function testConfig() + { + $config = config(); + + $this->assertInstanceOf('Hubzero\\Config\\Repository', $config); + + $val = config('application_env'); + + $this->assertEquals($val, 'testing'); + + $val = config('bar', 'foo'); + + $this->assertEquals($val, 'foo'); + } + + /** + * Test with() + * + * @covers \with() + * @return void + **/ + public function testWith() + { + $obj = with(new \stdClass); + + $this->assertInstanceOf('stdClass', $obj); + + $obj = with(new \Hubzero\Base\Obj(array('foo' => 'bar'))); + + $this->assertInstanceOf('Hubzero\\Base\\Obj', $obj); + $this->assertEquals($obj->get('foo'), 'bar'); + } + + /** + * Test classExists() + * + * @covers \classExists() + * @return void + **/ + public function testClassExists() + { + $this->assertFalse(classExists('Hubzero\\Foo\\Bar')); + + $this->assertTrue(classExists('Hubzero\\Base\\Obj')); + } +} diff --git a/core/libraries/Hubzero/Base/Tests/ObjTest.php b/core/libraries/Hubzero/Base/Tests/ObjTest.php new file mode 100644 index 00000000000..f40154fc4ba --- /dev/null +++ b/core/libraries/Hubzero/Base/Tests/ObjTest.php @@ -0,0 +1,180 @@ + 'for the money', + 'two' => 'for the show', + 'three' => 'to get ready', + 'four' => 'to go' + ); + + /** + * Test __construct + * + * @covers \Hubzero\Base\Obj::__construct + * @return void + **/ + public function testConstructor() + { + $obj = new Obj($this->data); + + foreach ($this->data as $key => $datum) + { + $this->assertTrue(isset($obj->$key)); + $this->assertEquals($obj->$key, $datum); + } + + $obj2 = new Obj($obj); + + foreach ($this->data as $key => $datum) + { + $this->assertTrue(isset($obj2->$key)); + $this->assertEquals($obj2->$key, $datum); + } + } + + /** + * Test __toString + * + * @covers \Hubzero\Base\Obj::__toString + * @return void + **/ + public function testToString() + { + $obj = new Obj($this->data); + + $result = (string)$obj; + + $this->assertEquals($result, 'Hubzero\Base\Obj'); + } + + /** + * Test setProperties + * + * @covers \Hubzero\Base\Obj::setProperties + * @return void + **/ + public function testSetProperties() + { + $obj = new Obj(); + + $this->assertFalse($obj->setProperties('foo')); + $this->assertTrue($obj->setProperties($this->data)); + + foreach ($this->data as $key => $datum) + { + $this->assertTrue(isset($obj->$key)); + $this->assertEquals($obj->$key, $datum); + } + + $obj = new Obj(); + + $data = new \stdClass; + $data->one = 'for the money'; + $data->two = 'for the show'; + $data->three = 'to get ready'; + $data->four = 'to go'; + + $this->assertTrue($obj->setProperties($data)); + + foreach (get_object_vars($data) as $key => $datum) + { + $this->assertTrue(isset($obj->$key)); + $this->assertEquals($obj->$key, $datum); + } + } + + /** + * Test getProperties + * + * @covers \Hubzero\Base\Obj::getProperties + * @return void + **/ + public function testGetProperties() + { + $data = $this->data; + $data['_private'] = 'Private property'; + + $obj = new Obj($data); + + $prop = $obj->getProperties(); + + $this->assertTrue(is_array($prop)); + $this->assertCount(4, $prop); + + foreach ($prop as $key => $val) + { + $this->assertEquals($this->data[$key], $val); + } + } + + /** + * Test setting a property + * + * @covers \Hubzero\Base\Obj::set + * @return void + **/ + public function testSet() + { + $obj = new Obj(); + + $this->assertInstanceOf('Hubzero\Base\Obj', $obj->set('foo', 'bar')); + $this->assertTrue(isset($obj->foo)); + $this->assertEquals($obj->foo, 'bar'); + } + + /** + * Test retrieving a set property and + * retriving a default value if a property isn't set + * + * @covers \Hubzero\Base\Obj::get + * @return void + **/ + public function testGet() + { + $obj = new Obj(); + $obj->set('foo', 'bar'); + + $this->assertEquals($obj->get('foo'), 'bar'); + $this->assertEquals($obj->get('bar', 'default'), 'default'); + } + + /** + * Test setting a default value if not alreay assigned + * + * @covers \Hubzero\Base\Obj::def + * @return void + **/ + public function testDef() + { + $obj = new Obj(); + + $obj->def('bar', 'ipsum'); + + $this->assertEquals($obj->get('bar'), 'ipsum'); + + $obj->set('foo', 'bar'); + $obj->def('foo', 'lorem'); + + $this->assertEquals($obj->get('foo'), 'bar'); + } +} diff --git a/core/libraries/Hubzero/Base/Traits/AssetAware.php b/core/libraries/Hubzero/Base/Traits/AssetAware.php new file mode 100644 index 00000000000..0469790079f --- /dev/null +++ b/core/libraries/Hubzero/Base/Traits/AssetAware.php @@ -0,0 +1,161 @@ +detectExtensionName(); + + $attr = array_merge(array( + 'type' => 'text/css', + 'media' => null, + 'attribs' => array() + ), $attributes); + + $asset = new Stylesheet($extension, $stylesheet); + + $asset = $this->isSuperGroupAsset($asset); + + if ($asset->exists()) + { + if ($asset->isDeclaration()) + { + \App::get('document')->addStyleDeclaration($asset->contents()); + } + else + { + \App::get('document')->addStyleSheet($asset->link(), $attr['type'], $attr['media'], $attr['attribs']); + } + } + + return $this; + } + + /** + * Push JS to the document + * + * @param string $asset Script to add + * @param string $extension Extension name, e.g.: com_example, mod_example, plg_example_test + * @param array $attributes Attributes + * @return object + */ + public function js($asset = '', $extension = null, $attributes = array()) + { + $extension = $extension ?: $this->detectExtensionName(); + + $attr = array_merge(array( + 'type' => 'text/javascript', + 'defer' => false, + 'async' => false + ), $attributes); + + $asset = new Javascript($extension, $asset); + + $asset = $this->isSuperGroupAsset($asset); + + if ($asset->exists()) + { + if ($asset->isDeclaration()) + { + \App::get('document')->addScriptDeclaration($asset->contents()); + } + else + { + \App::get('document')->addScript($asset->link(), $attr['type'], $attr['defer'], $attr['async']); + } + } + + return $this; + } + + /** + * Get the path to an image + * + * @param string $asset Image name + * @param string $extension Extension name, e.g.: com_example, mod_example, plg_example_test + * @return string + */ + public function img($asset, $extension = null) + { + $extension = $extension ?: $this->detectExtensionName(); + + $asset = new Image($extension, $asset); + + return $asset->link(); + } + + /** + * Determine the extension the view is being called from + * + * @return string + */ + private function detectExtensionName() + { + if ($this instanceof Plugin) + { + return 'plg_' . $this->_type . '_' . $this->_name; + } + else if ($this instanceof ControllerInterface) + { + return \Request::getCmd('option', $this->_option); + } + else if ($this instanceof Module) + { + return $this->module->module; + } + + return ''; + } + + /** + * Modify paths if in a super group + * + * @param object $asset Asset + * @return object + */ + protected function isSuperGroupAsset($asset) + { + if ($asset->extensionType() != 'components' + || $asset->isDeclaration() + || $asset->isExternal()) + { + return $asset; + } + + if (defined('JPATH_GROUPCOMPONENT')) + { + $base = JPATH_GROUPCOMPONENT; + + $asset->setPath('source', $base . DS . 'assets' . DS . $asset->type() . DS . $asset->file()); + //$asset->setPath('source', $base . DS . $asset->file()); + } + + return $asset; + } +} diff --git a/core/libraries/Hubzero/Base/Traits/ErrorBag.php b/core/libraries/Hubzero/Base/Traits/ErrorBag.php new file mode 100644 index 00000000000..791bdf4010f --- /dev/null +++ b/core/libraries/Hubzero/Base/Traits/ErrorBag.php @@ -0,0 +1,102 @@ +_errors); + } + elseif (!array_key_exists($i, $this->_errors)) + { + // If $i has been specified but does not exist, return false + return false; + } + else + { + $error = $this->_errors[$i]; + } + + // Check if only the string is requested + if ($error instanceof Exception && $toString) + { + return (string) $error; + } + + return $error; + } + + /** + * Return all errors, if any. + * + * @return array Array of error messages + */ + public function getErrors() + { + return $this->_errors; + } + + /** + * Add an error message. + * + * @param string $error Error message. + * @param string $key Specific key to set the value to + * @return object + */ + public function setError($error, $key=null) + { + if ($key !== null) + { + $this->_errors[$key] = $error; + } + else + { + array_push($this->_errors, $error); + } + return $this; + } + + /** + * Set the list of errors + * + * @param array $errors List of Error message. + * @return object + */ + public function setErrors($errors) + { + $this->_errors = $errors; + return $this; + } +} diff --git a/core/libraries/Hubzero/Base/Traits/Escapable.php b/core/libraries/Hubzero/Base/Traits/Escapable.php new file mode 100644 index 00000000000..cb3d01d2b1a --- /dev/null +++ b/core/libraries/Hubzero/Base/Traits/Escapable.php @@ -0,0 +1,61 @@ +_escape, array('htmlspecialchars', 'htmlentities'))) + { + return call_user_func($this->_escape, $var, ENT_COMPAT, $this->_charset); + } + + return call_user_func($this->_escape, $var); + } + + /** + * Sets the _escape() callback. + * + * @param mixed $spec The callback for _escape() to use. + * @return object Chainable + */ + public function setEscape($spec) + { + $this->_escape = $spec; + return $this; + } +} diff --git a/core/libraries/Hubzero/Base/helpers.php b/core/libraries/Hubzero/Base/helpers.php new file mode 100644 index 00000000000..e72442c6548 --- /dev/null +++ b/core/libraries/Hubzero/Base/helpers.php @@ -0,0 +1,152 @@ +get($key); + } + + return \Hubzero\Facades\Facade::getApplication(); + } +} + +if (! function_exists('config')) +{ + /** + * Get the specified configuration value. + * + * Inspired by Laravel (http://laravel.com) + * + * @param mixed $key array|string + * @param mixed $default Default value if key isn't found + * @return mixed + */ + function config($key = null, $default = null) + { + if (is_null($key)) + { + return app('config'); + } + + return app('config')->get($key, $default); + } +} + +if (! function_exists('ddie')) +{ + /** + * Dump the passed variables and end the script. + * + * @param mixed + * @return void + */ + function ddie($var) + { + foreach (func_get_args() as $var) + { + \Hubzero\Debug\Dumper::dump($var); + } + die(); + } +} + +if (! function_exists('dlog')) +{ + /** + * Dump the passed variables to the debug bar. + * + * @param mixed + * @return void + */ + function dlog() + { + foreach (func_get_args() as $var) + { + \Hubzero\Debug\Dumper::log($var); + } + } +} + +if (! function_exists('dump')) +{ + /** + * Dump the passed variables. + * + * @param mixed + * @return void + */ + function dump($var) + { + foreach (func_get_args() as $var) + { + \Hubzero\Debug\Dumper::dump($var); + } + } +} + +if (! function_exists('with')) +{ + /** + * Return the given object. Useful for chaining. + * + * Inspired by Laravel (http://laravel.com) + * + * @param mixed $object + * @return mixed + */ + function with($object) + { + return $object; + } +} + +if (! function_exists('classExists')) +{ + /** + * Checks for the existence of the provided class without + * diving into the HUBzero Facade autoloader. + * + * @param string $classname The classname to look for + * @return bool + **/ + function classExists($classname) + { + $result = false; + + foreach (spl_autoload_functions() as $loader) + { + if (is_array($loader) && isset($loader[0]) && $loader[0] == 'Hubzero\Facades\Facade') + { + $autoloader = $loader; + break; + } + } + + if (isset($autoloader)) + { + spl_autoload_unregister($autoloader); + + $result = class_exists($classname); + + spl_autoload_register($autoloader); + } + + return $result; + } +} diff --git a/core/libraries/Hubzero/Browser/Detector.php b/core/libraries/Hubzero/Browser/Detector.php new file mode 100644 index 00000000000..a24b44e80f4 --- /dev/null +++ b/core/libraries/Hubzero/Browser/Detector.php @@ -0,0 +1,1214 @@ + '|Opera[/ ]([0-9.]+)|', + 'version' => '|Version[/ ]([0-9.]+)|', + 'name' => 'Opera', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|OPR[/ ]([0-9.]+)|', + 'name' => 'Opera', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|OPiOS[/ ]([0-9.]+)|', + 'name' => 'Opera Mini', + 'platform' => 'iOS', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '|Edge[/ ]([0-9.]+)|', + 'name' => 'Edge', + 'platform' => 'Windows', + 'mobile' => false, + 'engine' => 'Edge' + ), + array( + 'regex' => '|Vivaldi[/ ]([0-9.]+)|', + 'name' => 'Vivaldi', + 'platform' => 'Windows', + 'mobile' => false, + 'engine' => 'Blink' + ), + array( + 'regex' => '|YaBrowser[/ ]([0-9.]+)|', + 'name' => 'Yandex', + 'platform' => 'Mac OS', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Yowser[/ ]([0-9.]+)|', + 'name' => 'Yandex', + 'platform' => 'Mac OS', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Chrome[/ ]([0-9.]+)|', + 'name' => 'Chrome', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|CrMo[/ ]([0-9.]+)|', + 'name' => 'Chrome', + 'platform' => '', + 'mobile' => false, + 'engine' => 'WebKit' + ), + array( + 'regex' => '|CriOS[/ ]([0-9.]+)|', + 'name' => 'Chrome', + 'platform' => '', + 'mobile' => true, + 'engine' => 'WebKit' + ), + array( + 'regex' => '|MSIE ([0-9.]+)|', + 'name' => 'MSIE', + 'platform' => 'Windows', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Internet Explorer/([0-9.]+)|', + 'name' => 'MSIE', + 'platform' => 'Windows', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|elaine/|', + 'name' => 'Palm', + 'platform' => '', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '|palmsource|', + 'name' => 'Palm', + 'platform' => '', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '|digital paths|', + 'name' => 'Palm', + 'platform' => '', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '|amaya/([0-9.]+)|', + 'name' => 'Amaya', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|ANTFresco/([0-9]+)|', + 'name' => 'Fresco', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|avantgo|', + 'name' => 'Avantgo', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Android|', + 'name' => 'Android', + 'platform' => 'Android', + 'mobile' => true, + 'engine' => 'WebKit' + ), + array( + 'regex' => '|[Kk]onqueror/([0-9]+)|', + 'name' => 'Konqueror', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Safari/([0-9]+)\.?([0-9]+)?|', + 'version' => '|Version[/ ]([0-9.]+)|', + 'name' => 'Safari', + 'platform' => '', + 'mobile' => false, + 'engine' => 'WebKit' + ), + array( + 'regex' => '|Iceweasel/([0-9.]+)|', + 'name' => 'Iceweasel', + 'platform' => 'Linux', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Firefox/([0-9.]+)|', + 'name' => 'Firefox', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Mozilla/([0-9.]+)|', + 'name' => 'Mozilla', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Lynx/([0-9]+)|', + 'name' => 'Lynx', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Links \(([0-9]+)|', + 'name' => 'Links', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|HotJava/([0-9]+)|', + 'name' => 'HotJava', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|UP[\/\.B\.L]|', + 'name' => 'Up', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ), + array( + 'regex' => '|Xiino/|', + 'name' => 'Xiino', + 'platform' => '', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '|Nokia|', + 'name' => 'Nokia', + 'platform' => '', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '|Ericsson|', + 'name' => 'Ericsson', + 'platform' => '', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '/BlackBerry|PlayBook|BB10/', + 'name' => 'BlackBerry', + 'platform' => '', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '|MOT-|', + 'name' => 'Motorola', + 'platform' => '', + 'mobile' => true, + 'engine' => '' + ), + array( + 'regex' => '/docomo|portalmmm/', + 'name' => 'imode', + 'platform' => '', + 'mobile' => false, + 'engine' => '' + ) + ); + + /** + * Browser instances container. + * + * @var array + */ + protected static $instances = array(); + + /** + * Create a browser instance (constructor). + * + * @param string $userAgent The browser string to parse. + * @param string $accept The HTTP_ACCEPT settings to use. + * @return void + */ + public function __construct($userAgent = null, $accept = null) + { + $this->match($userAgent, $accept); + } + + /** + * Returns the global Browser object, only creating it + * if it doesn't already exist. + * + * @param string $userAgent The browser string to parse. + * @param string $accept The HTTP_ACCEPT settings to use. + * @return object The Browser object. + */ + public static function getInstance($userAgent = null, $accept = null) + { + $signature = serialize(array($userAgent, $accept)); + + if (empty(self::$instances[$signature])) + { + self::$instances[$signature] = new self($userAgent, $accept); + } + + return self::$instances[$signature]; + } + + /** + * Parses the user agent string and inititializes the object with + * all the known features and quirks for the given browser. + * + * @param string $userAgent The browser string to parse. + * @param string $accept The HTTP_ACCEPT settings to use. + * @return void + */ + public function match($userAgent = null, $accept = null) + { + // Set our agent string. + if (is_null($userAgent)) + { + if (isset($_SERVER['HTTP_USER_AGENT'])) + { + $this->agent = trim($_SERVER['HTTP_USER_AGENT']); + } + } + else + { + $this->agent = $userAgent; + } + + $this->lowerAgent = strtolower($this->agent); + + // Set our accept string. + if (is_null($accept)) + { + if (isset($_SERVER['HTTP_ACCEPT'])) + { + $this->accept = strtolower(trim($_SERVER['HTTP_ACCEPT'])); + } + } + else + { + $this->accept = strtolower($accept); + } + + if (!empty($this->agent)) + { + $this->_setPlatform(); + + foreach ($this->regexes as $regex) + { + if (preg_match($regex['regex'], $this->agent, $version)) + { + $this->browser = strtolower($regex['name']); + $this->platform = ($regex['platform'] ? $regex['platform'] : $this->platform); + if (!$this->mobile) + { + $this->mobile = $regex['mobile']; + } + + if (isset($regex['version'])) + { + if (preg_match($regex['version'], $this->agent, $ver)) + { + $version = $ver; + } + } + + if (!empty($version) && isset($version[1])) + { + $bits = explode('.', $version[1]); + + $this->majorVersion = $bits[0]; + $this->minorVersion = (isset($bits[1]) ? $bits[1] : 0); + } + + break; + } + } + } + } + + /** + * Match the platform of the browser. + * + * This is a pretty simplistic implementation, but it's intended + * to let us tell what line breaks to send, so it's good enough + * for its purpose. + * + * @return void + */ + protected function _setPlatform() + { + $this->device = 'computer'; + + // Determine platform + // + // packs the os array + // use this order since some navigator user agents will put 'macintosh' in the navigator user agent string + // which would make the nt test register true + $a_mobile = array( + 'ios', 'android', 'blackberry os', 'symbian os', 'web os' //, 'windows' + ); + + $a_mac = array( + 'mac68k', 'macppc' + ); // this is not used currently + // same logic, check in order to catch the os's in order, last is always default item + $a_unix = array( + 'unixware', 'solaris', 'sunos', 'sun4', 'sun5', 'suni86', 'sun', + 'freebsd', 'openbsd', 'bsd' , 'irix5', 'irix6', 'irix', 'hpux9', + 'hpux10', 'hpux11', 'hpux', 'hp-ux', 'aix1', 'aix2', 'aix3', 'aix4', + 'aix5', 'aix', 'sco', 'unixware', 'mpras', 'reliant', 'dec', 'sinix', + 'unix' + ); + // only sometimes will you get a linux distro to id itself... + $a_linux = array( + 'kanotix', 'ubuntu', 'mepis', 'debian', 'suse', 'redhat', 'slackware', + 'mandrake', 'gentoo', 'linux' + ); + // note, order of os very important in os array, you will get failed ids if changed + $a_os = array( + 'beos', 'os2', 'amiga', 'webtv', 'android', 'iphone', 'ipad', 'mac', 'nt', 'win', + $a_unix, + $a_linux + ); + + //os tester + for ($i = 0; $i < count($a_os); $i++) + { + //unpacks os array, assigns to variable + $s_os = $a_os[$i]; + + //assign os to global os variable, os flag true on success + //!stristr($browser_string, "linux") corrects a linux detection bug + if (stristr($this->lowerAgent, 'android')) + { + $this->platform = 'Android'; + } + else if (!is_array($s_os) && stristr($this->lowerAgent, $s_os) && !stristr($this->lowerAgent, 'linux')) + { + $this->platform = $s_os; + + switch ($this->platform) + { + case 'ipad': + case 'iphone': + $this->platform = 'iOS'; + break; + + case 'win': + $this->platform = 'Windows'; + if (stristr($this->lowerAgent, '95')) + { + $this->platformVersion = '95'; + } + elseif ((stristr($this->lowerAgent, '9x 4.9')) || (stristr($this->lowerAgent, 'me'))) + { + $this->platformVersion = 'me'; + } + elseif (stristr($this->lowerAgent, '98')) + { + $this->platformVersion = '98'; + } + elseif (stristr($this->lowerAgent, '2000')) // windows 2000, for opera ID + { + $this->platformVersion = 5.0; + //$this->platform .= ' NT'; + } + elseif (stristr($this->lowerAgent, 'xp')) // windows 2000, for opera ID + { + $this->platformVersion = 5.1; + //$this->platform .= ' NT'; + } + elseif (stristr($this->lowerAgent, '2003')) // windows server 2003, for opera ID + { + $this->platformVersion = 5.2; + //$this->platform .= ' NT'; + } + elseif (stristr($this->lowerAgent, 'ce')) // windows CE + { + $this->platformVersion = 'ce'; + } + break; + + case 'nt': + $this->platform = 'Windows'; + if (stristr($this->lowerAgent, 'nt 5.2')) // windows server 2003 + { + $this->platformVersion = 5.2; + } + elseif (stristr($this->lowerAgent, 'nt 5.1') || stristr($this->lowerAgent, 'xp')) // windows xp + { + //$this->platformVersion = 5.1; + $this->platformVersion = 'XP'; + $this->platform = 'Windows'; + } + elseif (stristr($this->lowerAgent, 'nt 5') || stristr($this->lowerAgent, '2000')) // windows 2000 + { + //$this->platformVersion = 5.0; + $this->platformVersion = '2000'; + $this->platform = 'Windows'; + } + elseif (stristr($this->lowerAgent, 'nt 4')) // nt 4 + { + $this->platformVersion = 4; + } + elseif (stristr($this->lowerAgent, 'nt 3')) // nt 4 + { + $this->platformVersion = 3; + } else { + $this->platformVersion = ''; + } + break; + + case 'mac': + $this->platform = 'Mac OS'; + if (stristr($this->lowerAgent, 'os x')) + { + $this->platformVersion = 10; + } + // this is a crude test for os x, since safari, camino, ie 5.2, & moz >= rv 1.3 + // are only made for os x + /*elseif (($browser == 'safari') || ($browser == 'camino') || ($browser == 'shiira') || + (($browser == 'mozilla') && ($browser_ver >= 1.3)) || + (($browser == 'msie') && ($browser_ver >= 5.2))) + { + $this->platformVersion = 10; + }*/ + break; + + default: + break; + } + break; + } + // check that it's an array, check it's the second to last item + // in the main os array, the unix one that is + elseif (is_array($s_os) && ($i == (count($a_os) - 2))) + { + for ($j = 0; $j < count($s_os); $j++) + { + if (stristr($this->lowerAgent, $s_os[$j])) + { + $this->platform = 'Unix'; // if the os is in the unix array, it's unix, obviously... + $this->platformVersion = ($s_os[$j] != 'unix') ? $s_os[$j] : ''; // assign sub unix version from the unix array + break; + } + } + } + // check that it's an array, check it's the last item + // in the main os array, the linux one that is + elseif (is_array($s_os) && ($i == (count($a_os) - 1))) + { + for ($j = 0; $j < count($s_os); $j++) + { + if (stristr($this->lowerAgent, $s_os[$j])) + { + $this->platform = 'Linux'; + // assign linux distro from the linux array, there's a default + //search for 'lin', if it's that, set version to '' + $this->platformVersion = ($s_os[$j] != 'linux') ? $s_os[$j] : ''; + break; + } + } + } + } + + // if we're on iOS + if (in_array(strtolower($this->platform), $a_mobile)) + { + $this->mobile = true; + $this->device = 'phone'; + + if (preg_match('/iphone/i', strtolower($this->lowerAgent))) + { + $this->device = 'iPhone'; + } + if (preg_match('/ipad/i', strtolower($this->lowerAgent))) + { + $this->device = 'iPad'; + } + } + + if (strtolower($this->platform) == 'ios') + { + if (preg_match('/OS (\d\w\d)/i', $this->lowerAgent, $matches)) + { + if (isset($matches[1])) + { + $v = explode('_', $matches[1]); + $this->platformVersion = $v[0] . '.' . $v[1]; + } + } + } + + return $this; + } + + /** + * Return the currently matched device. + * + * @return string The user's device. + */ + public function device() + { + return $this->device; + } + + /** + * Return the currently matched platform. + * + * @return string The user's platform. + */ + public function platform() + { + return $this->platform; + } + + /** + * Return the currently matched platform. + * + * @return string The user's platform. + */ + public function platformVersion() + { + return $this->platformVersion; + } + + /** + * Retrieve the current browser. + * + * @return string The current browser. + */ + public function name() + { + return $this->browser; + } + + /** + * Retrieve the current browser's major version. + * + * @return integer The current browser's major version + */ + public function major() + { + return $this->majorVersion; + } + + /** + * Retrieve the current browser's minor version. + * + * @return integer The current browser's minor version. + */ + public function minor() + { + return $this->minorVersion; + } + + /** + * Retrieve the current browser's version. + * + * @return string The current browser's version. + */ + public function version($for='') + { + switch (strtolower($for)) + { + case 'major': + return $this->major(); + break; + + case 'minor': + return $this->minor(); + break; + + case 'platform': + return $this->platformVersion(); + break; + + default: + return $this->majorVersion . '.' . $this->minorVersion; + break; + } + } + + /** + * Return the full browser agent string. + * + * @return string The browser agent string + */ + public function agent() + { + return $this->agent; + } + + /** + * Determine if the given browser is the same as the current. + * + * @param string $browser The browser to check. + * @return boolean Is the given browser the same as the current? + */ + public function isBrowser($browser) + { + $browser = strtolower($browser); + return ($this->browser === $browser); + } + + /** + * Determines if the browser is a robot or not. + * + * @return boolean True if browser is a known robot. + */ + public function isRobot() + { + foreach ($this->robots as $robot) + { + //if (strpos($this->agent, $robot) !== false) + if (preg_match('/' . $robot . '/', $this->agent)) + { + return true; + } + } + + return false; + } + + /** + * Determines if the browser is mobile version or not. + * + * @return boolean True if browser is a known mobile version. + */ + public function isMobile() + { + return $this->mobile; + } +} diff --git a/core/libraries/Hubzero/Browser/Tests/DetectorTest.php b/core/libraries/Hubzero/Browser/Tests/DetectorTest.php new file mode 100644 index 00000000000..e33bb76c056 --- /dev/null +++ b/core/libraries/Hubzero/Browser/Tests/DetectorTest.php @@ -0,0 +1,168 @@ +string); + + $this->assertEquals($userAgentString->string, $browser->agent()); + $this->assertEquals(strtolower($userAgentString->browser), $browser->name()); + $this->assertEquals($userAgentString->browserVersion, $browser->version()); + $this->assertEquals($userAgentString->os, $browser->platform()); + + list($major, $minor) = explode('.', $userAgentString->browserVersion); + + $this->assertEquals(intval($major), $browser->major()); + $this->assertEquals(intval($major), $browser->version('major')); + + $this->assertEquals(intval($minor), $browser->minor()); + $this->assertEquals(intval($minor), $browser->version('minor')); + } + } + + /** + * Tests the isBrowser() method. + * + * @covers \Hubzero\Browser\Detector::isBrowser + * @return void + **/ + public function testIsBrowser() + { + $browser = new Detector('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.132 Safari/537.36 OPR/21.0.1432.67'); + + $this->assertTrue($browser->isBrowser('Opera')); + $this->assertFalse($browser->isBrowser('Chrome')); + + $browser = new Detector('Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/43.0.2357.51 Mobile/12B440 Safari/600.1.4'); + + $this->assertTrue($browser->isBrowser('Chrome')); + $this->assertFalse($browser->isBrowser('Safari')); + } + + /** + * Tests the isMobile() method. + * + * @covers \Hubzero\Browser\Detector::isMobile + * @return void + **/ + public function testIsMobile() + { + $uas = self::map(); + + foreach ($uas as $userAgentString) + { + $browser = new Detector($userAgentString->string); + + if ($userAgentString->device == 'iPhone' || $userAgentString->device == 'iPad' || $userAgentString->device == 'phone') + { + $this->assertTrue($browser->isMobile()); + } + else + { + $this->assertFalse($browser->isMobile()); + } + } + } + + /** + * Tests the isRobot() method. + * + * @covers \Hubzero\Browser\Detector::isRobot + * @return void + **/ + public function testIsRobot() + { + $uas = array( + 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0); 360Spider' => true, + 'Mozilla/5.0 (compatible; alexa site audit/1.0; http://www.alexa.com/help/webmasters; )' => true, + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10 _1) AppleWebKit/600.2.5 (KHTML, like Gecko) Version/8.0.2 Safari/600.2.5 (Applebot/0.1; +http://www.apple.com/go/applebot)' => true, + 'Mozilla/2.0 (compatible; Ask Jeeves/Teoma)' => true, + 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Safari/537.36' => true, + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.89 Vivaldi/1.0.83.38 Safari/537.36' => false, + 'Mozilla/5.0 (AmigaOS; U; AmigaOS 1.3; en-US; rv:1.8.1.21) Gecko/20090303 SeaMonkey/1.1.15' => false, + 'Mozilla/5 (X11; Linux x86_64) AppleWebKit/537.4 (KHTML like Gecko) Arch Linux Firefox/23.0 Xfce' => false, + 'Mozilla/5.0 (X11; CrOS x86_64 4731.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.67 Safari/537.36' => false + ); + + foreach ($uas as $userAgentString => $isRobot) + { + $browser = new Detector($userAgentString); + + if ($isRobot) + { + $this->assertTrue($browser->isRobot()); + } + else + { + $this->assertFalse($browser->isRobot()); + } + } + } + + /** + * Map test data + * + * @return array + */ + private static function map() + { + $collection = array(); + + $xml = new SimpleXmlElement(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'UserAgentStrings.xml')); + + if ($xml) + { + foreach ($xml->strings->string as $string) + { + $string = $string->field; + + $userAgentString = new stdClass(); + $userAgentString->browser = (string)$string[0]; + $userAgentString->browserVersion = (string)$string[1]; + $userAgentString->os = (string)$string[2]; + $userAgentString->osVersion = (string)$string[3]; + $userAgentString->device = (string)$string[4]; + $userAgentString->deviceVersion = (string)$string[5]; + $userAgentString->string = str_replace(array(PHP_EOL, ' '), ' ', (string)$string[6]); + + $collection[] = $userAgentString; + } + } + + return $collection; + } +} diff --git a/core/libraries/Hubzero/Browser/Tests/Fixtures/UserAgentStrings.xml b/core/libraries/Hubzero/Browser/Tests/Fixtures/UserAgentStrings.xml new file mode 100644 index 00000000000..28c65dfe480 --- /dev/null +++ b/core/libraries/Hubzero/Browser/Tests/Fixtures/UserAgentStrings.xml @@ -0,0 +1,146 @@ + + + + + Opera + 21.0 + Mac OS + 10.9.3 + unknown + unknown + + Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) + Chrome/34.0.1847.132 Safari/537.36 OPR/21.0.1432.67 + + + + Safari + 4.0 + iOS + 3.2 + iPad + unknown + + Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) + Version/4.0.4 Mobile/7B314 Safari/531.21.10gin_lib.cc + + + + Safari + 8.0 + iOS + 8.1.2 + iPhone + unknown + + Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) + Version/8.0 Mobile/12B440 Safari/600.1.4 + + + + Chrome + 41.0 + Mac OS + 10.10.2 + unknown + unknown + + Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) + Chrome/41.0.2272.118 Safari/537.36 + + + + Yandex + 15.6 + Mac OS + 10.10.2 + unknown + unknown + + Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) + Chrome/42.0.2311.152 YaBrowser/15.6.2311.3451 (beta) Yowser/2.0 Safari/537.36 + + + + MSIE + 8.0 + Windows + 7 + unknown + unknown + + Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; + .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0) + + + + Firefox + 35.0 + Mac OS + 10.10 + unknown + unknown + + Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:35.0) Gecko/20100101 Firefox/35.0 + + + + Opera Mini + 10.1 + iOS + 8.1.2 + iPhone + unknown + + Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) + OPiOS/10.1.1.92212 Mobile/12B440 Safari/9537.53 + + + + Chrome + 43.0 + iOS + 8.1.2 + iPhone + unknown + + Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) + CriOS/43.0.2357.51 Mobile/12B440 Safari/600.1.4 + + + + Edge + 12.10136 + Windows + 10.0 + unknown + unknown + + Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 + Edge/12.10136 + + + + Firefox + 40.0 + Windows + 10.0 + unknown + unknown + + Mozilla/5.0 (Windows NT 10.0; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0 + + + + Vivaldi + 1.0 + Windows + 7 + unknown + unknown + + Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.89 Vivaldi/1.0.83.38 Safari/537.36 + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Cache/Auditor.php b/core/libraries/Hubzero/Cache/Auditor.php new file mode 100644 index 00000000000..598460ab1ea --- /dev/null +++ b/core/libraries/Hubzero/Cache/Auditor.php @@ -0,0 +1,58 @@ +group = $group; + } + + /** + * Increase cache items count. + * + * @param string $size Cached item size + * @return void + */ + public function tally($size) + { + $this->size = number_format($this->size + $size, 2, '.', ''); + $this->count++; + } +} diff --git a/core/libraries/Hubzero/Cache/Manager.php b/core/libraries/Hubzero/Cache/Manager.php new file mode 100644 index 00000000000..eb8b9f98d05 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Manager.php @@ -0,0 +1,247 @@ +app = $app; + } + + /** + * Get a cache store instance by name. + * + * @param mixed $name string|null + * @return mixed + */ + public function storage($name = null) + { + $name = $name ?: $this->getDefaultDriver(); + + return $this->stores[$name] = (isset($this->stores[$name]) ? $this->stores[$name] : $this->resolve($name)); + } + + /** + * Resolve the given storage handler. + * + * @param string $name + * @return object + */ + protected function resolve($name) + { + $config = $this->getConfig($name); + + if (is_null($config)) + { + throw new InvalidArgumentException('Cache config is not defined.'); + } + + if (!isset($config['hash'])) + { + $config['hash'] = $this->app->hash(''); + } + if (!isset($config['cachebase'])) + { + $config['cachebase'] = PATH_APP . DS . 'cache' . DS . (isset($this->app['client']->alias) ? $this->app['client']->alias : $this->app['client']->name); + } + + if (isset($this->customCreators[$name])) + { + $config['cache_handler'] = $name; + + return $this->callCustomCreator($config); + } + else + { + $class = __NAMESPACE__ . '\\Storage\\' . ucfirst($name); + + if (!class_exists($class)) + { + throw new InvalidArgumentException("Cache store [{$name}] is not defined."); + } + + return new $class((array) $config); + } + } + + /** + * Call a custom driver creator. + * + * @param array $config + * @return mixed + */ + protected function callCustomCreator($config) + { + return $this->customCreators[$config['cache_handler']]($config); + } + + /** + * Get the cache connection configuration. + * + * @param string $name + * @return array + */ + protected function getConfig($name) + { + return $this->app['config']->get($name, array()); + } + + /** + * Get the default cache driver name. + * + * @return string + */ + public function getDefaultDriver() + { + return $this->app['config']['cache_handler']; + } + + /** + * Set the default cache driver name. + * + * @param string $name + * @return void + */ + public function setDefaultDriver($name) + { + $this->app['config']['cache_handler'] = $name; + } + + /** + * Register a custom driver creator Closure. + * + * @param string $driver + * @param object $callback + * @return object + */ + public function extend($driver, Closure $callback) + { + $this->customCreators[$driver] = $callback; + + return $this; + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) + { + $value = $this->storage()->get($key); + + if (!is_null($value)) + { + return $value; + } + + return $default instanceof Closure ? $default() : $default; + } + + /** + * Retrieve an item from the cache and delete it. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function pull($key, $default = null) + { + $value = $this->get($key, $default); + + $this->storage()->forget($key); + + return $value; + } + + /** + * Dynamically call the default driver instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return call_user_func_array(array($this->storage(), $method), $parameters); + } + + /** + * Get a list of available cache stores. + * + * @return array + * @since 2.1.12 + */ + public static function getStores() + { + // Instantiate variables + $stores = []; + + // Get a list of types, only including php files + $types = glob(__DIR__ . DIRECTORY_SEPARATOR . 'Storage' . DIRECTORY_SEPARATOR . '*.php'); + + // Loop through the types and find the ones that are available + foreach ($types as $type) + { + // Get just the file name + $type = basename($type); + + // Derive the class name from the type + $class = __NAMESPACE__ . '\\Storage\\' . str_ireplace('.php', '', ucfirst(trim($type))); + + // If the class doesn't exist...these are not the droids you're looking for... + if (!class_exists($class)) + { + continue; + } + + // Our class exists, so now we just need to know if it passes it's test method + if (call_user_func_array(array($class, 'isAvailable'), array())) + { + // Connector names should not have file extensions + $stores[] = str_ireplace('.php', '', $type); + } + } + + $stores = array_map('strtolower', $stores); + + return $stores; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/Apc.php b/core/libraries/Hubzero/Cache/Storage/Apc.php new file mode 100644 index 00000000000..c49b794eb4d --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/Apc.php @@ -0,0 +1,218 @@ +apcu = function_exists('apcu_fetch'); + + if (!self::isAvailable()) + { + throw new RuntimeException('Cannot use Apc cache storage. Apc extension is not loaded.'); + } + } + + /** + * Test to see if the cache storage is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return extension_loaded('apc'); + } + + /** + * Add an item to the cache only if it doesn't already exist + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function add($key, $value, $minutes) + { + $key = $this->id($key); + $seconds = $minutes * 60; + + return $this->apcu ? apcu_add($key, $value, $seconds) : apc_add($key, $value, $seconds); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + $key = $this->id($key); + $seconds = $minutes * 60; + + return $this->apcu ? apcu_store($key, $value, $seconds) : apc_store($key, $value, $seconds); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + $key = $this->id($key); + return $this->apcu ? apcu_fetch($key) : apc_fetch($key); + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + $key = $this->id($key); + return $this->apcu ? apcu_exists($key) : apc_exists($key); + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + $key = $this->id($key); + return $this->apcu ? apcu_delete($key) : apc_delete($key); + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $hash = $this->options['hash']; + + $allinfo = $this->apcu ? apcu_cache_info() : apc_cache_info('user'); + + foreach ($allinfo['cache_list'] as $key) + { + if (strpos($key['info'], $hash . '-cache-' . $group . '-') === 0) // xor $mode != 'group') + { + $this->apcu ? apcu_delete($key['info']) : apc_delete($key['info']); + } + } + return true; + } + + /** + * Force garbage collect expired cache data as items are removed only on fetch! + * + * @return boolean True on success, false otherwise. + */ + public function gc() + { + $hash = $this->options['hash']; + + $allinfo = $this->apcu ? apcu_cache_info() : apc_cache_info('user'); + + $keys = $allinfo['cache_list']; + + foreach ($allinfo['cache_list'] as $key) + { + if (strpos($key['info'], $hash . '-cache-')) + { + $this->apcu ? apcu_fetch($key['info']) : apc_fetch($key['info']); + } + } + } + + /** + * Get all cached data + * + * @return array + */ + public function all() + { + $allinfo = $this->apcu ? apcu_cache_info() : apc_cache_info('user'); + + $keys = $allinfo['cache_list']; + + $hash = $this->options['hash']; + + $data = array(); + + foreach ($keys as $key) + { + $name = $key['info']; + $namearr = explode('-', $name); + + if ($namearr !== false && $namearr[0] == $hash && $namearr[1] == 'cache') + { + $group = $namearr[2]; + + if (!isset($data[$group])) + { + $item = new Auditor($group); + } + else + { + $item = $data[$group]; + } + + $item->tally($key['mem_size'] / 1024); + + $data[$group] = $item; + } + } + + return $data; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/CacheLite.php b/core/libraries/Hubzero/Cache/Storage/CacheLite.php new file mode 100644 index 00000000000..9fabe52bb03 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/CacheLite.php @@ -0,0 +1,346 @@ +directory = $this->cleanPath($this->options['cachebase']); + + if (!is_dir($this->directory) || !is_readable($this->directory) || !is_writable($this->directory)) + { + throw new RuntimeException('Cache path should be directory with available read/write access.'); + } + + if (isset($this->options['engine'])) + { + $this->engine = $this->options['engine']; + } + + if (!$this->engine) + { + $cloptions = array( + 'cacheDir' => $this->directory . DS, + 'lifeTime' => isset($this->options['lifetime']) ? $this->options['lifetime'] : 15, + 'fileLocking' => isset($this->options['locking']) ? $this->options['locking'] : false, + 'automaticCleaningFactor' => isset($this->options['autoclean']) ? $this->options['autoclean'] : 200, + 'fileNameProtection' => false, + 'hashedDirectoryLevel' => 0, + 'caching' => true + ); + + $this->engine = $this->getEngine($options); + } + } + + /** + * Test to see if the cache storage is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + @include_once 'Cache' . DS . 'Lite.php'; + + if (class_exists('Cache_Lite')) + { + return true; + } + + return false; + } + + /** + * Get a new Cache_Lite instance + * + * @param array $options + * @return object Cache_Lite + */ + public function getEngine($options = array()) + { + return new Cache_Lite($options); + } + + /** + * Add an item to the cache only if it doesn't already exist + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function add($key, $value, $minutes) + { + if ($this->has($key)) + { + return false; + } + + return $this->put($key, $value, $minutes); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + @list($group, $name) = $this->id($key); + + $dir = $this->directory . DS . $group; + + // If the folder doesn't exist try to create it + if (!is_dir($dir)) + { + // Make sure the index file is there + $indexFile = $dir . DS . 'index.html'; + @mkdir($dir) && file_put_contents($indexFile, ''); + } + + // Make sure the folder exists + if (!is_dir($dir)) + { + return false; + } + + $this->engine->setOption('cacheDir', $this->directory . DS . $group . DS); + + return $this->engine->save($value, $name, $group); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + @list($group, $name) = $this->id($key); + + $this->engine->setOption('cacheDir', $this->directory . DS . $group . DS); + + return $this->engine->get($name, $group); + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + $key = $this->id($key); + + if (eaccelerator_get($key) !== null) + { + return false; + } + + return true; + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + @list($group, $name) = $this->id($key); + + $this->engine->setOption('cacheDir', $this->directory . DS . $group . DS); + + $success = $this->engine->remove($name, $group); + + if ($success == true) + { + return $success; + } + + return false; + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $success = true; + + if (is_dir($this->directory . DS . $group)) + { + $this->engine->setOption('cacheDir', $this->directory . DS . $group . DS); + $success = $this->engine->clean($group, 'group'); + } + + if ($success == true) + { + return $success; + } + + return false; + } + + /** + * Garbage collect expired cache data + * + * @return boolean True on success, false otherwise. + */ + public function gc() + { + $this->engine->setOption('automaticCleaningFactor', 1); + $this->engine->setOption('hashedDirectoryLevel', 1); + + $success1 = $this->engine->clean($this->directory . DS, false, 'old'); + + if (!($dh = opendir($this->directory . DS))) + { + return false; + } + + $result = true; + + while ($file = readdir($dh)) + { + if ($file != '.' && $file != '..' && $file != '.svn') + { + $file2 = $this->directory . DS . $file; + + if (is_dir($file2)) + { + $result = ($result and $this->engine->clean($file2 . DS, false, 'old')); + } + } + } + + $success = ($success1 && $result); + + return $success; + } + + /** + * Get all cached data + * + * @return array + */ + public function all() + { + $path = $this->directory; + + $data = array(); + + $dirIterator = new \DirectoryIterator($path); + foreach ($dirIterator as $folder) + { + if ($folder->isDot() || !$folder->isDir()) + { + continue; + } + + $name = $folder->getFilename(); + + $item = new Auditor($name); + + $files = new \DirectoryIterator($path . DS . $name); + foreach ($files as $file) + { + if ($folder->isDot() || $folder->isDir()) + { + continue; + } + + $item->tally(filesize($path . '/' . $name . '/' . $file->getFilename()) / 1024); + } + + $data[$name] = $item; + } + + return $data; + } + + /** + * Get the full path for the given cache key. + * + * @param string $key + * @return array + */ + protected function id($key) + { + $parts = explode('.', $key); + + $name = array_pop($parts); + $group = implode('.', $parts); + + return array($group, $name); + } + + /** + * Strip additional / or \ in a path name + * + * @param string $path The path to clean + * @param string $ds Directory separator (optional) + * @return string The cleaned path + */ + protected function cleanPath($path, $ds = DIRECTORY_SEPARATOR) + { + $path = trim($path); + + // Remove double slashes and backslahses and convert + // all slashes and backslashes to DIRECTORY_SEPARATOR + return preg_replace('#[/\\\\]+#', $ds, $path); + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/Eaccelerator.php b/core/libraries/Hubzero/Cache/Storage/Eaccelerator.php new file mode 100644 index 00000000000..9231cdb074d --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/Eaccelerator.php @@ -0,0 +1,215 @@ +has($key)) + { + return false; + } + + return $this->put($key, $value, $minutes); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + return eaccelerator_put($this->id($key), $value, $minutes * 60); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + return eaccelerator_get($this->id($key)); + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + $key = $this->id($key); + + if (eaccelerator_get($key) !== null) + { + return false; + } + + return true; + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + return eaccelerator_rm($this->id($key)); + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $keys = eaccelerator_list_keys(); + + if (is_array($keys)) + { + $hash = $this->options['hash']; + + foreach ($keys as $key) + { + // Trim leading ":" to work around list_keys namespace bug in eAcc. + // This will still work when bug is fixed. + $key['name'] = ltrim($key['name'], ':'); + + if (strpos($key['name'], $hash . '-cache-' . $group . '-') === 0) // xor $mode != 'group') + { + eaccelerator_rm($key['name']); + } + } + } + + return true; + } + + /** + * Garbage collect expired cache data + * + * @return boolean True on success, false otherwise. + */ + public function gc() + { + return eaccelerator_gc(); + } + + /** + * Get all cached data + * + * @return array + */ + public function getAll() + { + $keys = eaccelerator_list_keys(); + + $hash = $this->options['hash']; + + $data = array(); + + foreach ($keys as $key) + { + // Trim leading ":" to work around list_keys namespace bug in eAcc. + // This will still work when bug is fixed. + $name = ltrim($key['name'], ':'); + $namearr = explode('-', $name); + + if ($namearr !== false && $namearr[0] == $hash && $namearr[1] == 'cache') + { + $group = $namearr[2]; + + if (!isset($data[$group])) + { + $item = new Auditor($group); + } + else + { + $item = $data[$group]; + } + + $item->tally($key['size'] / 1024); + + $data[$group] = $item; + } + } + + return $data; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/File.php b/core/libraries/Hubzero/Cache/Storage/File.php new file mode 100644 index 00000000000..075867c7d4b --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/File.php @@ -0,0 +1,437 @@ +options['chmod'])) + { + $this->options['chmod'] = null; + } + + $this->directory = $this->cleanPath($this->options['cachebase']); + + if (!is_dir($this->directory)) + { + mkdir($this->directory, 0775); + } + + if (!is_dir($this->directory) || !is_readable($this->directory) || !is_writable($this->directory)) + { + throw new RuntimeException('Cache path should be directory with available read/write access.'); + } + } + + /** + * Test to see if the cache storage is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + $conf = new \Hubzero\Config\Repository('site'); + return is_writable($conf->get('cache_path', PATH_APP . DIRECTORY_SEPARATOR . 'cache')); + } + + /** + * Add an item to the cache only if it doesn't already exist + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function add($key, $value, $minutes) + { + if ($this->has($key)) + { + return false; + } + + return $this->put($key, $value, $minutes); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + $file = $this->path($key); + + $data = array( + 'time' => time(), + 'value' => $value, + 'ttl' => $this->expiration($minutes) + ); + + return $this->writeCacheFile($file, $data); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + $file = $this->path($key); + + if (!file_exists($file)) + { + return null; + } + + $data = @unserialize(file_get_contents($file)); + + if (!$data) + { + throw new RuntimeException('Cache file is invalid.'); + } + + if ($this->isDataExpired($data)) + { + $this->forget($key); + return null; + } + + return $data['value']; + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + $file = $this->path($key); + + if (!file_exists($file)) + { + return false; + } + + $data = @unserialize(file_get_contents($file)); + + if (!$data) + { + throw new RuntimeException('Cache file is invalid.'); + } + + if ($this->isDataExpired($data)) + { + $this->forget($key); + return false; + } + + return true; + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + $file = $this->path($key); + + if (file_exists($file)) + { + return unlink($file); + } + + return true; + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $path = $this->directory . ($group ? DIRECTORY_SEPARATOR . $group : ''); + $root = ($path == $this->directory); + + if (is_dir($path)) + { + foreach (new DirectoryIterator($path) as $file) + { + if (!$root || (!$file->isDot() && !in_array(strtolower($file->getFilename()), static::$skip))) + { + if ($file->isDir()) + { + //$this->clean(($group ? $group . DIRECTORY_SEPARATOR : '') . $file->getFilename()); + } + else + { + unlink($file->getPathname()); + } + } + } + + if (!$root) + { + @rmdir($path); + } + } + } + + /** + * Garbage collect expired cache data + * + * @param string $group + * @return void + */ + public function gc($group = null) + { + $result = true; + + $path = $this->directory . ($group ? DIRECTORY_SEPARATOR . $group : ''); + + if (is_dir($path)) + { + foreach (new DirectoryIterator($path) as $file) + { + if ($file->isDot() || in_array(strtolower($file->getFilename()), static::$skip)) + { + continue; + } + + if ($file->isDir()) + { + $result = $this->gc($file->getFilename()); + + if (!$result) + { + break; + } + + continue; + } + + if (!file_exists($file->getPathname())) + { + continue; + } + + $data = @unserialize(file_get_contents($file->getPathname())); + + if (!$data) + { + throw new RuntimeException(sprintf('Cache file "%s" is invalid.', $file->getPathname())); + } + + if ($this->isDataExpired($data)) + { + $result = unlink($file->getPathname()); + } + } + } + + return $result; + } + + /** + * Get all cached data + * + * @return array + */ + public function all() + { + $path = $this->directory; + + $data = array(); + + $dirIterator = new DirectoryIterator($path); + foreach ($dirIterator as $folder) + { + if ($folder->isDot() || !$folder->isDir()) + { + continue; + } + + $name = $folder->getFilename(); + + $item = new Auditor($name); + + $files = new DirectoryIterator($path . DIRECTORY_SEPARATOR . $name); + + foreach ($files as $file) + { + if ($file->isDot() || $file->isDir() || in_array(strtolower($file->getFilename()), static::$skip)) + { + continue; + } + + $item->tally(filesize($path . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR . $file->getFilename()) / 1024); + } + + $data[$name] = $item; + } + + return $data; + } + + /** + * Get the expiration time based on the given minutes. + * + * @param integer $minutes + * @return integer + */ + protected function expiration($minutes) + { + if ($minutes === 0) + { + return 9999999999; + } + + return time() + ($minutes * 60); + } + + /** + * Get the working directory of the cache. + * + * @return string + */ + public function getDirectory() + { + return $this->directory; + } + + /** + * Get the full path for the given cache key. + * + * @param string $key + * @return string + */ + protected function path($key) + { + $parts = explode('.', $key); + + $path = array_shift($parts); + $path = $this->directory . ($path ? DIRECTORY_SEPARATOR . $this->cleanPath($path) : ''); + + return $path . DIRECTORY_SEPARATOR . $this->id($key) . '.php'; + } + + /** + * Strip additional / or \ in a path name + * + * @param string $path The path to clean + * @param string $ds Directory separator (optional) + * @return string The cleaned path + */ + protected function cleanPath($path, $ds = DIRECTORY_SEPARATOR) + { + $path = trim($path); + + // Remove double slashes and backslahses and convert + // all slashes and backslashes to DIRECTORY_SEPARATOR + return preg_replace('#[/\\\\]+#', $ds, $path); + } + + /** + * Get the full path for the given cache key. + * + * @param string $key + * @return string + */ + protected function writeCacheFile($filename, $data) + { + $dir = pathinfo($filename, PATHINFO_DIRNAME); + + if (!file_exists($dir)) + { + $mod = $this->options['chmod'] ? $this->options['chmod'] : 0777; + mkdir($dir, $mod); + } + + $isNew = !file_exists($filename); + $result = file_put_contents($filename, serialize($data), LOCK_EX) !== false; + + if ($isNew && $result !== false && $this->options['chmod']) + { + chmod($filename, $this->options['chmod']); + } + + return $result; + } + + /** + * Check if the given data is expired + * + * @param array $data + * @return boolean + */ + protected function isDataExpired(array $data) + { + return $data['ttl'] !== 0 && time() > $data['ttl']; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/Memcache.php b/core/libraries/Hubzero/Cache/Storage/Memcache.php new file mode 100644 index 00000000000..5853d52bdd9 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/Memcache.php @@ -0,0 +1,277 @@ +options['engine'])) + { + $this->engine = $this->options['engine']; + } + + if (!$this->engine) + { + if (!isset($this->options['servers']) || empty($this->options['servers'])) + { + $conf = new \Hubzero\Config\Repository('site'); + + $this->options['compress'] = $config->get('memcache_compress', false) == false ? 0 : MEMCACHE_COMPRESSED; + + $this->options['servers'] = array( + array( + 'host' => $config->get('memcache_server_host', 'localhost'), + 'port' => $config->get('memcache_server_port', 11211), + 'persist' => $config->get('memcache_persist', true) + ) + ); + } + + $this->engine = $this->connect($this->options['servers']); + } + } + + /** + * Test to see if the cache storage is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + if ((extension_loaded('memcache') && class_exists('Memcache')) != true) + { + return false; + } + return true; + } + + /** + * Create a new Memcached connection. + * + * @param array $servers + * @return object \Memcached + * @throws \RuntimeException + */ + public function connect(array $servers) + { + $memcache = $this->getEngine(); + + // For each server in the array, we'll just extract the configuration and add + // the server to the Memcached connection. Once we have added all of these + // servers we'll verify the connection is successful and return it back. + foreach ($servers as $server) + { + $memcache->addServer( + $server['host'], $server['port'], $server['weight'] + ); + } + + if ($memcache->getVersion() === false) + { + throw new RuntimeException('Could not establish Memcached connection.'); + } + + // Memcahed has no list keys, we do our own accounting, initialise key index + if ($memcache->get($this->options['hash'] . '-index') === false) + { + $empty = array(); + self::$_db->set($this->oprions['hash'] . '-index', $empty, $this->options['compress'], 0); + } + + return $memcache; + } + + /** + * Get a new Memcached instance. + * + * @return object \Memcache + */ + public function getEngine() + { + return new \Memcache; + } + + /** + * Add an item to the cache only if it doesn't already exist + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function add($key, $value, $minutes) + { + return $this->engine->add($this->id($key), $value, $minutes * 60); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + return $this->engine->set($this->id($key), $value, $minutes * 60); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + $value = $this->engine->get($this->id($key)); + + if ($this->engine->getResultCode() == 0) + { + return $value; + } + + return null; + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + return $this->engine->get($this->id($key)) !== false || $this->engine->getResultCode() != \Memcached::RES_NOTFOUND; + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + return $this->engine->delete($this->id($key)); + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $hash = $this->options['hash']; + + $index = $this->engine->get($hash . '-index'); + if ($index === false) + { + $index = array(); + } + + foreach ($index as $key => $value) + { + if (strpos($value->name, $hash . '-cache-' . $group . '-') === 0) // xor $mode != 'group') + { + $this->engine->delete($value->name, 0); + unset($index[$key]); + } + } + + $this->engine->replace($hash . '-index', $index, 0); + } + + /** + * Get all cached data + * + * @return array + */ + public function all() + { + $hash = $this->options['hash']; + + $index = $this->engine->get($hash . '-index'); + if ($index === false) + { + $index = array(); + } + + foreach ($index as $key) + { + if (empty($key)) + { + continue; + } + + $namearr = explode('-', $key->name); + + if ($namearr !== false && $namearr[0] == $hash && $namearr[1] == 'cache') + { + $group = $namearr[2]; + + if (!isset($data[$group])) + { + $item = new Auditor($group); + } + else + { + $item = $data[$group]; + } + + $item->tally($key->size / 1024); + + $data[$group] = $item; + } + } + + return $data; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/Memcached.php b/core/libraries/Hubzero/Cache/Storage/Memcached.php new file mode 100644 index 00000000000..5ca1519198d --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/Memcached.php @@ -0,0 +1,267 @@ +options['engine'])) + { + $this->engine = $this->options['engine']; + } + + if (!$this->engine) + { + if (!isset($this->options['servers']) || empty($this->options['servers'])) + { + $conf = new \Hubzero\Config\Repository('site'); + + $this->options['servers'] = array( + array( + 'host' => $config->get('memcache_server_host', 'localhost'), + 'port' => $config->get('memcache_server_port', 11211), + 'weight' => 1 + ) + ); + } + + $this->engine = $this->connect($this->options['servers']); + } + } + + /** + * Test to see if the cache storage is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + if ((extension_loaded('memcached') && class_exists('Memcached')) != true) + { + return false; + } + return true; + } + + /** + * Create a new Memcached connection. + * + * @param array $servers + * @return object \Memcached + * @throws \RuntimeException + */ + public function connect(array $servers) + { + $memcached = $this->getEngine(); + + // For each server in the array, we'll just extract the configuration and add + // the server to the Memcached connection. Once we have added all of these + // servers we'll verify the connection is successful and return it back. + foreach ($servers as $server) + { + $memcached->addServer( + $server['host'], $server['port'], $server['weight'] + ); + } + + if ($memcached->getVersion() === false) + { + throw new RuntimeException('Could not establish Memcached connection.'); + } + + return $memcached; + } + + /** + * Get a new Memcached instance. + * + * @return object \Memcached + */ + public function getEngine() + { + return new \Memcached; + } + + /** + * Add an item to the cache only if it doesn't already exist + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function add($key, $value, $minutes) + { + return $this->engine->add($this->id($key), $value, $minutes * 60); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + return $this->engine->set($this->id($key), $value, $minutes * 60); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + $value = $this->engine->get($this->id($key)); + + if ($this->engine->getResultCode() == 0) + { + return $value; + } + + return null; + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + return $this->engine->get($this->id($key)) !== false || $this->engine->getResultCode() != \Memcached::RES_NOTFOUND; + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + return $this->engine->delete($this->id($key)); + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $hash = $this->options['hash']; + + $index = $this->engine->get($hash . '-index'); + if ($index === false) + { + $index = array(); + } + + foreach ($index as $key => $value) + { + if (strpos($value->name, $hash . '-cache-' . $group . '-') === 0) // xor $mode != 'group') + { + $this->engine->delete($value->name, 0); + unset($index[$key]); + } + } + $this->engine->replace($hash . '-index', $index, 0); + } + + /** + * Get all cached data + * + * @return array + */ + public function all() + { + $hash = $this->options['hash']; + + $index = $this->engine->get($hash . '-index'); + if ($index === false) + { + $index = array(); + } + + foreach ($index as $key) + { + if (empty($key)) + { + continue; + } + + $namearr = explode('-', $key->name); + + if ($namearr !== false && $namearr[0] == $hash && $namearr[1] == 'cache') + { + $group = $namearr[2]; + + if (!isset($data[$group])) + { + $item = new Auditor($group); + } + else + { + $item = $data[$group]; + } + + $item->tally($key->size / 1024); + + $data[$group] = $item; + } + } + + return $data; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/Memory.php b/core/libraries/Hubzero/Cache/Storage/Memory.php new file mode 100644 index 00000000000..a03ed6586e8 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/Memory.php @@ -0,0 +1,223 @@ +has($key)) + { + return false; + } + + return $this->put($key, $value, $minutes); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + $this->data[$this->id($key)] = array( + 'time' => time(), + 'value' => $value, + 'ttl' => $this->expiration($minutes) + ); + + return true; + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + if ($this->has($key)) + { + return $this->data[$this->id($key)]['value']; + } + + return null; + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + $key = $this->id($key); + + if (!isset($this->data[$key])) + { + return false; + } + + $value = $this->data[$key]; + + if ($this->isDataExpired($value)) + { + unset($this->data[$key]); + return false; + } + + return true; + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + if ($this->has($key)) + { + unset($this->data[$this->id($key)]); + } + + return true; + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $prefix = $this->options['hash'] . '-cache-' . $group . '-'; + + foreach ($this->data as $key => $value) + { + if (substr($key, 0, strlen($prefix)) == $prefix) + { + unset($this->data[$key]); + } + } + + return true; + } + + /** + * Garbage collect expired cache data + * + * @return boolean + */ + public function gc() + { + $prefix = $this->options['hash'] . '-cache-'; + + foreach ($this->data as $key => $value) + { + if (substr($key, 0, strlen($prefix)) == $prefix) + { + $value = $this->data[$key]; + + if ($this->isDataExpired($value)) + { + unset($this->data[$key]); + } + } + } + + return true; + } + + /** + * Get the expiration time based on the given minutes. + * + * @param integer $minutes + * @return integer + */ + protected function expiration($minutes) + { + if ($minutes === 0) + { + return 9999999999; + } + + return time() + ($minutes * 60); + } + + /** + * Check if the given data is expired + * + * @param array $data + * @return boolean + */ + protected function isDataExpired(array $data) + { + return $data['ttl'] !== 0 && time() > $data['ttl']; + //return $data['ttl'] !== 0 && time() - $data['time'] > $data['ttl']; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/None.php b/core/libraries/Hubzero/Cache/Storage/None.php new file mode 100644 index 00000000000..6a597a79348 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/None.php @@ -0,0 +1,175 @@ +options = array_merge($this->options, $options); + + if (!isset($this->options['client'])) + { + $this->options['client'] = ''; + } + if (!isset($this->options['language'])) + { + $this->options['language'] = 'en-GB'; + } + + if (!isset($this->options['hash'])) + { + $config = new \Hubzero\Config\Repository('site'); + $this->options['hash'] = md5($config->get('secret')); + } + } + + /** + * Test to see if the cache storage is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return true; + } + + /** + * Add an item to the cache only if it doesn't already exist + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function add($key, $value, $minutes) + { + return false; + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + return false; + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return false; + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + return null; + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + return false; + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + return true; + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + } + + /** + * Garbage collect expired cache data + * + * @return boolean + */ + public function gc() + { + return true; + } + + /** + * Get all cached data + * + * @return array + */ + public function all() + { + return array(); + } + + /** + * Get a cache_id string from an id/group pair + * + * @param string $id The cache data id + * @return string The cache id string + */ + protected function id($key) + { + $parts = explode('.', $key); + $group = array_shift($parts); + $name = implode('.', $parts); + + $id = md5($this->options['client'] . '-' . $name . '-' . $this->options['language']); + + return $this->options['hash'] . '-cache-' . $group . '-' . $id; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/StorageInterface.php b/core/libraries/Hubzero/Cache/Storage/StorageInterface.php new file mode 100644 index 00000000000..d3123320477 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/StorageInterface.php @@ -0,0 +1,58 @@ +has($key)) + { + return false; + } + + return $this->put($key, $value, $minutes); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + return wincache_ucache_set($this->id($key), $value, $minutes * 60); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + return wincache_ucache_get($this->id($key)); + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + return wincache_ucache_exists($this->id($key)); + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + return wincache_ucache_delete($this->id($key)); + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $hash = $this->options['hash']; + + $allinfo = wincache_ucache_info(); + + foreach ($allinfo['cache_entries'] as $key) + { + if (strpos($key['key_name'], $hash . '-cache-' . $group . '-') === 0) // xor $mode != 'group') + { + wincache_ucache_delete($key['key_name']); + } + } + + return true; + } + + /** + * Force garbage collect expired cache data as items are removed only on get/add/delete/info etc + * + * @return boolean True on success, false otherwise. + */ + public function gc() + { + $hash = $this->options['hash']; + + $allinfo = wincache_ucache_info(); + + foreach ($allinfo['cache_entries'] as $key) + { + if (strpos($key['key_name'], $hash . '-cache-')) + { + wincache_ucache_get($key['key_name']); + } + } + } + + /** + * Get all cached data + * + * @return array + */ + public function all() + { + $hash = $this->options['hash']; + + $allinfo = wincache_ucache_info(); + + $data = array(); + + foreach ($allinfo['cache_entries'] as $key) + { + $name = $key['key_name']; + $namearr = explode('-', $name); + + if ($namearr !== false && $namearr[0] == $hash && $namearr[1] == 'cache') + { + $group = $namearr[2]; + if (!isset($data[$group])) + { + $item = new Auditor($group); + } + else + { + $item = $data[$group]; + } + if (isset($key['value_size'])) + { + $item->tally($key['value_size'] / 1024); + } + else + { + // Dummy, WINCACHE version is too low. + $item->tally(1); + } + + $data[$group] = $item; + } + } + + return $data; + } +} diff --git a/core/libraries/Hubzero/Cache/Storage/XCache.php b/core/libraries/Hubzero/Cache/Storage/XCache.php new file mode 100644 index 00000000000..a94ba057067 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Storage/XCache.php @@ -0,0 +1,192 @@ +id($key); + + if (xcache_isset($key)) + { + return false; + } + + return $this->put($key, $value, $minutes * 60); + } + + /** + * Store an item in the cache for a given number of minutes. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + return xcache_set($this->id($key), $value, $minutes * 60); + } + + /** + * Store an item in the cache indefinitely. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function forever($key, $value) + { + return $this->put($key, $value, 0); + } + + /** + * Retrieve an item from the cache by key. + * + * @param string $key + * @return mixed + */ + public function get($key) + { + $value = xcache_get($this->id($key)); + + if (isset($value)) + { + return $value; + } + } + + /** + * Check if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function has($key) + { + return xcache_isset($this->id($key)); + } + + /** + * Remove an item from the cache. + * + * @param string $key + * @return bool + */ + public function forget($key) + { + return xcache_unset($this->id($key)); + } + + /** + * Remove all items from the cache. + * + * @param string $group + * @return void + */ + public function clean($group = null) + { + $hash = $this->options['hash']; + + $allinfo = xcache_list(XC_TYPE_VAR, 0); + + foreach ($allinfo['cache_list'] as $key) + { + if (strpos($key['name'], $hash . '-cache-' . $group . '-') === 0) // xor $mode != 'group') + { + xcache_unset($key['name']); + } + } + + return true; + } + + /** + * Get all cached data + * + * This requires the php.ini setting xcache.admin.enable_auth = Off. + * + * @return array + */ + public function all() + { + $hash = $this->options['hash']; + + $allinfo = xcache_list(XC_TYPE_VAR, 0); + + $data = array(); + + foreach ($allinfo['cache_list'] as $key) + { + $namearr = explode('-', $key['name']); + + if ($namearr !== false && $namearr[0] == $secret && $namearr[1] == 'cache') + { + $group = $namearr[2]; + + if (!isset($data[$group])) + { + $item = new Auditor($group); + } + else + { + $item = $data[$group]; + } + + $item->tally($key['size'] / 1024); + + $data[$group] = $item; + } + } + + return $data; + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Fixtures/config.json b/core/libraries/Hubzero/Cache/Tests/Fixtures/config.json new file mode 100755 index 00000000000..2e349a37855 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Fixtures/config.json @@ -0,0 +1,49 @@ +{ + "cache_handler": "none", + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971", + "apc": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971" + }, + "cachelite": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971" + }, + "eaccelerator": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971" + }, + "file": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971" + }, + "memcache": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971", + "host": "127.0.0.1", + "port": "11211" + }, + "memcached": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971", + "host": "127.0.0.1", + "port": "11211" + }, + "memory": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971" + }, + "none": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971" + }, + "wincache": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971" + }, + "xcache": { + "cachebase": "./cache", + "hash": "a667603a206b376ac2b26bf54b716971" + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/ManagerTest.php b/core/libraries/Hubzero/Cache/Tests/ManagerTest.php new file mode 100755 index 00000000000..9b8dffc0153 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/ManagerTest.php @@ -0,0 +1,96 @@ + $value) + { + $app['config']->set($key, $value); + } + $app['config']->set('foo', array( + 'hash' => '', + 'cachebase' => '' + )); + + $this->cache = new Manager($app); + } + + /** + * Test that an exception is thrown when selecting + * a nonexistent storage type. + * + * @expectedException \InvalidArgumentException + * + * @return void + */ + public function testStorageThrowsException() + { + $this->cache->storage('foo'); + } + + /** + * Test setting the default storage type + * + * @return void + */ + public function testSetDefaultDriver() + { + $this->cache->setDefaultDriver('memory'); + + $this->assertEquals('memory', $this->cache->getDefaultDriver()); + } + + /** + * Test adding custom storage type + * + * @return void + */ + public function testExtend() + { + $this->cache->extend('foo', function($config) + { + return new None; + }); + + $this->assertInstanceOf('Hubzero\Cache\Storage\None', $this->cache->storage('foo')); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/AbstractCacheTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/AbstractCacheTest.php new file mode 100755 index 00000000000..d6ed2c54338 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/AbstractCacheTest.php @@ -0,0 +1,147 @@ + $value) + { + $app['config']->set($key, $value); + } + + $this->cache = new Manager($app); + } + + /** + * @return array + */ + public function dataProvider() + { + return [ + ['key1', 'value1', 1], + ['key2', 'value2', 100], + ['key3', 'value3', null], + ['key4', true, null], + ['key5', false, null], + ['key6', array(), null], + ['key7', new \DateTime('now', new \DateTimeZone('UTC')), null], + ]; + } + + /** + * Test if an item exists int he cache + * + * @dataProvider dataProvider + * + * @param string $key + * @param mixed $value + * @param int|null $ttl + * @return void + */ + public function testHas($key, $value, $ttl) + { + $this->assertTrue($this->cache->forget($key)); + $this->assertFalse($this->cache->has($key)); + $this->assertTrue($this->cache->put($key, $value, $ttl)); + $this->assertTrue($this->cache->has($key)); + } + + /** + * Test adding item to cache, returning FALSE if it already exists + * + * @dataProvider dataProvider + * + * @param string $key + * @param mixed $value + * @param int|null $ttl + * @return void + */ + public function testAdd($key, $value, $ttl) + { + $this->cache->put($key, $value, $ttl); + $this->assertFalse($this->cache->add($key, $value, $ttl)); + } + + /** + * Test retrieving item from cache + * + * @dataProvider dataProvider + * + * @param string $key + * @param mixed $value + * @param int|null $ttl + * @return void + */ + public function testGet($key, $value, $ttl) + { + $this->cache->put($key, $value, $ttl); + $this->assertEquals($value, $this->cache->get($key)); + } + + /** + * Test removing item from cache + * + * @dataProvider dataProvider + * + * @param string $key + * @param mixed $value + * @param int|null $ttl + * @return void + */ + public function testForget($key, $value, $ttl) + { + $this->cache->put($key, $value, $ttl); + $this->assertTrue($this->cache->forget($key)); + $this->assertFalse($this->cache->has($key)); + } + + /** + * Test has() with expired data + * + * @return void + */ + public function testHasWithTtlExpired() + { + $this->cache->put('key1', 'value1', (1 / 60)); + sleep(2); + $this->assertFalse($this->cache->has('key1')); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/ApcTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/ApcTest.php new file mode 100755 index 00000000000..9a20bda2c39 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/ApcTest.php @@ -0,0 +1,39 @@ +markTestSkipped( + 'The APCu extension is not available.' + ); + } + if (!ini_get('apc.enable_cli')) + { + $this->markTestSkipped( + 'You need to enable apc.enable_cli' + ); + } + + parent::setup(); + + $this->cache->setDefaultDriver('apc'); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/CacheLiteTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/CacheLiteTest.php new file mode 100755 index 00000000000..4f8e69a1a7b --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/CacheLiteTest.php @@ -0,0 +1,35 @@ +markTestSkipped( + 'The CacheLite library is not available.' + ); + } + + parent::setup(); + + $this->cache->setDefaultDriver('cachelite'); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/EacceleratorTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/EacceleratorTest.php new file mode 100755 index 00000000000..21e993ecdf9 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/EacceleratorTest.php @@ -0,0 +1,35 @@ +markTestSkipped( + 'The eaccelerator extension is not available.' + ); + } + + parent::setup(); + + $this->cache->setDefaultDriver('eaccelerator'); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/FileTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/FileTest.php new file mode 100755 index 00000000000..ff6f03d79a0 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/FileTest.php @@ -0,0 +1,36 @@ +cache->setDefaultDriver('file'); + } + + /** + * Clear out any leftover test data + * + * @return void + */ + public function tearDown() + { + $this->cache->clean(); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/MemcacheTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/MemcacheTest.php new file mode 100755 index 00000000000..ddc1f88bc3c --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/MemcacheTest.php @@ -0,0 +1,33 @@ +markTestSkipped( + 'The Memcache extension is not available.' + ); + } + + parent::setup(); + + $this->cache->setDefaultDriver('memcache'); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/MemcachedTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/MemcachedTest.php new file mode 100755 index 00000000000..a88fb4af798 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/MemcachedTest.php @@ -0,0 +1,33 @@ +markTestSkipped( + 'The Memcached extension is not available.' + ); + } + + parent::setup(); + + $this->cache->setDefaultDriver('memcached'); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/MemoryTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/MemoryTest.php new file mode 100755 index 00000000000..246bc122889 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/MemoryTest.php @@ -0,0 +1,26 @@ +cache->setDefaultDriver('memory'); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/NoneTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/NoneTest.php new file mode 100755 index 00000000000..0d841b8594f --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/NoneTest.php @@ -0,0 +1,107 @@ + $value) + { + $app['config']->set($key, $value); + } + + $this->cache = new Manager($app); + + $this->cache->setDefaultDriver('none'); + } + + /** + * Test if an item exists in the cache + * + * @return void + */ + public function testHas() + { + $this->cache->put('key', 'value', 15); + $this->assertFalse($this->cache->has('key')); + } + + /** + * Test adding item to cache, returning FALSE if it already exists + * + * @return void + */ + public function testAdd() + { + $this->cache->put('key', 'value', 15); + $this->assertFalse($this->cache->add('key', 'value', 15)); + } + + /** + * Test retrieving item from cache + * + * @return void + */ + public function testGet() + { + $this->cache->put('key', 'value', 15); + $this->assertNull($this->cache->get('key')); + } + + /** + * Test puting something into the cache + * + * @return void + */ + public function testPut() + { + $this->assertFalse($this->cache->put('key', 'value', 15)); + } + + /** + * Test removing item from cache + * + * @return void + */ + public function testForget() + { + $this->assertTrue($this->cache->forget('key')); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/WinCacheTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/WinCacheTest.php new file mode 100755 index 00000000000..58278a8f5f5 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/WinCacheTest.php @@ -0,0 +1,39 @@ +markTestSkipped( + 'The wincache library is not available.' + ); + } + if (!ini_get('wincache.ucenabled')) + { + $this->markTestSkipped( + 'You need to enable wincache.ucenabled' + ); + } + + parent::setup(); + + $this->cache->setDefaultDriver('wincache'); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/Storage/XCacheTest.php b/core/libraries/Hubzero/Cache/Tests/Storage/XCacheTest.php new file mode 100755 index 00000000000..30448b3e743 --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/Storage/XCacheTest.php @@ -0,0 +1,33 @@ +markTestSkipped( + 'The xcache library is not available.' + ); + } + + parent::setup(); + + $this->cache->setDefaultDriver('xcache'); + } +} diff --git a/core/libraries/Hubzero/Cache/Tests/cache/index.html b/core/libraries/Hubzero/Cache/Tests/cache/index.html new file mode 100644 index 00000000000..399c491b2cc --- /dev/null +++ b/core/libraries/Hubzero/Cache/Tests/cache/index.html @@ -0,0 +1 @@ + diff --git a/core/libraries/Hubzero/Component/AdminController.php b/core/libraries/Hubzero/Component/AdminController.php new file mode 100644 index 00000000000..5b32290ebe6 --- /dev/null +++ b/core/libraries/Hubzero/Component/AdminController.php @@ -0,0 +1,27 @@ +_option . ($this->_controller ? '&controller=' . $this->_controller : ''), false) + ); + } +} diff --git a/core/libraries/Hubzero/Component/ApiController.php b/core/libraries/Hubzero/Component/ApiController.php new file mode 100644 index 00000000000..0f46d694776 --- /dev/null +++ b/core/libraries/Hubzero/Component/ApiController.php @@ -0,0 +1,1025 @@ + 'index' + ); + + /** + * The name of the task to be executed + * + * @var string + */ + protected $_doTask = null; + + /** + * The name of this controller + * + * @var string + */ + protected $_controller = null; + + /** + * The name of this component + * + * @var string + */ + protected $_option = null; + + /** + * The name of the model + * + * Default is a singular inflection of a plural controller name + * + * @var string + */ + protected $_model = null; + + /** + * Is this a dynamically generated endpoint? + * + * @var bool + */ + protected $isDynamic = false; + + /** + * Response object + * + * @var object + */ + public $response = null; + + /** + * Methods needing Auth + * + * @var array + */ + public $authenticated = array('all'); + + /** + * Methods skipping Auth + * + * @var array + */ + public $unauthenticated = array(); + + /** + * Methods needing rate limiting + * + * @var array + */ + public $rateLimited = array(); + + /** + * Methods skipping rate limiting + * + * @var array + */ + public $notRateLimited = array('all'); + + /** + * Constructor + * + * @param array $config Optional configurations to be used + * @return void + */ + public function __construct(Response $response, $config=array()) + { + $this->response = $response; + + $r = new ReflectionClass($this); + + if (!$r->inNamespace()) + { + throw new InvalidControllerException(Lang::txt('JLIB_APPLICATION_ERROR_INVALID_CONTROLLER_CLASS'), 500); + } + + // Set the name + if (!isset($config['name']) || !$config['name']) + { + // Components\Component\Api\Controllers\Controller + $segments = explode('\\', $r->getName()); + $config['name'] = strtolower($segments[1]); + } + $this->_name = $config['name']; + + // Set the controller name + if (!isset($config['controller']) || !$config['controller']) + { + // Components\Component\Api\Controllers\Controller + $config['controller'] = strtolower($r->getShortName()); + } + $this->_controller = $config['controller']; + + // Set the component name + $this->_option = 'com_' . $this->_name; + + // Is this a dynamically created endpoint? + // If so, we'll need to do some route parsing on the fly + if ($r->getName() == 'Hubzero\\Component\\ApiController') + { + $this->isDynamic = true; + + $request = App::get('request'); + $segment = $request->segment(3); + + if ($segment) + { + if ($segment == 'list') + { + $request->setVar('task', $request->getCmd('task', $segment)); + } + else + { + $request->setVar('task', $request->getCmd('task', 'read')); + $request->setVar($this->resolveModel()->getPrimaryKey(), $segment); + } + } + } + + // Get all the public methods of this class + foreach ($r->getMethods(ReflectionMethod::IS_PUBLIC) as $method) + { + $name = $method->getName(); + + // Ensure task isn't in the exclude list and ends in 'Task' + if (!in_array($name, array('registerTask', 'unregisterTask')) + && substr(strtolower($name), -4) == 'task') + { + // Remove the 'Task' suffix + $name = substr($name, 0, -4); + // Auto register the methods as tasks. + $this->_taskMap[strtolower($name)] = $name; + } + } + } + + /** + * Register (map) a task to a method in the class. + * + * @param string $task The task. + * @param string $method The name of the method in the derived class to perform for this task. + * @return void + */ + public function registerTask($task, $method) + { + if (in_array(strtolower($method), $this->_taskMap)) + { + $this->_taskMap[strtolower($task)] = $method; + } + + return $this; + } + + /** + * Unregister (unmap) a task in the class. + * + * @param string $task The task. + * @return object This object to support chaining. + */ + public function unregisterTask($task) + { + unset($this->_taskMap[strtolower($task)]); + + return $this; + } + + /** + * Determines task being called and attempts to execute it + * + * @return void + */ + public function execute() + { + // Incoming task + $this->_task = strtolower(Request::getCmd('task', '')); + + $doTask = null; + + // Check if the default task is set + if (!$this->_task) + { + if (isset($this->_taskMap['__default'])) + { + $doTask = $this->_taskMap['__default']; + } + } + // Check if the task is in the taskMap + else if (isset($this->_taskMap[$this->_task])) + { + $doTask = $this->_taskMap[$this->_task]; + } + + // Raise an error (hopefully, this shouldn't happen) + if (!$doTask) + { + throw new InvalidTaskException(Lang::txt('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $this->_task), 404); + } + + // Record the actual task being fired + $doTask .= 'Task'; + + // Call the task + $this->$doTask(); + } + + /** + * Check that the user is authenticated + * + * @return void + */ + protected function requiresAuthentication() + { + if (!App::get('authn')['user_id']) + { + App::abort(403, Lang::txt('JGLOBAL_AUTH_ACCESS_DENIED')); + } + } + + /** + * Set response content + * + * @param string $message + * @param integer $status + * @param string $reason + * @return void + */ + public function send($message = null, $status = null, $reason = null) + { + $this->response->setContent($message); + $this->response->setStatusCode($status ? $status : 200); + } + + /** + * Displays available options and parameters this component offers. + * + * @apiMethod GET + * @apiUri / + * @return void + */ + public function indexTask() + { + // var to hold output + $output = new stdClass(); + $output->component = substr($this->_option, 4); + $bits = explode('v', get_class($this)); + $output->version = str_replace('_', '.', end($bits)); + $output->tasks = array(); + $output->errors = array(); + + // create reflection class of file + $classReflector = new ReflectionClass($this); + + if ($this->isDynamic) + { + $model = $this->resolveModel(); + $properties = $model->getStructure()->getTableColumns($model->getTableName()); + $properties = $this->normalizeProperties($properties); + + $output->version = 1; + } + + // loop through each method and process doc + foreach ($classReflector->getMethods() as $method) + { + // create docblock object & make sure we have something + $phpdoc = new \phpDocumentor\Reflection\DocBlock($method); + + // skip constructor + if (substr($method->getName(), -4) != 'Task' || in_array($method->getName(), array('registerTask', 'unregisterTask'))) + { + continue; + } + + // skip method in the parent class (already processed), + /*if ($className != $method->getDeclaringClass()->getName()) + { + //continue; + }*/ + + // skip if we dont have a short desc + // but put in error + if (!$phpdoc->getShortDescription()) + { + $output->errors[] = Lang::txt( + 'JLIB_APPLICATION_ERROR_DOCBLOCK_MISSING', + $method->getName(), + str_replace(PATH_ROOT, '', $classReflector->getFileName()) + ); + continue; + } + + // create endpoint data array + $endpoint = array( + 'name' => substr($method->getName(), 0, -4), + 'description' => preg_replace('/\s+/', ' ', $phpdoc->getShortDescription()), + 'method' => '', + 'uri' => '', + 'parameters' => array() + ); + + // loop through each tag + foreach ($phpdoc->getTags() as $tag) + { + // get tag name and content + $name = strtolower(str_replace('api', '', $tag->getName())); + $content = $tag->getContent(); + + // handle parameters separately + // json decode param input + if ($name == 'parameter') + { + $parameter = json_decode($content); + + if (json_last_error() != JSON_ERROR_NONE) + { + $output->errors[] = Lang::txt( + 'JLIB_APPLICATION_ERROR_DOCLBOCK_PARSING', + $method->getName(), + str_replace(PATH_ROOT, '', $classReflector->getFileName()) + ); + continue; + } + + $endpoint['parameters'][] = (array) $parameter; + continue; + } + + if ($name == 'uri' && $method->getName() == 'indexTask') + { + $content .= $output->component; + + if ($this->isDynamic) + { + $content .= '/' . $this->_controller; + } + } + + // add data to endpoint data + $endpoint[$name] = $content; + } + + if ($this->isDynamic && $endpoint['name'] != 'index') + { + $endpoint['uri'] = str_replace('{component}', $output->component, $endpoint['uri']); + $endpoint['uri'] = str_replace('{controller}', $this->_controller, $endpoint['uri']); + $endpoint['uri'] = str_replace('{primary key}', '{' . $model->getPrimaryKey() . '}', $endpoint['uri']); + + foreach ($properties as $prop => $type) + { + $parameter = array( + 'name' => $prop, + 'description' => '', + 'type' => $type, + 'required' => false, + 'default' => null + ); + + if ($prop == $model->getPrimaryKey() && in_array($endpoint['name'], array('read', 'update', 'delete'))) + { + $parameter['required'] = true; + + if (in_array($endpoint['name'], array('read', 'delete'))) + { + $endpoint['parameters'] = array($parameter); + break; + } + } + + $endpoint['parameters'][] = $parameter; + } + } + + // add endpoint to output + $output->tasks[] = $endpoint; + } + + if (count($output->errors) <= 0) + { + unset($output->errors); + } + + $this->send($output); + } + + /** + * List entries + * + * @apiMethod GET + * @apiUri /{component}/{controller}/list + * @return void + */ + public function listTask() + { + $query = $this->resolveModel(); + + $properties = $query->getStructure()->getTableColumns($query->getTableName()); + $properties = $this->normalizeProperties($properties); + + $searches = array(); + + // Build a list of incoming filters from + // the propertes of the model + foreach ($properties as $property => $type) + { + if ($property == $query->getPrimaryKey()) + { + continue; + } + + if ($type == 'text') + { + $searches[] = $property; + continue; + } + + if ($type == 'integer') + { + $dflt = null; + + // 'state' is a very commonly used field and we want + // it to default to published entries + if ($property == 'state') + { + $dflt = $query::STATE_PUBLISHED; + if (User::authorise('core.manage', $this->_option)) + { + $dflt = null; + } + } + + $filters[$property] = Request::getInt($property, $dflt); + + // 'access' is another commonly used field where we + // want to control what values are actually allowed + if ($property == 'access') + { + if (is_null($filters[$property])) + { + $filters[$property] = User::getAuthorisedViewLevels(); + } + } + } + else + { + $filters[$property] = Request::getVar($property); + } + } + + $filters['sort'] = Request::getString('sort', $query->orderBy); + $filters['sort_Dir'] = Request::getWord('sort_Dir', $query->orderDir); + + if (!isset($properties[$filters['sort']])) + { + $filters['sort'] = $query->orderBy; + } + + if (!in_array($filters['sort_Dir'], array('asc', 'desc'))) + { + $filters['sort_Dir'] = $query->orderDir; + } + + foreach ($filters as $field => $value) + { + if ($field == 'sort' || $field == 'sort_Dir') + { + continue; + } + + if (is_null($value)) + { + continue; + } + + if (is_array($value)) + { + $query->whereIn($field, $value); + } + else + { + if ($properties[$field] == 'string' && !$value) + { + continue; + } + + if ($properties[$field] == 'date' && !$value) + { + continue; + } + + $query->whereEquals($field, $value); + } + } + + // Are searching for anything? + if ($search = (string)Request::getString('search')) + { + foreach ($searches as $i => $property) + { + if ($i == 0) + { + $query->whereLike($property, $search, 1); + } + else + { + $query->orWhereLike($property, $search, 1); + } + } + $query->resetDepth(); + } + + // Get a total record count + $total = with(clone $query)->total(); + + // Build the response + $response = new stdClass; + $response->records = array(); + $response->total = $total; + + if ($response->total) + { + $base = rtrim(Request::base(), '/'); + + $rows = $query + ->order($filters['sort'], $filters['sort_Dir']) + ->paginated('limitstart', 'limit') + ->rows(); + + foreach ($rows as $row) + { + $obj = $row->toObject(); + foreach ($properties as $property => $type) + { + if ($type == 'date') + { + if ($obj->$property && $obj->$property != '0000-00-00 00:00:00') + { + $obj->$property = with(new Date($obj->$property))->format('Y-m-d\TH:i:s\Z'); + } + } + if ($type == 'text') + { + unset($obj->$property); + } + } + if (method_exists($row, 'link')) + { + $obj->url = str_replace('/api', '', $base . '/' . ltrim(Route::url($row->link()), '/')); + } + + $response->records[] = $obj; + } + } + + $this->send($response); + } + + /** + * Create an entry + * + * @apiMethod POST + * @apiUri /{component}/{controller} + * @return void + */ + public function createTask() + { + $this->requiresAuthentication(); + + $name = $this->resolveModel(); + + $properties = $model->getStructure()->getTableColumns($model->getTableName()); + $properties = $this->normalizeProperties($properties); + + foreach ($properties as $property => $type) + { + if ($property == $model->getPrimaryKey()) + { + continue; + } + + if ($type == 'integer') + { + $fields[$property] = Request::getInt($property, 0, 'post'); + } + /*else if ($type == 'date') + { + $fields[$property] = Request::getString($property, with(new Date('now'))->toSql(), 'post'); + }*/ + else + { + $fields[$property] = Request::getVar($property, null, 'post'); + } + } + + if (!$model->set($fields)) + { + App::abort(500, Lang::txt('JLIB_APPLICATION_ERROR_BIND_FAILED', $model->getError())); + } + + // Trigger before save event + $result = Event::trigger('on' . $model->getModelName() . 'BeforeSave', array(&$model, true)); + + // Save the data + if (!$model->save()) + { + App::abort(500, Lang::txt('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError())); + } + + // Trigger after save event + Event::trigger('on' . $model->getModelName() . 'AfterSave', array(&$model, true)); + + // Log activity + if (method_exists($model, 'link')) + { + $base = rtrim(Request::base(), '/'); + $url = str_replace('/api', '', $base . '/' . ltrim(Route::url($model->link()), '/')); + } + + $recipients = array(); + + if ($model->hasAttribute('created_by')) + { + $recipients[] = $model->get('created_by'); + } + + Event::trigger('system.logActivity', [ + 'activity' => [ + 'action' => 'created', + 'scope' => $this->_name . '.' . $model->getModelName(), + 'scope_id' => $model->get($model->getPrimaryKey()), + 'description' => Lang::txt( + 'JLIB_ACTIVITY_ITEM_CREATED', + $model->getModelName() . ' #' . $model->get($model->getPrimaryKey()) + ), + 'details' => $model->toArray() + ], + 'recipients' => $recipients + ]); + + /*if ($model->hasAttribute('created_by') + && $model->hasAttribute('anonymous')) + { + $model->set('created_by', 0); + }*/ + + // Format timestamps before sending output + foreach ($properties as $property => $type) + { + if ($type == 'date' && $model->get($property) && $model->get($property) != '0000-00-00 00:00:00') + { + $model->set($property, with(new Date($model->get($property)))->format('Y-m-d\TH:i:s\Z')); + } + } + + // Send results + $this->send($model->toObject()); + } + + /** + * Read an entry + * + * @apiMethod GET + * @apiUri /{component}/{controller}/{primary key} + * @return void + */ + public function readTask() + { + $model = $this->resolveModel(); + + // Load record + $model = $model + ->whereEquals($model->getPrimaryKey(), Request::getVar($model->getPrimaryKey())) + ->row(); + + if ($model->isNew()) + { + App::abort(404, Lang::txt('JLIB_APPLICATION_ERROR_ITEM_NOT_FOUND')); + } + + // Format timestamps before sending output + $properties = $model->getStructure()->getTableColumns($model->getTableName()); + $properties = $this->normalizeProperties($properties); + + foreach ($properties as $property => $type) + { + if ($type == 'date' && $model->get($property) && $model->get($property) != '0000-00-00 00:00:00') + { + $model->set($property, with(new Date($model->get($property)))->format('Y-m-d\TH:i:s\Z')); + } + } + + // Send results + $this->send($model->toObject()); + } + + /** + * Update an entry + * + * @apiMethod PUT + * @apiUri /{component}/{controller}/{primary key} + * @return void + */ + public function updateTask() + { + // Require authenitcation + $this->requiresAuthentication(); + + $model = $this->resolveModel(); + + // Load record + $model = $model + ->whereEquals($model->getPrimaryKey(), Request::getVar($model->getPrimaryKey())) + ->row(); + + if ($model->isNew()) + { + App::abort(404, Lang::txt('JLIB_APPLICATION_ERROR_ITEM_NOT_FOUND')); + } + + // Collect data + $properties = $model->getStructure()->getTableColumns($model->getTableName()); + $properties = $this->normalizeProperties($properties); + + foreach ($properties as $property => $type) + { + if ($property == $model->getPrimaryKey()) + { + continue; + } + + if ($type == 'integer') + { + $fields[$property] = Request::getInt($property, $model->get($property, 0)); + } + else + { + $fields[$property] = Request::getVar($property, $model->get($property)); + } + } + + if (!$model->set($fields)) + { + App::abort(500, Lang::txt('JLIB_APPLICATION_ERROR_BIND_FAILED', $model->getError())); + } + + // Trigger before save event + $result = Event::trigger('on' . $model->getModelName() . 'BeforeSave', array(&$model, true)); + + // Save the data + if (!$model->save()) + { + App::abort(500, Lang::txt('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError())); + } + + // Trigger after save event + Event::trigger('on' . $model->getModelName() . 'AfterSave', array(&$model, true)); + + // Log activity + if (method_exists($model, 'link')) + { + $base = rtrim(Request::base(), '/'); + $url = str_replace('/api', '', $base . '/' . ltrim(Route::url($model->link()), '/')); + } + + $recipients = array(); + + if ($model->hasAttribute('created_by')) + { + $recipients[] = $model->get('created_by'); + } + if ($model->hasAttribute('modified_by')) + { + $recipients[] = $model->get('modified_by'); + } + + Event::trigger('system.logActivity', [ + 'activity' => [ + 'action' => 'updated', + 'scope' => $this->_name . '.' . $model->getModelName(), + 'scope_id' => $model->get($model->getPrimaryKey()), + 'description' => Lang::txt( + 'JLIB_ACTIVITY_ITEM_UPDATED', + $model->getModelName() . ' #' . $model->get($model->getPrimaryKey()) + ), + 'details' => $model->toArray() + ], + 'recipients' => $recipients + ]); + + // Format timestamps before sending output + foreach ($properties as $property => $type) + { + if ($type == 'date' && $model->get($property) && $model->get($property) != '0000-00-00 00:00:00') + { + $model->set($property, with(new Date($model->get($property)))->format('Y-m-d\TH:i:s\Z')); + } + } + + // Send results + $this->send($model->toObject()); + } + + /** + * Delete an entry + * + * @apiMethod DELETE + * @apiUri /{component}/{controller}/{primary key} + * @return void + */ + public function deleteTask() + { + // Require authenitcation + $this->requiresAuthentication(); + + $model = $this->resolveModel(); + + // Load record + $model = $model + ->whereEquals($model->getPrimaryKey(), Request::getVar($model->getPrimaryKey())) + ->row(); + + if ($model->isNew()) + { + App::abort(404, Lang::txt('JLIB_APPLICATION_ERROR_ITEM_NOT_FOUND')); + } + + if (!$model->destroy()) + { + App::abort(500, Lang::txt('JLIB_APPLICATION_ERROR_DELETE_FAILED', $model->getError())); + } + + // Log activity + $recipients = array(); + + if ($model->hasAttribute('created_by')) + { + $recipients[] = $model->get('created_by'); + } + if ($model->hasAttribute('modified_by')) + { + $recipients[] = $model->get('modified_by'); + } + + Event::trigger('system.logActivity', [ + 'activity' => [ + 'action' => 'deleted', + 'scope' => $this->_name . '.' . $model->getModelName(), + 'scope_id' => $model->get($model->getPrimaryKey()), + 'description' => Lang::txt( + 'JLIB_ACTIVITY_ITEM_DELETED', + $model->getModelName() . ' #' . $model->get($model->getPrimaryKey()) + ), + 'details' => $model->toArray() + ], + 'recipients' => $recipients + ]); + + // Send status + $this->send(null, 204); + } + + /** + * Reduce field types to a couple generic types + * + * @param array $properties + * @return array + */ + protected function normalizeProperties($properties) + { + foreach ($properties as $property => $type) + { + if (preg_match('/(.*?)\(\d+\).*/i', $type, $matches)) + { + $type = $matches[1]; + + $properties[$property] = $type; + } + + switch ($type) + { + case 'text': + case 'tinytext': + case 'mediumtext': + case 'longtext': + case 'blob': + $properties[$property] = 'text'; + break; + + case 'int': + case 'integer': + case 'smallint': + case 'tinyint': + case 'bigint': + case 'mediumint': + case 'year': + $properties[$property] = 'integer'; + break; + + case 'datetime': + case 'date': + case 'timestamp': + case 'time': + $properties[$property] = 'date'; + break; + + case 'varchar': + case 'char': + default: + $properties[$property] = 'string'; + break; + } + } + + return $properties; + } + + /** + * Get the model + * + * @return object + * @throws Exception + */ + protected function resolveModel() + { + if (!$this->_model) + { + // If no model name is explicitely set then derive the + // name (singular) from the controller name (plural) + if (!$this->_model) + { + $this->_model = \Hubzero\Utility\Inflector::singularize($this->_controller); + } + + // Does the model name include the namespace? + // We need a fully qualified name + if ($this->_model && !strstr($this->_model, '\\')) + { + $this->_model = 'Components\\' . ucfirst($this->_name) . '\\Models\\' . ucfirst(strtolower($this->_model)); + } + } + + $model = $this->_model; + + // Make sure the class exists + if (!class_exists($model)) + { + $file = explode('\\', $model); + $file = strtolower(end($file)); + + $path = \Component::path($this->_option) . '/models/' . $file . '.php'; + + require_once $path; + + if (!class_exists($model)) + { + App::abort(500, Lang::txt('JLIB_APPLICATION_ERROR_MODEL_GET_NAME', $model)); + } + } + + return new $model; + } +} diff --git a/core/libraries/Hubzero/Component/ControllerInterface.php b/core/libraries/Hubzero/Component/ControllerInterface.php new file mode 100644 index 00000000000..fb4e463721d --- /dev/null +++ b/core/libraries/Hubzero/Component/ControllerInterface.php @@ -0,0 +1,21 @@ +app = $app; + } + + /** + * Checks if the component is enabled + * + * @param string $option The component option. + * @param boolean $strict If set and the component does not exist, false will be returned. + * @return boolean + */ + public function isEnabled($option, $strict = false) + { + $result = $this->load($option, $strict); + + return ($result->enabled == 1);// | $this->app->isAdmin()); + } + + /** + * Gets the parameter object for the component + * + * @param string $option The option for the component. + * @param boolean $strict If set and the component does not exist, false will be returned + * @return object A Registry object. + */ + public function params($option, $strict = false) + { + return $this->load($option, $strict)->params; + } + + /** + * Get the base path to a component + * + * @param string $option The name of the component. + * @return string + */ + public function path($option) + { + $result = $this->load($option); + + if (!isset($result->path)) + { + $result->path = ''; + + $paths = array( + PATH_APP . DIRECTORY_SEPARATOR . 'components' . DIRECTORY_SEPARATOR . substr($result->option, 4), + PATH_APP . DIRECTORY_SEPARATOR . 'components' . DIRECTORY_SEPARATOR . $result->option, + PATH_CORE . DIRECTORY_SEPARATOR . 'components' . DIRECTORY_SEPARATOR . substr($result->option, 4), + PATH_CORE . DIRECTORY_SEPARATOR . 'components' . DIRECTORY_SEPARATOR . $result->option + ); + + foreach ($paths as $path) + { + if (is_dir($path)) + { + $result->path = $path; + break; + } + } + } + + return $result->path; + } + + /** + * Make sure component name follows naming conventions + * + * @param string $option The element value for the extension + * @return string + */ + public function canonical($option) + { + if (is_array($option)) + { + $option = implode('', $option); + } + $option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option); + if (substr($option, 0, strlen('com_')) != 'com_') + { + $option = 'com_' . $option; + } + return $option; + } + + /** + * Render the component. + * + * @param string $option The component option. + * @param array $params The component parameters + * @return object + */ + public function render($option, $params = array()) + { + $client = (isset($this->app['client']->alias) ? $this->app['client']->alias : $this->app['client']->name); + + // Load template language files. + $lang = $this->app['language']; + if ($this->app->has('template')) + { + $template = $this->app['template']->template; + + $lang->load('tpl_' . $template, PATH_APP . DIRECTORY_SEPARATOR . 'bootstrap' . DIRECTORY_SEPARATOR . $client . DIRECTORY_SEPARATOR . 'language', null, false, true); + $lang->load('tpl_' . $template, $this->app['template']->path, null, false, true); + } + + if (empty($option)) + { + // Throw 404 if no component + $this->app->abort(404, $lang->translate('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND')); + } + + $option = $this->canonical($option); + + // Record the scope + $scope = $this->app->has('scope') ? $this->app->get('scope') : null; + + // Set scope to component name + $this->app->set('scope', $option); + + // Build the component path. + $file = substr($option, 4); + + // Get component path + define('PATH_COMPONENT', $this->path($option) . DIRECTORY_SEPARATOR . $client); + define('PATH_COMPONENT_SITE', $this->path($option) . DIRECTORY_SEPARATOR . 'site'); + define('PATH_COMPONENT_ADMINISTRATOR', $this->path($option) . DIRECTORY_SEPARATOR . 'admin'); + + // Legacy compatibility + // @TODO: Deprecate this! + define('JPATH_COMPONENT', PATH_COMPONENT); + define('JPATH_COMPONENT_SITE', PATH_COMPONENT_SITE); + define('JPATH_COMPONENT_ADMINISTRATOR', PATH_COMPONENT_ADMINISTRATOR); + + $path = PATH_COMPONENT . DIRECTORY_SEPARATOR . $file . '.php'; + $namespace = '\\Components\\' . ucfirst(substr($option, 4)) . '\\' . ucfirst($client) . '\\Bootstrap'; + $found = false; + + // Make sure the component is enabled + if ($this->isEnabled($option)) + { + // Check to see if the class is autoload-able + if (class_exists($namespace)) + { + $found = true; + $path = $namespace; + + // Infer the appropriate language path and load from there + $file = with(new \ReflectionClass($namespace))->getFileName(); + $bits = explode(DIRECTORY_SEPARATOR, $file); + $local = implode(DIRECTORY_SEPARATOR, array_slice($bits, 0, -1)); + + // Load local language files + $lang->load($option, $local, null, false, true); + } + else if (file_exists($path)) + { + $found = true; + + // Load local language files + $lang->load($option, PATH_COMPONENT, null, false, true); + } + } + + // Make sure we found something + if (!$found) + { + $this->app->abort(404, $lang->translate('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND_OR_ENABLED')); + } + + // Load base language file + $lang->load($option, PATH_APP, null, false, true); + + // Handle template preview outlining. + $contents = null; + + // Execute the component. + $contents = $this->execute($path); + + // Revert the scope + $this->app->forget('scope'); + $this->app->set('scope', $scope); + + return $contents; + } + + /** + * Execute the component. + * + * @param string $path The component path. + * @return string The component output + */ + protected function execute($path) + { + ob_start(); + + if (file_exists($path)) + { + $this->executePath($path); + } + else + { + $this->executeBootstrap($path); + } + + $contents = ob_get_contents(); + ob_end_clean(); + + return $contents; + } + + /** + * Execute the component from an old path based component + * + * @param string $path The component path + * @return void + */ + protected function executePath($path) + { + require_once $path; + } + + /** + * Execute the component from a new bootstrapped component + * + * @param string $namespace The namespace of the component to start + * @return void + */ + protected function executeBootstrap($namespace) + { + with(new $namespace)->start(); + } + + /** + * Get component router + * + * @param string $option Name of the component + * @param string $client Client to load the router for + * @return object Component router + */ + public function router($option, $client = null, $version = null) + { + $option = $this->canonical($option); + $client = ($client ? $client : $this->app['client']->alias); + $key = $option . $client; + + if (!isset(self::$routers[$key])) + { + $compname = ucfirst(substr($option, 4)); + + $client = ucfirst($client); + + $legacy = $compname . 'Router'; + $name = '\\Components\\' . $compname . '\\' . $client . '\\Router'; + + if (!class_exists($name) && !class_exists($legacy)) + { + // Use the component routing handler if it exists + $paths = array(); + if (!is_null($version)) + { + $paths[] = $this->path($option) . DIRECTORY_SEPARATOR . strtolower($client) . DIRECTORY_SEPARATOR . 'routerv' . $version . '.php'; + } + $paths[] = $this->path($option) . DIRECTORY_SEPARATOR . strtolower($client) . DIRECTORY_SEPARATOR . 'router.php'; + $paths[] = $this->path($option) . DIRECTORY_SEPARATOR . 'router.php'; + + // Use the custom routing handler if it exists + foreach ($paths as $path) + { + if (file_exists($path)) + { + require_once $path; + break; + } + } + } + + if (class_exists($name)) + { + $reflection = new ReflectionClass($name); + + if (in_array('Hubzero\\Component\\Router\\RouterInterface', $reflection->getInterfaceNames())) + { + self::$routers[$key] = new $name; + } + } + else if (class_exists($legacy)) + { + $reflection = new ReflectionClass($legacy); + + if (in_array('Hubzero\\Component\\Router\\RouterInterface', $reflection->getInterfaceNames())) + { + self::$routers[$key] = new $legacy; + } + } + + if (!isset(self::$routers[$key])) + { + self::$routers[$key] = new Legacy($compname); + } + } + + return self::$routers[$key]; + } + + /** + * Load the installed components into the components property. + * + * @param string $option The element value for the extension + * @param boolean $strict If set and the component does not exist, the enabled attribute will be set to false. + * @return object + */ + public function load($option, $strict = false) + { + $option = $this->canonical($option); + + if (isset(self::$components[$option])) + { + return self::$components[$option]; + } + + // Do we have a database connection? + if ($this->app->has('db')) + { + $db = $this->app->get('db'); + + $query = $db->getQuery() + ->select('extension_id', 'id') + ->select('element', '"option"') + ->select('params') + ->select('enabled') + ->from('#__extensions') + ->whereEquals('type', 'component') + ->whereEquals('element', $option); + + $db->setQuery($query->toString()); + + if (!$this->app->has('cache.store') || !($cache = $this->app['cache.store'])) + { + $cache = new \Hubzero\Cache\Storage\None(); + } + + if (!($data = $cache->get('_system.' . $option))) + { + $data = $db->loadObject(); + $cache->put('_system.' . $option, $data, $this->app['config']->get('cachetime', 15)); + } + + self::$components[$option] = $data; + + if ($error = $db->getErrorMsg()) + { + throw new Exception($this->app['language']->translate('JLIB_APPLICATION_ERROR_COMPONENT_NOT_LOADING', $option, $error), 500); + } + } + + // Create a default object + if (empty(self::$components[$option])) + { + self::$components[$option] = new stdClass; + self::$components[$option]->option = $option; + self::$components[$option]->enabled = $strict ? 0 : 1; + self::$components[$option]->params = ''; + self::$components[$option]->id = 0; + } + + // Convert the params to an object. + if (is_string(self::$components[$option]->params)) + { + self::$components[$option]->params = new Registry(self::$components[$option]->params); + } + + return self::$components[$option]; + } +} diff --git a/core/libraries/Hubzero/Component/Router/Base.php b/core/libraries/Hubzero/Component/Router/Base.php new file mode 100644 index 00000000000..6cb548e2938 --- /dev/null +++ b/core/libraries/Hubzero/Component/Router/Base.php @@ -0,0 +1,25 @@ +component = $component; + } + + /** + * Generic preprocess function for missing or legacy component router + * + * @param array $query An associative array of URL arguments + * @return array The URL arguments to use to assemble the subsequent URL. + */ + public function preprocess($query) + { + return $query; + } + + /** + * Generic build function for missing or legacy component router + * + * @param array &$query An array of URL arguments + * @return array The URL arguments to use to assemble the subsequent URL. + */ + public function build(&$query) + { + $function = $this->component . 'BuildRoute'; + + if (function_exists($function)) + { + return $function($query); + } + + return array(); + } + + /** + * Generic parse function for missing or legacy component router + * + * @param array &$segments The segments of the URL to parse. + * @return array The URL attributes to be used by the application. + */ + public function parse(&$segments) + { + $function = $this->component . 'ParseRoute'; + + if (function_exists($function)) + { + return $function($segments); + } + + return array(); + } +} diff --git a/core/libraries/Hubzero/Component/Router/RouterInterface.php b/core/libraries/Hubzero/Component/Router/RouterInterface.php new file mode 100644 index 00000000000..d55ce7858b9 --- /dev/null +++ b/core/libraries/Hubzero/Component/Router/RouterInterface.php @@ -0,0 +1,46 @@ + 'display' + ); + + /** + * The name of the task to be executed + * + * @var string + */ + protected $_doTask = null; + + /** + * The name of this controller + * + * @var string + */ + protected $_controller = null; + + /** + * The name of this component + * + * @var string + */ + protected $_option = null; + + /** + * The base path to this component + * + * @var string + */ + protected $_basePath = null; + + /** + * Redirection URL + * + * @var string + * @deprecated + */ + protected $_redirect = null; + + /** + * The message to display + * + * @var string + * @deprecated + */ + protected $_message = null; + + /** + * Message type + * + * @var string + * @deprecated + */ + protected $_messageType = 'message'; + + /** + * Constructor + * + * @param array $config Optional configurations to be used + * @return void + */ + public function __construct($config=array()) + { + $this->_redirect = null; + $this->_message = null; + $this->_messageType = 'message'; + + // Get the reflection info + $r = new ReflectionClass($this); + + // Is it namespaced? + if ($r->inNamespace()) + { + // It is! This makes things easy. + $this->_controller = strtolower($r->getShortName()); + } + + // Set the name + if (empty($this->_name)) + { + if (isset($config['name'])) + { + $this->_name = $config['name']; + } + else + { + $segments = null; + $cls = $r->getName(); + + // If namespaced... + if (strstr($cls, '\\')) + { + $segments = explode('\\', $cls); + } + // If matching the pattern of ComponentControllerName + else if (preg_match('/(.*)Controller(.*)/i', $cls, $segments)) + { + $this->_controller = isset($segments[2]) ? strtolower($segments[2]) : null; + } + // Uh-oh! + else + { + throw new InvalidControllerException(Lang::txt('Controller::__construct() : Can\'t get or parse class name.'), 500); + } + + $this->_name = strtolower($segments[1]); + } + } + + // Set the base path + if (array_key_exists('base_path', $config)) + { + $this->_basePath = $config['base_path']; + } + else + { + // Set base path relative to the controller file rather than + // an absolute path. This gives us a little more flexibility. + $this->_basePath = dirname(dirname($r->getFileName())); + } + + // Set the component name + $this->_option = 'com_' . $this->_name; + + // Determine the methods to exclude from the base class. + $xMethods = get_class_methods('\\Hubzero\\Component\\SiteController'); + + // Get all the public methods of this class + foreach ($r->getMethods(ReflectionMethod::IS_PUBLIC) as $method) + { + $name = $method->getName(); + + // Ensure task isn't in the exclude list and ends in 'Task' + if ((!in_array($name, $xMethods) || $name == 'displayTask') + && substr(strtolower($name), -4) == 'task') + { + // Remove the 'Task' suffix + $name = substr($name, 0, -4); + // Auto register the methods as tasks. + $this->_taskMap[strtolower($name)] = $name; + } + } + + // get language object & get any loaded lang for option + $lang = \Lang::getRoot(); + $loaded = $lang->getPaths($this->_option); + + // Load language file if we dont have one yet + if (!isset($loaded) || empty($loaded)) + { + $lang->load($this->_option, $this->_basePath . '/../..'); + } + + // Set some commonly used vars + // + // [!] Deprecated + // These will be going away in a future version. Do not use. + $this->juser = \User::getInstance(); + $this->database = \App::get('db'); + $this->config = \Component::params($this->_option); + } + + /** + * Method to set an overloaded variable to the component + * + * @param string $property Name of overloaded variable to add + * @param mixed $value Value of the overloaded variable + * @return void + */ + public function __set($property, $value) + { + $this->_data[$property] = $value; + } + + /** + * Method to get an overloaded variable of the component + * + * @param string $property Name of overloaded variable to retrieve + * @return mixed Value of the overloaded variable + */ + public function __get($property) + { + if (isset($this->_data[$property])) + { + return $this->_data[$property]; + } + } + + /** + * Method to check if a poperty is set + * + * @param string $property Name of overloaded variable to add + * @return boolean + */ + public function __isset($property) + { + return isset($this->_data[$property]); + } + + /** + * Determines task being called and attempts to execute it + * + * @return void + */ + public function execute() + { + // Incoming task + $this->_task = strtolower(\Request::getCmd('task', \Request::getWord('layout', ''))); + + // Check if the task is in the taskMap + if (isset($this->_taskMap[$this->_task])) + { + $doTask = $this->_taskMap[$this->_task]; + } + // Check if the default task is set + elseif (isset($this->_taskMap['__default'])) + { + $doTask = $this->_taskMap['__default']; + } + // Raise an error (hopefully, this shouldn't happen) + else + { + throw new InvalidTaskException(Lang::txt('The requested task "%s" was not found.', $this->_task), 404); + } + + $name = $this->_controller; + $layout = preg_replace('/[^A-Z0-9_]/i', '', $doTask); + if (!$this->_controller) + { + $cls = get_class($this); + // Attempt to parse the controller name from the class name + if ((ucfirst($this->_name) . 'Controller') != $cls + && preg_match('/(\w)Controller(.*)/i', $cls, $r)) + { + $this->_controller = strtolower($r[2]); + $name = $this->_controller; + $layout = preg_replace('/[^A-Z0-9_]/i', '', $doTask); + } + // Namepsaced component + else if (preg_match('/(.?)Controllers\\\(.*)/i', $cls, $r)) + { + $this->_controller = strtolower($r[2]); + $name = $this->_controller; + $layout = preg_replace('/[^A-Z0-9_]/i', '', $doTask); + } + // No controller name found - single controller component + else + { + $name = $doTask; + } + } + + // Instantiate a view with layout the same name as the task + $this->view = new View(array( + 'base_path' => $this->_basePath, + 'name' => $name, + 'layout' => $layout + )); + + // Set some commonly used vars + $this->view->set('option', $this->_option) + ->set('task', $doTask) + ->set('controller', $this->_controller); + + // Record the actual task being fired + $doTask .= 'Task'; + + // On before do task hook + $this->_onBeforeDoTask(); + + // Call the task + $this->$doTask(); + } + + /** + * Reset the view object + * + * @param string $name The name of the view + * @param string $layout The name of the layout (optional) + * @return void + */ + public function setView($name, $layout=null) + { + $config = array( + 'name' => $name + ); + if ($layout) + { + $config['layout'] = $layout; + } + $this->view = new View($config); + + // Set some commonly used vars + $this->view->option = $this->_option; + $this->view->task = $name; + $this->view->controller = $this->_controller; + } + + /** + * Get the last task that is being performed or was most recently performed. + * + * @return string The task that is being performed or was most recently performed. + */ + public function getTask() + { + return $this->_task; + } + + /** + * Register (map) a task to a method in the class. + * + * @param string $task The task. + * @param string $method The name of the method in the derived class to perform for this task. + * @return object Supports chaining. + */ + public function registerTask($task, $method) + { + if (in_array(strtolower($method), $this->_taskMap)) + { + $this->_taskMap[strtolower($task)] = $method; + } + + return $this; + } + + /** + * Unregister (unmap) a task in the class. + * + * @param string $task The task. + * @return object Supports chaining. + */ + public function unregisterTask($task) + { + unset($this->_taskMap[strtolower($task)]); + + return $this; + } + + /** + * Register the default task to perform if a mapping is not found. + * + * @param string $method The name of the method in the derived class to perform if a named task is not found. + * @return object Supports chaining. + */ + public function registerDefaultTask($method) + { + return $this->registerTask('__default', $method); + } + + /** + * Disable default task, remove __default from the taskmap + * + * When default task disabled the controller will give a 404 error if the method called doesn't exist + * + * @return void + */ + public function disableDefaultTask() + { + return $this->unregisterTask('__default'); + } + + /** + * Method to redirect the application to a new URL and optionally include a message + * + * @param string $url URL to redirect to. Optional. + * @param string $msg Message to display on redirect. Optional. + * @param string $type Message type. Optional, defaults to 'message'. + * @return void + * @deprecated + */ + public function redirect($url=null, $msg=null, $type=null) + { + if ($url) + { + $this->setRedirect($url, $msg, $type); + } + + if ($this->_redirect != null) + { + \App::redirect($this->_redirect, $this->_message, $this->_messageType); + } + } + + /** + * Set a URL for browser redirection. + * + * @param string $url URL to redirect to. + * @param string $msg Message to display on redirect. Optional, defaults to + * value set internally by controller, if any. + * @param string $type Message type. Optional, defaults to 'message'. + * @return object + * @deprecated + */ + public function setRedirect($url, $msg=null, $type=null) + { + $this->_redirect = $url; + if ($msg !== null) + { + // controller may have set this directly + $this->_message = $msg; + } + + // Ensure the type is not overwritten by a previous call to setMessage. + if (empty($type)) + { + if (empty($this->_messageType)) + { + $this->_messageType = 'message'; + } + } + // If the type is explicitly set, set it. + else + { + $this->_messageType = $type; + } + + return $this; + } + + /** + * Set a URL for browser redirection. + * + * @param string $msg Message to display on redirect. Optional, defaults to + * value set internally by controller, if any. + * @param string $type Message type. Optional, defaults to 'message'. + * @return object + * @deprecated + */ + public function setMessage($msg, $type='message') + { + // controller may have set this directly + $this->_message = $msg; + $this->_messageType = $type; + + return $this; + } + + /** + * Method to check admin access permission + * + * @return boolean True on success + * @deprecated + */ + protected function _authorize() + { + // Check if they are logged in + if ($this->juser->isGuest()) + { + return false; + } + + if ($this->juser->authorise('core.admin', $this->_option)) + { + return true; + } + + return false; + } + + /** + * Perform before actually calling the given task + * + * @return void + */ + protected function _onBeforeDoTask() + { + // Do nothing - override in subclass + } +} diff --git a/core/libraries/Hubzero/Component/View.php b/core/libraries/Hubzero/Component/View.php new file mode 100644 index 00000000000..f2a7be257a5 --- /dev/null +++ b/core/libraries/Hubzero/Component/View.php @@ -0,0 +1,127 @@ + + * @return void + */ + public function __construct($config = array()) + { + parent::__construct($config); + + // Set a base path for use by the view + if (!array_key_exists('base_path', $config)) + { + $config['base_path'] = ''; + + if (defined('PATH_COMPONENT')) + { + $config['base_path'] = PATH_COMPONENT; + } + } + $this->_basePath = $config['base_path']; + } + + /** + * Create a component view and return it + * + * @param string $layout View layout + * @param string $name View name + * @return object + */ + public function view($layout, $name=null) + { + // If we were passed only a view model, just render it. + if ($layout instanceof AbstractView) + { + return $layout; + } + + $view = new self(array( + 'base_path' => $this->_basePath, + 'name' => ($name ? $name : $this->_name), + 'layout' => $layout + )); + $view->set('option', $this->option) + ->set('controller', $this->controller) + ->set('task', $this->task); + + return $view; + } + + /** + * Dynamically handle calls to the class. + * + * @param string $method + * @param array $parameters + * @return mixed + * @throws \BadMethodCallException + * @since 1.3.1 + */ + public function __call($method, $parameters) + { + if (!static::hasHelper($method)) + { + foreach ($this->_path['helper'] as $path) + { + $file = $path . DIRECTORY_SEPARATOR . $method . '.php'; + if (file_exists($file)) + { + include_once $file; + break; + } + } + + $option = ($this->option ? $this->option : \Request::getCmd('option')); + $option = ucfirst(substr($option, 4)); + + // Namespaced + $invokable1 = '\\Components\\' . $option . '\\Helpers\\' . ucfirst($method); + + // Old naming scheme "OptionHelperMethod" + $invokable2 = $option . 'Helper' . ucfirst($method); + + $callback = null; + if (class_exists($invokable1)) + { + $callback = new $invokable1(); + } + else if (class_exists($invokable2)) + { + $callback = new $invokable2(); + } + + if (is_callable($callback)) + { + $callback->setView($this); + + $this->helper($method, $callback); + } + } + + return parent::__call($method, $parameters); + } +} diff --git a/core/libraries/Hubzero/Config/Exception/EmptyDirectoryException.php b/core/libraries/Hubzero/Config/Exception/EmptyDirectoryException.php new file mode 100644 index 00000000000..dc429ceb6d5 --- /dev/null +++ b/core/libraries/Hubzero/Config/Exception/EmptyDirectoryException.php @@ -0,0 +1,12 @@ +defaultPath = $defaultPath; + } + + /** + * Get the default path + * + * @return string + */ + public function getDefaultPath() + { + return $this->defaultPath; + } + + /** + * Load the given configuration group. + * + * @param string $client + * @return array + */ + public function load($client = null) + { + $data = array(); + + // First we'll get the root configuration path for the environment which is + // where all of the configuration files live for that namespace, as well + // as any environment folders with their specific configuration items. + try + { + $paths = $this->getPaths($this->defaultPath); + + if (empty($paths)) + { + throw new EmptyDirectoryException("Configuration directory: [" . $this->defaultPath . "] is empty"); + } + + foreach ($paths as $path) + { + // Get file information + $info = pathinfo($path); + $group = isset($info['filename']) ? strtolower($info['filename']) : ''; + $extension = isset($info['extension']) ? strtolower($info['extension']) : ''; + + if (!$extension || $extension == 'html') + { + continue; + } + + $data[$group] = $this->getParser($extension)->parse($path); + } + + if (empty($data)) + { + throw new EmptyDirectoryException("Configuration directory: [" . $this->defaultPath . "] is empty"); + } + + // If a client is specified... + if ($client) + { + $paths = $this->getPaths($this->defaultPath . DIRECTORY_SEPARATOR . $client); + + foreach ($paths as $path) + { + // Get file information + $info = pathinfo($path); + $group = isset($info['filename']) ? strtolower($info['filename']) : ''; + $extension = isset($info['extension']) ? strtolower($info['extension']) : ''; + + if (!$extension || $extension == 'html') + { + continue; + } + + if (!isset($data[$group])) + { + $data[$group] = array(); + } + + $data[$group] = array_replace_recursive( + $data[$group], + $this->getParser($extension)->parse($path) + ); + } + } + } + catch (\Exception $e) + { + $loader = new Legacy(); + + $data = $loader->toArray(); + + if (!empty($data)) + { + $loader->split(); + } + } + + return $data; + } + + /** + * Gets a parser for a given file extension + * + * @param string $extension + * @return object + * @throws UnsupportedFormatException If `$extension` is an unsupported file format + */ + protected function getParser($extension) + { + $parser = null; + + $extension = strtolower($extension); + + foreach (Processor::all() as $fileParser) + { + if (in_array($extension, $fileParser->getSupportedExtensions())) + { + $parser = $fileParser; + break; + } + } + + // If none exist, then throw an exception + if ($parser === null) + { + throw new UnsupportedFormatException(sprintf('Unsupported configuration format "%s"', $extension)); + } + + return $parser; + } + + /** + * Checks `$path` to see if it is either an array, a directory, or a file + * + * @param mixed $path + * @return array + */ + protected function getPaths($path) + { + $paths = array(); + + // If `$path` is an array + if (is_array($path)) + { + foreach ($path as $unverifiedPath) + { + $paths = array_merge($paths, $this->getPaths($unverifiedPath)); + } + + return $paths; + } + + // If `$path` is a directory + if (is_dir($path)) + { + $paths = glob($path . '/*.*'); + + return $paths; + } + + // If `$path` is a file + if (file_exists($path)) + { + $paths[] = $path; + } + + return $paths; + } +} diff --git a/core/libraries/Hubzero/Config/FileWriter.php b/core/libraries/Hubzero/Config/FileWriter.php new file mode 100644 index 00000000000..755b7ddab73 --- /dev/null +++ b/core/libraries/Hubzero/Config/FileWriter.php @@ -0,0 +1,141 @@ + 'array')) + { + $this->format = $format; + $this->path = $path; + $this->options = $options; + } + + /** + * Create a new file configuration loader. + * + * @param object $contents + * @param string $group + * @param string $client + * @return boolean + */ + public function write($contents, $group, $client = null) + { + $path = $this->getPath($client, $group); + + if (!$path) + { + return false; + } + + $contents = $this->toContent($contents, $this->format, $this->options); + + return !($this->putContent($path, $contents) === false); + } + + /** + * Generate the path to write + * + * @param string $client + * @param string $group + * @return string + */ + private function getPath($client, $group) + { + $path = $this->path; + + if (is_null($path)) + { + return null; + } + + $file = $path . DIRECTORY_SEPARATOR . ($client ? $client . DIRECTORY_SEPARATOR : '') . $group . '.' . $this->format; + + return $file; + } + + /** + * Turn contents into a string of the correct format + * + * @param mixed $content + * @param string $format + * @param array $options + * @return string + */ + public function toContent($contents, $format, $options = array()) + { + if (!($contents instanceof Registry)) + { + $contents = new Registry($contents); + } + + return $contents->toString($format, $options); + } + + /** + * Write the contents of a file. + * + * @param string $path + * @param string $contents + * @param mixed $mode + * @return boolean + */ + public function putContent($file, $contents, $mode = '0640') + { + $path = dirname($file); + + if (!is_dir($path)) + { + if (!@mkdir($path, 0750)) + { + return false; + } + } + + $result = @file_put_contents($file, $contents); + + if ($result) + { + @chmod($file, octdec($mode)); + } + + return $result; + } +} diff --git a/core/libraries/Hubzero/Config/Legacy.php b/core/libraries/Hubzero/Config/Legacy.php new file mode 100644 index 00000000000..8016fa31529 --- /dev/null +++ b/core/libraries/Hubzero/Config/Legacy.php @@ -0,0 +1,283 @@ + array( + 'application_env', + 'editor', + 'list_limit', + 'helpurl', + 'debug', + 'debug_lang', + 'feed_limit', + 'feed_email', + 'secret', + 'gzip', + 'error_reporting', + 'api_server', + 'xmlrpc_server', + 'log_path', + 'tmp_path', + 'live_site', + 'force_ssl', + 'offset', + 'sitename', + 'robots', + 'captcha', + 'access' + ), + 'cache' => array( + 'caching', + 'cachetime', + 'cache_handler', + 'memcache_settings' + ), + 'database' => array( + 'dbtype', + 'host', + 'user', + 'password', + 'db', + 'dbcharset', + 'dbcollation', + 'dbprefix' + ), + 'ftp' => array( + 'ftp_enabled', + 'ftp_host', + 'ftp_port', + 'ftp_user', + 'ftp_pass', + 'ftp_root' + ), + 'mail' => array( + 'mailer', + 'mailfrom', + 'fromname', + 'smtpauth', + 'smtphost', + 'smtpport', + 'smtpuser', + 'smtppass', + 'smtpsecure', + 'sendmail' + ), + 'meta' => array( + 'MetaAuthor', + 'MetaTitle', + 'MetaDesc', + 'MetaKeys', + 'MetaRights', + 'MetaVersion' + ), + 'offline' => array( + 'display_offline_message', + 'offline_image', + 'offline_message', + 'offline' + ), + 'seo' => array( + 'sef', + 'sef_rewrite', + 'sef_suffix', + 'sef_groups', + 'unicodeslugs', + 'sitename_pagetitles' + ), + 'session' => array( + 'session_handler', + 'lifetime', + 'cookiesubdomains', + 'cookie_path', + 'cookie_domain' + ) + ); + + /** + * Create a new configuration repository. + * + * @param string $path + * @return void + */ + public function __construct($path = null) + { + $this->reset(); + + if (!$path) + { + $path = defined('PATH_ROOT') ? PATH_ROOT : __DIR__; + } + + $this->file = $path . DIRECTORY_SEPARATOR . 'configuration.php'; + + if ($this->exists()) + { + $data = $this->read($this->file); + $data = \Hubzero\Utility\Arr::fromObject($data); + + $config = array(); + + foreach ($data as $key => $value) + { + foreach ($this->map as $group => $values) + { + if (!isset($config[$group])) + { + $config[$group] = array(); + } + if (in_array($key, $values)) + { + $config[$group][$key] = $value; + } + } + } + + parent::__construct($config); + } + } + + /** + * Determine if the given configuration exists. + * + * @return bool + */ + public function exists() + { + return file_exists($this->file); + } + + /** + * Determine if the given configuration value exists. + * + * @param string $file Path to file to load + * @return bool + */ + public function read($file) + { + if (!file_exists($file) || (filesize($file) < 10)) + { + throw new FileNotFoundException('No configuration file found and no installation code available.', 500); + } + + require_once $file; + + if (!class_exists('\\JConfig')) + { + throw new UnsupportedFormatException('Invalid configuration file.', 500); + } + + $config = new \JConfig; + + if (defined('PATH_ROOT') && defined('PATH_APP')) + { + if (isset($config->tmp_path)) + { + if (substr($config->tmp_path, strlen(PATH_ROOT)) == DIRECTORY_SEPARATOR . 'tmp') + { + $config->tmp_path = PATH_APP . substr($config->tmp_path, strlen(PATH_ROOT)); + } + } + + if (isset($config->log_path)) + { + if (substr($config->log_path, strlen(PATH_ROOT)) == DIRECTORY_SEPARATOR . 'logs') + { + $config->log_path = PATH_APP . substr($config->log_path, strlen(PATH_ROOT)); + } + } + } + + return $config; + } + + /** + * Split the config file into new format + * + * @param string $format + * @param string $path + * @return void + */ + public function split($format = null, $path = null) + { + $format = $format ?: 'php'; + if (!$path) + { + $path = defined('PATH_ROOT') ? PATH_ROOT : __DIR__; + $path .= DIRECTORY_SEPARATOR . 'config'; + } + + $writer = new FileWriter( + $format, + $path + ); + + foreach ($this->map as $group => $values) + { + $contents = array(); + foreach ($values as $key) + { + $contents[$key] = $this->get($group . '.' . $key); + } + + $writer->write($contents, $group); + } + } + + /** + * Write the contents of the legacy config + * + * @param string $file + * @return bool + */ + public function update($file = null) + { + $file = ($file ?: $this->file); + + $contents = $this->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + $original = '0640'; + + if (is_file($file)) + { + // Track original permissions + $original = '0' . decoct(fileperms($file) & 0777); + + // First try and make sure the file is writable + @chmod($file, octdec('0640')); + } + + $result = file_put_contents($file, $contents); + + if (is_file($file)) + { + @chmod($file, octdec($original)); + } + + return $result; + } +} diff --git a/core/libraries/Hubzero/Config/Processor.php b/core/libraries/Hubzero/Config/Processor.php new file mode 100644 index 00000000000..36f52697bdb --- /dev/null +++ b/core/libraries/Hubzero/Config/Processor.php @@ -0,0 +1,143 @@ +getSupportedExtensions())) + { + $class = get_class($inst); + } + } + + if (!class_exists($class)) + { + throw new InvalidArgumentException(sprintf('Unable to load format class for format "%s"', $type), 500); + } + } + + self::$instances[$type] = new $class; + } + + return self::$instances[$type]; + } + + /** + * Return a list of all available processors. + * + * @return array + */ + public static function all() + { + foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'Processor' . DIRECTORY_SEPARATOR . '*.php') as $path) + { + $type = strtolower(basename($path, '.php')); + + if (!isset(self::$instances[$type])) + { + $class = __NAMESPACE__ . '\\Processor\\' . ucfirst($type); + + if (!class_exists($class)) + { + include_once $path; + } + + self::$instances[$type] = new $class; + } + } + + return self::$instances; + } + + /** + * Returns an array of allowed file extensions for this parser + * + * @return array + */ + public function getSupportedExtensions() + { + return array(); + } + + /** + * Parses a file from `$path` and gets its contents as an array + * + * @param string $path + * @return array + */ + public function parse($path) + { + return array(); + } + + /** + * Try to determine if the data can be parsed + * + * @param string $data + * @return boolean + */ + public function canParse($data) + { + return false; + } + + /** + * Converts an object into a formatted string. + * + * @param object $object Data Source Object. + * @param array $options An array of options for the formatter. + * @return string Formatted string. + */ + abstract public function objectToString($object, $options = null); + + /** + * Converts a formatted string into an object. + * + * @param string $data Formatted string + * @param array $options An array of options for the formatter. + * @return object Data Object + */ + abstract public function stringToObject($data, $options = null); +} diff --git a/core/libraries/Hubzero/Config/Processor/Ini.php b/core/libraries/Hubzero/Config/Processor/Ini.php new file mode 100644 index 00000000000..4dc306a285c --- /dev/null +++ b/core/libraries/Hubzero/Config/Processor/Ini.php @@ -0,0 +1,302 @@ + $value) + { + // If the value is an object then we need to put it in a local section. + if (is_object($value)) + { + // Add the section line. + $local[] = ''; + $local[] = '[' . $key . ']'; + + // Add the properties for this section. + foreach (get_object_vars($value) as $k => $v) + { + $local[] = $k . '=' . $this->getValueAsINI($v); + } + } + else + { + // Not in a section so add the property to the global array. + $global[] = $key . '=' . $this->getValueAsINI($value); + } + } + + return implode("\n", array_merge($global, $local)); + } + + /** + * Parse an INI formatted string and convert it into an object. + * + * @param string $data INI formatted string to convert. + * @param mixed $options An array of options used by the formatter, or a boolean setting to process sections. + * @return object Data object. + */ + public function stringToObject($data, $options = array()) + { + if (is_object($data)) + { + return $data; + } + + $sections = (isset($options['processSections'])) ? $options['processSections'] : false; + + // Check the memory cache for already processed strings. + $hash = md5($data . ':' . (int) $sections); + if (isset(self::$cache[$hash])) + { + return self::$cache[$hash]; + } + + // If no lines present just return the object. + if (empty($data)) + { + return new stdClass; + } + + // Initialize variables. + $obj = new stdClass; + $section = false; + $lines = explode("\n", $data); + + // Process the lines. + foreach ($lines as $line) + { + // Trim any unnecessary whitespace. + $line = trim($line); + + // Ignore empty lines and comments. + if (empty($line) || ($line{0} == ';')) + { + continue; + } + + if ($sections) + { + $length = strlen($line); + + // If we are processing sections and the line is a section add the object and continue. + if (($line[0] == '[') && ($line[$length - 1] == ']')) + { + $section = substr($line, 1, $length - 2); + $obj->$section = new stdClass; + continue; + } + } + elseif ($line{0} == '[') + { + continue; + } + + // Check that an equal sign exists and is not the first character of the line. + if (!strpos($line, '=')) + { + // Maybe throw exception? + continue; + } + + // Get the key and value for the line. + list ($key, $value) = explode('=', $line, 2); + + // Validate the key. + if (preg_match('/[^A-Z0-9_]/i', $key)) + { + // Maybe throw exception? + continue; + } + + // If the value is quoted then we assume it is a string. + $length = strlen($value); + if ($length && ($value[0] == '"') && ($value[$length - 1] == '"')) + { + // Strip the quotes and Convert the new line characters. + $value = stripcslashes(substr($value, 1, ($length - 2))); + $value = str_replace('\n', "\n", $value); + } + else + { + // If the value is not quoted, we assume it is not a string. + + // If the value is 'false' assume boolean false. + if ($value == 'false') + { + $value = false; + } + // If the value is 'true' assume boolean true. + elseif ($value == 'true') + { + $value = true; + } + // If the value is numeric than it is either a float or int. + elseif (is_numeric($value)) + { + // If there is a period then we assume a float. + if (strpos($value, '.') !== false) + { + $value = (float) $value; + } + else + { + $value = (int) $value; + } + } + } + + // If a section is set add the key/value to the section, otherwise top level. + if ($section) + { + $obj->$section->$key = $value; + } + else + { + $obj->$key = $value; + } + } + + // Cache the string to save cpu cycles -- thus the world :) + self::$cache[$hash] = clone ($obj); + + return $obj; + } + + /** + * Method to get a value in an INI format. + * + * @param mixed $value The value to convert to INI format. + * @return string The value in INI format. + */ + protected function getValueAsINI($value) + { + // Initialize variables. + $string = ''; + + switch (gettype($value)) + { + case 'integer': + case 'double': + $string = $value; + break; + + case 'boolean': + $string = $value ? 'true' : 'false'; + break; + + case 'string': + // Sanitize any CRLF characters.. + $string = '"' . str_replace(array("\r\n", "\n"), '\\n', $value) . '"'; + break; + } + + return $string; + } +} diff --git a/core/libraries/Hubzero/Config/Processor/Json.php b/core/libraries/Hubzero/Config/Processor/Json.php new file mode 100644 index 00000000000..07e31578154 --- /dev/null +++ b/core/libraries/Hubzero/Config/Processor/Json.php @@ -0,0 +1,144 @@ + $error_message, + 'type' => json_last_error(), + 'file' => $path, + ); + throw new ParseException($error); + } + + return $data; + } + + /** + * Try to determine if the data can be parsed + * + * @param string $data + * @return boolean + */ + public function canParse($data) + { + $data = trim($data); + $data = trim($data, '"'); + + if ((substr($data, 0, 1) != '{') && (substr($data, -1, 1) != '}')) + { + return false; + } + + $obj = json_decode($data); + if (json_last_error() != JSON_ERROR_NONE) + { + return false; + } + + return true; + } + + /** + * Converts an object into a JSON formatted string. + * + * @param object $object Data source object. + * @param array $options Options used by the formatter. + * @return string JSON formatted string. + */ + public function objectToString($object, $options = array()) + { + if (is_string($object)) + { + return $object; + } + + if (isset($options['pretty_print']) && $options['pretty_print']) + { + return json_encode($object, JSON_PRETTY_PRINT); + } + + return json_encode($object); + } + + /** + * Parse a JSON formatted string and convert it into an object. + * + * If the string is not in JSON format, this method will attempt to parse it as INI format. + * + * @param string $data JSON formatted string to convert. + * @param array $options Options used by the formatter. + * @return object Data object. + */ + public function stringToObject($data, $options = array('processSections' => false)) + { + if (is_object($data)) + { + return $data; + } + + if (is_bool($options)) + { + $options = array('processSections' => $options); + } + + $data = trim($data); + $data = trim($data, '"'); + + if ((substr($data, 0, 1) != '{') && (substr($data, -1, 1) != '}')) + { + $obj = Base::instance('ini')->stringToObject($data, $options); + } + else + { + $obj = json_decode($data); + } + return $obj; + } +} diff --git a/core/libraries/Hubzero/Config/Processor/Php.php b/core/libraries/Hubzero/Config/Processor/Php.php new file mode 100644 index 00000000000..213387f8ffa --- /dev/null +++ b/core/libraries/Hubzero/Config/Processor/Php.php @@ -0,0 +1,199 @@ + 'PHP file threw an exception', + 'exception' => $exception, + ) + ); + } + + // If we have a callable, run it and expect an array back + if (is_callable($temp)) + { + $temp = call_user_func($temp); + } + + // Check for array, if its anything else, throw an exception + if (!$temp || !is_array($temp)) + { + throw new UnsupportedFormatException('PHP file does not return an array'); + } + + return $temp; + } + + /** + * Converts an object into a php class or array string. + * + * @param object $object Data Source Object + * @param array $options Parameters used by the formatter + * @return string Config class formatted string + */ + public function objectToString($object, $options = array()) + { + if (is_string($object)) + { + return $object; + } + + $format = 'object'; + if (isset($options['format']) && $options['format']) + { + $format = $options['format']; + } + + // Build the object variables string + $vars = ''; + foreach (get_object_vars($object) as $k => $v) + { + if (is_scalar($v)) + { + switch ($format) + { + case 'object': + $vars .= "\tvar $" . $k . " = '" . addcslashes($v, '\\\'') . "';\n"; + break; + case 'array': + $vars .= "\t'" . $k . "' => '" . addcslashes($v, '\\\'') . "',\n"; + break; + } + } + elseif (is_array($v) || is_object($v)) + { + switch ($format) + { + case 'object': + $vars .= "\tvar $" . $k . " = " . $this->getArrayString((array) $v) . ";\n"; + break; + case 'array': + $vars .= "\t'" . $k . "' => " . $this->getArrayString((array) $v) . ",\n"; + break; + } + } + elseif (is_null($v)) + { + switch ($format) + { + case 'object': + $vars .= "\tvar $" . $k . " = '';\n"; + break; + case 'array': + $vars .= "\t'" . $k . "' => '',\n"; + break; + } + } + } + + $str = ""; + } + + return $str; + } + + /** + * Parse a PHP class formatted string and convert it into an object. + * + * @param string $data PHP Class formatted string to convert. + * @param array $options Options used by the formatter. + * @return object Data object. + */ + public function stringToObject($data, $options = array()) + { + return true; + } + + /** + * Method to get an array as an exported string. + * + * @param array $a The array to get as a string. + * @return array + */ + protected function getArrayString($a) + { + $s = 'array('; + $i = 0; + foreach ($a as $k => $v) + { + $s .= ($i) ? ', ' : ''; + $s .= '"' . $k . '" => '; + if (is_array($v) || is_object($v)) + { + $s .= $this->getArrayString((array) $v); + } + else + { + $s .= '"' . addslashes($v) . '"'; + } + $i++; + } + $s .= ')'; + return $s; + } +} diff --git a/core/libraries/Hubzero/Config/Processor/Xml.php b/core/libraries/Hubzero/Config/Processor/Xml.php new file mode 100644 index 00000000000..8f98d64798d --- /dev/null +++ b/core/libraries/Hubzero/Config/Processor/Xml.php @@ -0,0 +1,210 @@ + $latestError->message, + 'type' => $latestError->level, + 'code' => $latestError->code, + 'file' => $latestError->file, + 'line' => $latestError->line, + ); + throw new ParseException($error); + } + + $data = new stdClass; + foreach ($xml->children() as $node) + { + $data->{$node['name']} = $this->getValueFromNode($node); + } + $data = json_decode(json_encode($data), true); + + return $data; + } + + /** + * Try to determine if the data can be parsed + * + * @param string $data + * @return boolean + */ + public function canParse($data) + { + $data = trim($data); + + if ((substr($data, 0, 1) != '<') && (substr($data, -1, 1) != '>')) + { + return false; + } + + return true; + } + + /** + * Converts an object into an XML formatted string. + * + * @param object $object Data source object. + * @param array $options Options used by the formatter. + * @return string XML formatted string. + */ + public function objectToString($object, $options = array()) + { + if (is_string($object)) + { + return $object; + } + + // Initialise variables. + $rootName = (isset($options['name'])) ? $options['name'] : 'registry'; + $nodeName = (isset($options['nodeName'])) ? $options['nodeName'] : 'node'; + + // Create the root node. + $root = simplexml_load_string('<' . $rootName . ' />'); + + // Iterate over the object members. + $this->getXmlChildren($root, $object, $nodeName); + + return $root->asXML(); + } + + /** + * Parse a XML formatted string and convert it into an object. + * + * @param string $data XML formatted string to convert. + * @param array $options Options used by the formatter. + * @return object Data object. + */ + public function stringToObject($data, $options = array()) + { + if (is_object($data)) + { + return $data; + } + + $obj = new stdClass; + + $xml = simplexml_load_string($data); + + foreach ($xml->children() as $node) + { + $obj->{$node['name']} = $this->getValueFromNode($node); + } + + return $obj; + } + + /** + * Method to get a PHP native value for a SimpleXMLElement object. -- called recursively + * + * @param object $node SimpleXMLElement object for which to get the native value. + * @return mixed Native value of the SimpleXMLElement object. + */ + protected function getValueFromNode($node) + { + switch ($node['type']) + { + case 'integer': + $value = (string) $node; + return (int) $value; + break; + case 'string': + return (string) $node; + break; + case 'boolean': + $value = (string) $node; + return (bool) $value; + break; + case 'double': + $value = (string) $node; + return (float) $value; + break; + case 'array': + $value = array(); + foreach ($node->children() as $child) + { + $value[(string) $child['name']] = $this->getValueFromNode($child); + } + break; + default: + $value = new stdClass; + foreach ($node->children() as $child) + { + $value->{$child['name']} = $this->getValueFromNode($child); + } + break; + } + + return $value; + } + + /** + * Method to build a level of the XML string -- called recursively + * + * @param object &$node SimpleXMLElement object to attach children. + * @param object $var Object that represents a node of the XML document. + * @param string $nodeName The name to use for node elements. + * @return void + */ + protected function getXmlChildren(&$node, $var, $nodeName) + { + // Iterate over the object members. + foreach ((array) $var as $k => $v) + { + if (is_scalar($v)) + { + $n = $node->addChild($nodeName, $v); + $n->addAttribute('name', $k); + $n->addAttribute('type', gettype($v)); + } + else + { + $n = $node->addChild($nodeName); + $n->addAttribute('name', $k); + $n->addAttribute('type', gettype($v)); + + $this->getXmlChildren($n, $v, $nodeName); + } + } + } +} diff --git a/core/libraries/Hubzero/Config/Processor/Yaml.php b/core/libraries/Hubzero/Config/Processor/Yaml.php new file mode 100644 index 00000000000..ef0885b5353 --- /dev/null +++ b/core/libraries/Hubzero/Config/Processor/Yaml.php @@ -0,0 +1,187 @@ + 'Error parsing YAML', + 'exception' => $exception, + ) + ); + } + + return $data; + } + + /** + * Try to determine if the data can be parsed + * + * @param string $data + * @return boolean + */ + public function canParse($data) + { + $data = trim($data); + + try + { + // Parse config string + $parsed = SymfonyYaml::parse($data, true); + } + catch (Exception $e) + { + return false; + } + + return true; + } + + /** + * Converts an object into a YAML formatted string. + * + * @param object $object Data source object. + * @param array $options Options used by the formatter. + * @return string YAML formatted string. + */ + public function objectToString($object, $options = array()) + { + if (is_string($object)) + { + return $object; + } + + return SymfonyYaml::dump((array) $this->asArray($object), 2); + } + + /** + * Method to recursively convert an object of data to an array. + * + * @param object $data An object of data to return as an array. + * @return array Array representation of the input object. + */ + protected function asArray($data) + { + $array = array(); + + foreach (get_object_vars((object) $data) as $k => $v) + { + if (is_object($v)) + { + $array[$k] = $this->asArray($v); + } + else + { + $array[$k] = $v; + } + } + + return $array; + } + + /** + * Parse a YAML formatted string and convert it into an object. + * + * @param string $data YAML formatted string to convert. + * @param array $options Options used by the formatter. + * @return object Data object. + */ + public function stringToObject($data, $options = array()) + { + if (is_object($data)) + { + return $data; + } + + $data = trim($data); + + // Try to parse, catching exception if it fails + try + { + // Parse config string + $parsed = SymfonyYaml::parse($data, true); + } + catch (Exception $e) + { + // Throw an exception Hubzero knows how to catch + throw new ParseException( + array( + 'message' => 'Error parsing YAML', + 'exception' => $e, + ) + ); + } + + if (!$parsed) + { + $parsed = ''; + } + + return (is_string($parsed) ? $parsed : $this->toObject($parsed)); + } + + /** + * Convert an array to an object + * + * @param array $data + * @return object Data object. + */ + protected function toObject($data) + { + $obj = new stdClass; + + foreach ($data as $key => $datum) + { + if (is_array($datum)) + { + $obj->$key = $this->toObject($datum); + } + else + { + $obj->$key = $datum; + } + } + + return $obj; + } +} diff --git a/core/libraries/Hubzero/Config/Registry.php b/core/libraries/Hubzero/Config/Registry.php new file mode 100644 index 00000000000..065b0283e1c --- /dev/null +++ b/core/libraries/Hubzero/Config/Registry.php @@ -0,0 +1,600 @@ +data = new stdClass; + + // Optionally load supplied data. + if ($data) + { + $this->parse($data, $format); + } + } + + /** + * Magic function to clone the registry object. + * + * @return object + */ + public function __clone() + { + $this->data = unserialize(serialize($this->data)); + } + + /** + * Magic function to render this object as a string using default args of toString method. + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Reset all internal data + * + * @return void + */ + public function reset() + { + $this->data = new stdClass; + } + + /** + * Gets this object represented as an ArrayIterator. + * + * This allows the data properties to be accessed via a foreach statement. + * + * @return object This object represented as an ArrayIterator. + * @see IteratorAggregate::getIterator() + */ + public function getIterator() + { + return new \ArrayIterator($this->data); + } + + /** + * Count elements of the data object + * + * @return integer The custom count as an integer. + */ + public function count() + { + return count(get_object_vars($this->data)); + } + + /** + * Implementation for the JsonSerializable interface. + * Allows us to pass Registry objects to json_encode. + * + * @return object + */ + public function jsonSerialize() + { + return $this->data; + } + + /** + * Sets a default value if not already assigned. + * + * @param string $key The name of the parameter. + * @param string $default An optional value for the parameter. + * @return string The value set, or the default if the value was not previously set (or null). + */ + public function def($key, $default = '') + { + $value = $this->get($key, (string) $default); + + $this->set($key, $value); + + return $value; + } + + /** + * Check if a registry path exists. + * + * @param string $path Registry path + * @return boolean + */ + public function has($path) + { + $default = microtime(true); + + return $this->get($path, $default) !== $default; + } + + /** + * Get a registry value. + * + * @param string $path Registry path (e.g. config.cache.file) + * @param mixed $default Optional default value, returned if the internal value is null. + * @return mixed Value of entry or null + */ + public function get($path, $default = null) + { + // Return default value if path is empty + if (empty($path)) + { + return $default; + } + + if (!strpos($path, $this->separator)) + { + return (isset($this->data->$path) && $this->data->$path !== null && $this->data->$path !== '') ? $this->data->$path : $default; + } + + // Explode the registry path into an array + $nodes = explode($this->separator, $path); + + // Initialize the current node to be the registry root. + $node = $this->data; + $found = false; + + // Traverse the registry to find the correct node for the result. + foreach ($nodes as $n) + { + if (is_array($node) && isset($node[$n])) + { + $node = $node[$n]; + $found = true; + continue; + } + + if (!isset($node->$n)) + { + return $default; + } + + $node = $node->$n; + $found = true; + } + + if (!$found || $node === null || $node === '') + { + return $default; + } + + return $node; + } + + /** + * Set a registry value. + * + * @param string $path Registry Path (e.g. config.cache.file) + * @param mixed $value Value of entry + * @param string $separator The key separator + * @return object This method is chainable + */ + public function set($path, $value, $separator = null) + { + if (empty($separator)) + { + $separator = $this->separator; + } + + // Explode the registry path into an array and remove empty + // nodes that occur as a result of a double separator. ex: foo..test + // Finally, re-key the array so they are sequential. + $nodes = array_values(array_filter(explode($separator, $path), 'strlen')); + + if ($nodes) + { + // Initialize the current node to be the registry root. + $node = $this->data; + + // Traverse the registry to find the correct node for the result. + for ($i = 0, $n = count($nodes) - 1; $i < $n; $i++) + { + if (is_object($node)) + { + if (!isset($node->{$nodes[$i]}) && ($i != $n)) + { + $node->{$nodes[$i]} = new stdClass; + } + + // Pass the child as pointer in case it is an object + $node = &$node->{$nodes[$i]}; + + continue; + } + + if (is_array($node)) + { + if (!isset($node[$nodes[$i]]) && ($i != $n)) + { + $node[$nodes[$i]] = new stdClass; + } + + // Pass the child as pointer in case it is an array + $node = &$node[$nodes[$i]]; + } + } + } + + // Get the old value if exists so we can return it + if (is_object($node)) + { + $node->{$nodes[$i]} = $value; + } + else if (is_array($node)) + { + $node[$nodes[$i]] = $value; + } + + return $this; + } + + /** + * Load the contents of a file into the registry + * + * @param string $file Path to file to load + * @return boolean True on success + * @throws InvalidArgumentException + */ + public function read($file) + { + if (is_file($file)) + { + return file_get_contents($file); + } + + throw new InvalidArgumentException(sprintf('File does not exist at path %s', $file)); + } + + /** + * Write the contents of the registry to a file + * + * @param string $file Path to file to load + * @param string $format Format of the file [optional: defaults to JSON] + * @param mixed $options Options used by the formatter + * @return boolean True on success + */ + public function write($file, $format = 'json', $options = array()) + { + return file_put_contents($file, $this->processor($format)->objectToString($this->data, $options)); + } + + /** + * Load a string into the registry + * + * @param string $data String to load into the registry + * @param string $format Format of the string + * @param mixed $options Options used by the formatter + * @return boolean True on success + */ + public function parse($data, $format = '', $options = array()) + { + if (is_array($data) || is_object($data)) + { + $this->bind($this->data, $data); + } + else if (!empty($data) && is_string($data)) + { + // See if it's a file or a string + if (is_file($data) && is_readable($data)) + { + $data = $this->read($data); + } + + if (!$format) + { + foreach ($this->processors() as $name => $processor) + { + if ($processor->canParse($data)) + { + $format = $name; + break; + } + } + + if (!$format) + { + return false; + } + } + + $obj = $this->processor($format)->stringToObject($data, $options); + + $this->bind($this->data, $obj); + } + + return true; + } + + /** + * Merge a Registry object into this one + * + * @param mixed $source Source data to merge. + * @param boolean $recursive True to support recursive merge the children values. + * @return boolean True on success + */ + public function merge($source, $recursive = false) + { + if (!$source) + { + return false; + } + + // If the source isn't already a Registry + // we'll turn it into one + if (!($source instanceof Registry) && !method_exists($source, 'toArray')) + { + $source = new self($source); + } + + // Load the variables into the registry's default namespace. + $this->bind($this->data, $source->toArray(), $recursive, false); + + return true; + } + + /** + * Transforms a namespace to an array + * + * @return array An associative array holding the namespace data + */ + public function toArray() + { + return (array) $this->asArray($this->data); + } + + /** + * Transforms a namespace to an object + * + * @return object An an object holding the namespace data + */ + public function toObject() + { + return $this->data; + } + + /** + * Get a namespace in a given string format + * + * @param string $format Format to return the string in + * @param mixed $options Parameters used by the formatter, see formatters for more info + * @return string Namespace in string format + */ + public function toString($format = 'json', $options = array()) + { + return $this->processor($format)->objectToString($this->data, $options); + } + + /** + * Get the list of all available processors + * + * @return array + */ + public function processors() + { + return Processor::all(); + } + + /** + * Get the processor for a specific format + * + * @param string $format Format to return the processor for + * @return object + */ + public function processor($format = 'json') + { + return Processor::instance($format); + } + + /** + * Method to recursively bind data to a parent object. + * + * @param object $parent The parent object on which to attach the data values. + * @param mixed $data An array or object of data to bind to the parent object. + * @param boolean $recursive True to support recursive bindData. + * @param boolean $allowNull True to allow null values. + * @return void + */ + protected function bind($parent, $data, $recursive = true, $allowNull = true) + { + // Ensure the input data is an array. + $data = is_object($data) + ? get_object_vars($data) + : (array) $data; + + foreach ($data as $k => $v) + { + if (!$allowNull && !(($v !== null) && ($v !== ''))) + { + continue; + } + + if ($recursive && ((is_array($v) && Arr::isAssociative($v)) || is_object($v))) + { + if (!isset($parent->$k) || !is_object($parent->$k)) + { + $parent->$k = new stdClass; + } + + $this->bind($parent->$k, $v, $recursive, $allowNull); + continue; + } + + $parent->$k = $v; + } + } + + /** + * Method to recursively convert an object of data to an array. + * + * @param object $data An object of data to return as an array. + * @return array Array representation of the input object. + */ + protected function asArray($data) + { + $array = array(); + + if (is_object($data)) + { + $data = get_object_vars($data); + } + + foreach ($data as $k => $v) + { + if (is_object($v) || is_array($v)) + { + $array[$k] = $this->asArray($v); + continue; + } + $array[$k] = $v; + } + + return $array; + } + + /** + * Method to extract a sub-registry from path + * + * @param string $path Registry path (e.g. config.cache.file) + * @return mixed Registry object if data is present + */ + public function extract($path) + { + $data = $this->get($path); + + if (is_null($data)) + { + return null; + } + + return new self($data); + } + + /** + * Dump to one dimension array. + * + * @param string $separator The key separator. + * @return array Dumped array. + */ + public function flatten($separator = null) + { + $array = array(); + + $this->toFlatten($separator, $this->data, $array); + + return $array; + } + + /** + * Method to recursively convert data to one dimension array. + * + * @param string $separator The key separator. + * @param array|object $data Data source of this scope. + * @param array &$array The result array, it is pass by reference. + * @param string $prefix Last level key prefix. + * @return void + */ + protected function toFlatten($separator = null, $data = null, &$array = array(), $prefix = '') + { + $data = (array) $data; + + if (empty($separator)) + { + $separator = $this->separator; + } + + foreach ($data as $k => $v) + { + $key = $prefix ? $prefix . $separator . $k : $k; + + if (is_object($v) || is_array($v)) + { + $this->toFlatten($separator, $v, $array, $key); + continue; + } + + $array[$key] = $v; + } + } + + /** + * Determine if the given configuration option exists. + * + * @param string $key + * @return bool + */ + public function offsetExists($key) + { + return $this->has($key); + } + + /** + * Get a configuration option. + * + * @param string $key + * @return mixed + */ + public function offsetGet($key) + { + return $this->get($key); + } + + /** + * Set a configuration option. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function offsetSet($key, $value) + { + $this->set($key, $value); + } + + /** + * Unset a configuration option. + * + * @param string $key + * @return void + */ + public function offsetUnset($key) + { + $this->set($key, null); + } +} diff --git a/core/libraries/Hubzero/Config/Repository.php b/core/libraries/Hubzero/Config/Repository.php new file mode 100644 index 00000000000..dd6d576730a --- /dev/null +++ b/core/libraries/Hubzero/Config/Repository.php @@ -0,0 +1,154 @@ +loader = $loader; + $this->client = $client; + + $items = $this->load($this->client); + + parent::__construct($items); + } + + /** + * Load the configuration for a specified client. + * + * @param string $client + * @return void + */ + public function load($client) + { + return $this->loader->load($client); + } + + /** + * Get the loader implementation. + * + * @return object + */ + public function getLoader() + { + return $this->loader; + } + + /** + * Set the loader implementation. + * + * @param object $loader + * @return void + */ + public function setLoader($loader) + { + $this->loader = $loader; + } + + /** + * Set the current configuration client. + * + * @param string $client + * @return void + */ + public function setClient($client) + { + $this->client = (string) $client; + } + + /** + * Get the current configuration client. + * + * @return string + */ + public function getClient() + { + return $this->client; + } + + /** + * Get a registry value. + * + * @param string $path Registry path (e.g. config.cache.file) + * @param mixed $default Optional default value, returned if the internal value is null. + * @return mixed Value of entry or null + */ + public function get($path, $default = null) + { + // Return default value if path is empty + if (empty($path)) + { + return $default; + } + + if (strpos($path, $this->separator)) + { + return parent::get($path, $default); + } + + $nodes = get_object_vars($this->data); + $found = false; + + // Traverse the registry to find the correct node for the result. + foreach ($nodes as $n => $node) + { + if (is_array($node) && isset($node[$path])) + { + $value = $node[$path]; + $found = true; + continue; + } + + if (!isset($node->$path)) + { + continue; + } + + $value = $node->$path; + $found = true; + } + + if (!$found || $value === null || $value === '') + { + //return $default; + return parent::get($path, $default); + } + + return $value; + } +} diff --git a/core/libraries/Hubzero/Config/Tests/FileLoaderTest.php b/core/libraries/Hubzero/Config/Tests/FileLoaderTest.php new file mode 100644 index 00000000000..ea7319c2e32 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/FileLoaderTest.php @@ -0,0 +1,120 @@ + array( + 'application_env' => 'development', + 'editor' => 'ckeditor', + 'list_limit' => '25', + 'helpurl' => 'English (GB) - HUBzero help', + 'debug' => '1', + 'debug_lang' => '0', + 'sef' => '1', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'sef_groups' => '0', + 'feed_limit' => '10', + 'feed_email' => 'author' + ), + 'seo' => array( + 'sef' => '1', + 'sef_groups' => '0', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'unicodeslugs' => '0', + 'sitename_pagetitles' => '0' + ) + ); + + $path = __DIR__ . '/Files/Repository'; + + $loader = new FileLoader($path); + + $this->assertEquals($path, $loader->getDefaultPath()); + + $data = $loader->load(); + + $this->assertEquals($expected, $data); + + $expected['app']['application_env'] = 'production'; + $expected['app']['editor'] = 'none'; + $expected['app']['debug'] = '0'; + $expected['session'] = array( + 'cookie_domain' => '', + 'cookie_path' => '', + 'cookiesubdomains' => '0', + 'lifetime' => '45', + 'session_handler' => 'database' + ); + + $data = $loader->load('api'); + + $this->assertEquals($expected, $data); + + // Test with multiple paths + $path = array( + __DIR__ . '/Files/Repository', + __DIR__ . '/Files/Repository/api' + ); + + $loader = new FileLoader($path); + + $data = $loader->load(); + + $this->assertEquals($expected, $data); + + // Test with a bad path + $expected = array(); + $path = __DIR__ . '/Foo'; + + $loader = new FileLoader($path); + + $data = $loader->load(); + + $this->assertEquals($expected, $data); + + // Test loading a specific file + $loader = new FileLoader(__DIR__ . '/Files/Repository/seo.php'); + + $data = $loader->load(); + + $expected = array( + 'seo' => array( + 'sef' => '1', + 'sef_groups' => '0', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'unicodeslugs' => '0', + 'sitename_pagetitles' => '0' + ) + ); + + $this->assertEquals($expected, $data); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/Files/Legacy/Invalid/configuration.php b/core/libraries/Hubzero/Config/Tests/Files/Legacy/Invalid/configuration.php new file mode 100644 index 00000000000..5717155f134 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/Legacy/Invalid/configuration.php @@ -0,0 +1,10 @@ + '1440', 'limit' => '10000'); + var $short = array('period' => '1', 'limit' => '120'); + var $redis_password = 'warglebargle'; + var $sef = '1'; + var $sef_groups = '0'; + var $sef_rewrite = '1'; + var $sef_suffix = '0'; + var $sitename_pagetitles = '0'; + var $unicodeslugs = '0'; + var $cookie_domain = ''; + var $cookie_path = ''; + var $cookiesubdomains = '0'; + var $lifetime = '120'; + var $session_handler = 'database'; + var $solr_client_id = '12b910947122dfab5238b9e728774486'; + var $solr_client_secret = '6e291d7c6a9c8859104dd04332f5f07cbb30d6c0'; + var $solr_host = 'localhost'; + var $solr_password = 'drowssaprlos'; + var $solr_port = '2093'; + var $solr_username = 'hubzerosolrworker'; +} +// @codeCoverageIgnoreEnd diff --git a/core/libraries/Hubzero/Config/Tests/Files/Repository/api/app.php b/core/libraries/Hubzero/Config/Tests/Files/Repository/api/app.php new file mode 100644 index 00000000000..f6e84769f02 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/Repository/api/app.php @@ -0,0 +1,20 @@ + 'production', + 'editor' => 'none', + 'list_limit' => '25', + 'helpurl' => 'English (GB) - HUBzero help', + 'debug' => '0', + 'debug_lang' => '0', + 'sef' => '1', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'sef_groups' => '0', + 'feed_limit' => '10', + 'feed_email' => 'author' +); +// @codeCoverageIgnoreEnd diff --git a/core/libraries/Hubzero/Config/Tests/Files/Repository/api/session.php b/core/libraries/Hubzero/Config/Tests/Files/Repository/api/session.php new file mode 100644 index 00000000000..7875d0049a0 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/Repository/api/session.php @@ -0,0 +1,13 @@ + '', + 'cookie_path' => '', + 'cookiesubdomains' => '0', + 'lifetime' => '45', + 'session_handler' => 'database', +); +// @codeCoverageIgnoreEnd diff --git a/core/libraries/Hubzero/Config/Tests/Files/Repository/app.php b/core/libraries/Hubzero/Config/Tests/Files/Repository/app.php new file mode 100644 index 00000000000..82ace332543 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/Repository/app.php @@ -0,0 +1,20 @@ + 'development', + 'editor' => 'ckeditor', + 'list_limit' => '25', + 'helpurl' => 'English (GB) - HUBzero help', + 'debug' => '1', + 'debug_lang' => '0', + 'sef' => '1', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'sef_groups' => '0', + 'feed_limit' => '10', + 'feed_email' => 'author' +); +// @codeCoverageIgnoreEnd diff --git a/core/libraries/Hubzero/Config/Tests/Files/Repository/index.html b/core/libraries/Hubzero/Config/Tests/Files/Repository/index.html new file mode 100644 index 00000000000..399c491b2cc --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/Repository/index.html @@ -0,0 +1 @@ + diff --git a/core/libraries/Hubzero/Config/Tests/Files/Repository/seo.php b/core/libraries/Hubzero/Config/Tests/Files/Repository/seo.php new file mode 100644 index 00000000000..3eae6438a7b --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/Repository/seo.php @@ -0,0 +1,14 @@ + '1', + 'sef_groups' => '0', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'unicodeslugs' => '0', + 'sitename_pagetitles' => '0' +); +// @codeCoverageIgnoreEnd diff --git a/core/libraries/Hubzero/Config/Tests/Files/test.ini b/core/libraries/Hubzero/Config/Tests/Files/test.ini new file mode 100755 index 00000000000..4d1d680a001 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/test.ini @@ -0,0 +1,25 @@ +[app] +application_env="development" +editor="ckeditor" +list_limit=25 +helpurl="English (GB) - HUBzero help" +debug=1 +debug_lang=0 +sef=1 +sef_rewrite=1 +sef_suffix=0 +sef_groups=0 +feed_limit=10 +feed_email="author" +bad value +gzip=true +unicodeslugs=false +version=2.2 + +[seo] +sef=1 +sef_groups=0 +sef_rewrite=1 +sef_suffix=0 +unicodeslugs=0 +sitename_pagetitles=0 \ No newline at end of file diff --git a/core/libraries/Hubzero/Config/Tests/Files/test.json b/core/libraries/Hubzero/Config/Tests/Files/test.json new file mode 100755 index 00000000000..1fbf84df68b --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/test.json @@ -0,0 +1,24 @@ +{ + "app":{ + "application_env":"development", + "editor":"ckeditor", + "list_limit":25, + "helpurl":"English (GB) - HUBzero help", + "debug":1, + "debug_lang":0, + "sef":1, + "sef_rewrite":1, + "sef_suffix":0, + "sef_groups":0, + "feed_limit":10, + "feed_email":"author" + }, + "seo":{ + "sef":1, + "sef_groups":0, + "sef_rewrite":1, + "sef_suffix":0, + "unicodeslugs":0, + "sitename_pagetitles":0 + } +} \ No newline at end of file diff --git a/core/libraries/Hubzero/Config/Tests/Files/test.md b/core/libraries/Hubzero/Config/Tests/Files/test.md new file mode 100755 index 00000000000..17dd59a0d4a --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/test.md @@ -0,0 +1,25 @@ +# app +`application_env` is "development" +`editor` is "ckeditor" +`list_limit` is 25 +`helpurl` is "English (GB) - HUBzero help" +`debug` is 1 +`debug_lang` is 0 +`sef` is 1 +`sef_rewrite` is 1 +`sef_suffix` is 0 +`sef_groups` is 0 +`feed_limit` is 10 +`feed_email` is "author" +`bad` value +`gzip` is true +`unicodeslugs` is false +`version` is 2.2 + +# seo +`sef` is 1 +`sef_groups` is 0 +`sef_rewrite` is 1 +`sef_suffix` is 0 +`unicodeslugs` is 0 +`sitename_pagetitles` is 0 \ No newline at end of file diff --git a/core/libraries/Hubzero/Config/Tests/Files/test.php b/core/libraries/Hubzero/Config/Tests/Files/test.php new file mode 100644 index 00000000000..97a4aee6df5 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/test.php @@ -0,0 +1,30 @@ + array( + 'application_env' => 'development', + 'editor' => 'ckeditor', + 'list_limit' => '25', + 'helpurl' => 'English (GB) - HUBzero help', + 'debug' => '1', + 'debug_lang' => '0', + 'sef' => '1', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'sef_groups' => '0', + 'feed_limit' => '10', + 'feed_email' => 'author', + ), + 'seo' => array( + 'sef' => '1', + 'sef_groups' => '0', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'unicodeslugs' => '0', + 'sitename_pagetitles' => '0', + ), +); +// @codeCoverageIgnoreEnd diff --git a/core/libraries/Hubzero/Config/Tests/Files/test.xml b/core/libraries/Hubzero/Config/Tests/Files/test.xml new file mode 100755 index 00000000000..f018774a73c --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/test.xml @@ -0,0 +1,29 @@ + + + + development + ckeditor + 25 + English (GB) - HUBzero help + 1 + 0 + 1 + 1 + 0 + 0 + 10 + author + + 500.1 + 5000.7 + + + + 1 + 0 + 1 + 0 + 0 + 0 + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Config/Tests/Files/test.yaml b/core/libraries/Hubzero/Config/Tests/Files/test.yaml new file mode 100755 index 00000000000..e34c433c20a --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/test.yaml @@ -0,0 +1,20 @@ +app: + application_env: development + editor: ckeditor + list_limit: 25 + helpurl: 'English (GB) - HUBzero help' + debug: 1 + debug_lang: 0 + sef: 1 + sef_rewrite: 1 + sef_suffix: 0 + sef_groups: 0 + feed_limit: 10 + feed_email: author +seo: + sef: 1 + sef_groups: 0 + sef_rewrite: 1 + sef_suffix: 0 + unicodeslugs: 0 + sitename_pagetitles: 0 \ No newline at end of file diff --git a/core/libraries/Hubzero/Config/Tests/Files/testCallable.php b/core/libraries/Hubzero/Config/Tests/Files/testCallable.php new file mode 100644 index 00000000000..abd48191efd --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/testCallable.php @@ -0,0 +1,40 @@ + array( + 'application_env' => 'development', + 'editor' => 'ckeditor', + 'list_limit' => '25', + 'helpurl' => 'English (GB) - HUBzero help', + 'debug' => '1', + 'debug_lang' => '0', + 'sef' => '1', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'sef_groups' => '0', + 'feed_limit' => '10', + 'feed_email' => 'author', + ), + 'seo' => array( + 'sef' => '1', + 'sef_groups' => '0', + 'sef_rewrite' => '1', + 'sef_suffix' => '0', + 'unicodeslugs' => '0', + 'sitename_pagetitles' => '0', + ), + ); + + return $config; + } +} + +return array(new ConfigGetter(), 'getConfig'); +// @codeCoverageIgnoreEnd diff --git a/core/libraries/Hubzero/Config/Tests/Files/testEmpty.php b/core/libraries/Hubzero/Config/Tests/Files/testEmpty.php new file mode 100644 index 00000000000..e372982d320 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Files/testEmpty.php @@ -0,0 +1,7 @@ +setExpectedException('Hubzero\\Config\\Exception\\FileNotFoundException'); + + $loader->read($path . '/configuration.php'); + } + + /** + * Tests reading an invalid file + * + * @covers \Hubzero\Config\Legacy::read + * @return void + **/ + public function testReadErrorsWithInvalidFile() + { + $path = __DIR__ . '/Files'; + + $loader = new Legacy($path); + + $this->setExpectedException('Hubzero\\Config\\Exception\\UnsupportedFormatException'); + + $loader->read($path . '/Legacy/Invalid/configuration.php'); + } + + /** + * Tests constructor + * + * @covers \Hubzero\Config\Legacy::__construct + * @covers \Hubzero\Config\Legacy::exists + * @return void + **/ + public function testExists() + { + $path = __DIR__ . '/Files/Legacy'; + + $loader = new Legacy($path); + + $this->assertTrue($loader->exists()); + + $path = __DIR__ . '/Files/Repository'; + + $loader = new Legacy($path); + + $this->assertFalse($loader->exists()); + } + + /** + * Tests reading an invalid file + * + * @covers \Hubzero\Config\Legacy::read + * @return void + **/ + public function testRead() + { + if (!defined('PATH_ROOT')) + { + define('PATH_ROOT', '/var/www/hub'); + } + + if (!defined('PATH_APP')) + { + define('PATH_APP', PATH_ROOT . '/app'); + } + + $path = __DIR__ . '/Files'; + + $loader = new Legacy($path); + + $file = $loader->read($path . '/Legacy/configuration.php'); + + $this->assertInstanceOf('JConfig', $file); + $this->assertEquals(PATH_APP . '/tmp', $file->tmp_path); + $this->assertEquals(PATH_APP . '/logs', $file->log_path); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/Processor/IniTest.php b/core/libraries/Hubzero/Config/Tests/Processor/IniTest.php new file mode 100644 index 00000000000..ec15df9ece2 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Processor/IniTest.php @@ -0,0 +1,233 @@ +app = new stdClass(); + $data->app->application_env = "development"; + $data->app->editor = "ckeditor"; + $data->app->list_limit = 25; + $data->app->helpurl = "English (GB) - HUBzero help"; + $data->app->debug = 1; + $data->app->debug_lang = 0; + $data->app->sef = 1; + $data->app->sef_rewrite = 1; + $data->app->sef_suffix = 0; + $data->app->sef_groups = 0; + $data->app->feed_limit = 10; + $data->app->feed_email = "author"; + $data->app->gzip = true; + $data->app->unicodeslugs = false; + $data->app->version = 2.2; + + $data->seo = new stdClass(); + $data->seo->sef = 1; + $data->seo->sef_groups = 0; + $data->seo->sef_rewrite = 1; + $data->seo->sef_suffix = 0; + $data->seo->unicodeslugs = 0; + $data->seo->sitename_pagetitles = 0; + + $this->obj = $data; + $this->arr = array( + 'app' => (array)$data->app, + 'seo' => (array)$data->seo + ); + + $this->processor = new Ini(); + + parent::setUp(); + } + + /** + * Tests the getSupportedExtensions() method. + * + * @covers \Hubzero\Config\Processor\Ini::getSupportedExtensions + * @return void + **/ + public function testGetSupportedExtensions() + { + $extensions = $this->processor->getSupportedExtensions(); + + $this->assertTrue(is_array($extensions)); + $this->assertCount(1, $extensions); + $this->assertTrue(in_array('ini', $extensions)); + } + + /** + * Tests the canParse() method. + * + * @covers \Hubzero\Config\Processor\Ini::canParse + * @return void + **/ + public function testCanParse() + { + $this->assertFalse($this->processor->canParse('Cras justo odio, dapibus ac facilisis in, egestas eget quam.')); + $this->assertFalse($this->processor->canParse('{"application_env":"development","editor":"ck = editor","list_limit":"25"}')); + $this->assertFalse($this->processor->canParse('Cras justo odio dapibus ac facilisis in, egestas eget quam.')); + $this->assertTrue($this->processor->canParse($this->str)); + } + + /** + * Tests the parse() method. + * + * @covers \Hubzero\Config\Processor\Ini::parse + * @return void + **/ + public function testParse() + { + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.ini'); + + $this->assertEquals($this->arr, $result); + + $this->setExpectedException('Hubzero\Config\Exception\ParseException'); + + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.xml'); + } + + /** + * Tests the objectToString() method. + * + * @covers \Hubzero\Config\Processor\Ini::objectToString + * @covers \Hubzero\Config\Processor\Ini::getValueAsINI + * @return void + **/ + public function testObjectToString() + { + // Test that a string is returned as-is + $result = $this->processor->objectToString($this->str); + + $this->assertEquals($this->str, $result); + + // Test object to string conversion + $result = $this->processor->objectToString($this->obj); + + $this->assertEquals($this->str, $result); + } + + /** + * Tests the stringToObject() method. + * + * @covers \Hubzero\Config\Processor\Ini::stringToObject + * @return void + **/ + public function testStringToObject() + { + // Test that an object is returned as-is + $result = $this->processor->stringToObject($this->obj, array('processSections' => true)); + + $this->assertEquals($this->obj, $result); + + // Test that an empty string returns an empty stdClass object + $result = $this->processor->stringToObject('', array('processSections' => true)); + + $this->assertEquals(new stdClass, $result); + + // Test that a string gets converted as expected + $result = $this->processor->stringToObject($this->str, array('processSections' => true)); + + $this->assertEquals($this->obj, $result); + + // Test that a string gets converted as expected + $result = $this->processor->stringToObject($this->str, array('processSections' => false)); + + $data = new stdClass(); + $data->application_env = "development"; + $data->editor = "ckeditor"; + $data->list_limit = 25; + $data->helpurl = "English (GB) - HUBzero help"; + $data->debug = 1; + $data->debug_lang = 0; + $data->sef = 1; + $data->sef_rewrite = 1; + $data->sef_suffix = 0; + $data->sef_groups = 0; + $data->feed_limit = 10; + $data->feed_email = "author"; + $data->gzip = true; + $data->unicodeslugs = false; + $data->version = 2.2; + $data->sef = 1; + $data->sef_groups = 0; + $data->sef_rewrite = 1; + $data->sef_suffix = 0; + $data->unicodeslugs = 0; + $data->sitename_pagetitles = 0; + + $this->assertEquals($data, $result); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/Processor/JsonTest.php b/core/libraries/Hubzero/Config/Tests/Processor/JsonTest.php new file mode 100644 index 00000000000..86aacbd8441 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Processor/JsonTest.php @@ -0,0 +1,195 @@ +app = new stdClass(); + $data->app->application_env = "development"; + $data->app->editor = "ckeditor"; + $data->app->list_limit = 25; + $data->app->helpurl = "English (GB) - HUBzero help"; + $data->app->debug = 1; + $data->app->debug_lang = 0; + $data->app->sef = 1; + $data->app->sef_rewrite = 1; + $data->app->sef_suffix = 0; + $data->app->sef_groups = 0; + $data->app->feed_limit = 10; + $data->app->feed_email = "author"; + + $data->seo = new stdClass(); + $data->seo->sef = 1; + $data->seo->sef_groups = 0; + $data->seo->sef_rewrite = 1; + $data->seo->sef_suffix = 0; + $data->seo->unicodeslugs = 0; + $data->seo->sitename_pagetitles = 0; + + $this->obj = $data; + $this->arr = array( + 'app' => (array)$data->app, + 'seo' => (array)$data->seo + ); + + $this->processor = new Json(); + + parent::setUp(); + } + + /** + * Tests the getSupportedExtensions() method. + * + * @covers \Hubzero\Config\Processor\Json::getSupportedExtensions + * @return void + **/ + public function testGetSupportedExtensions() + { + $extensions = $this->processor->getSupportedExtensions(); + + $this->assertTrue(is_array($extensions)); + $this->assertCount(1, $extensions); + $this->assertTrue(in_array('json', $extensions)); + } + + /** + * Tests the canParse() method. + * + * @covers \Hubzero\Config\Processor\Json::canParse + * @return void + **/ + public function testCanParse() + { + $this->assertFalse($this->processor->canParse('Cras justo odio, dapibus ac facilisis in, egestas eget quam.')); + $this->assertFalse($this->processor->canParse('development')); + $this->assertFalse($this->processor->canParse('{Cras justo odio, dapibus ac facilisis in, egestas eget quam.}')); + $this->assertTrue($this->processor->canParse($this->str)); + } + + /** + * Tests the parse() method. + * + * @covers \Hubzero\Config\Processor\Json::parse + * @return void + **/ + public function testParse() + { + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.json'); + + $this->assertEquals($this->arr, $result); + + $this->setExpectedException('Hubzero\Config\Exception\ParseException'); + + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.xml'); + } + + /** + * Tests the objectToString() method. + * + * @covers \Hubzero\Config\Processor\Json::objectToString + * @return void + **/ + public function testObjectToString() + { + // Test that a string is returned as-is + $result = $this->processor->objectToString($this->str); + + $this->assertEquals($this->str, $result); + + // Test object to string conversion + $result = $this->processor->objectToString($this->obj); + + $this->assertEquals($this->str, $result); + } + + /** + * Tests the stringToObject() method. + * + * @covers \Hubzero\Config\Processor\Json::stringToObject + * @return void + **/ + public function testStringToObject() + { + $result = $this->processor->stringToObject($this->obj, true); + + $this->assertEquals($this->obj, $result); + + $result = $this->processor->stringToObject($this->str, true); + + $this->assertEquals($this->obj, $result); + + $result = $this->processor->stringToObject(' +[app] +application_env="development" +editor="ckeditor" +list_limit=25 +helpurl="English (GB) - HUBzero help" +debug=1 +debug_lang=0 +sef=1 +sef_rewrite=1 +sef_suffix=0 +sef_groups=0 +feed_limit=10 +feed_email="author" + +[seo] +sef=1 +sef_groups=0 +sef_rewrite=1 +sef_suffix=0 +unicodeslugs=0 +sitename_pagetitles=0', true); + + $this->assertEquals($this->obj, $result); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/Processor/PhpTest.php b/core/libraries/Hubzero/Config/Tests/Processor/PhpTest.php new file mode 100644 index 00000000000..b0ddeb2b514 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Processor/PhpTest.php @@ -0,0 +1,231 @@ + \'1\', + \'bar\' => \'\', + \'app\' => array("application_env" => "development", "editor" => "ckeditor", "list_limit" => "25", "helpurl" => "English (GB) - HUBzero help", "debug" => "1", "debug_lang" => "0", "sef" => "1", "sef_rewrite" => "1", "sef_suffix" => "0", "sef_groups" => "0", "feed_limit" => "10", "feed_email" => "author"), + \'seo\' => array("sef" => "1", "sef_groups" => "0", "sef_rewrite" => "1", "sef_suffix" => "0", "unicodeslugs" => "0", "sitename_pagetitles" => "0"), +);'; + + /** + * Expected data as a string + * + * @var string + */ + private $strObject = ' "development", "editor" => "ckeditor", "list_limit" => "25", "helpurl" => "English (GB) - HUBzero help", "debug" => "1", "debug_lang" => "0", "sef" => "1", "sef_rewrite" => "1", "sef_suffix" => "0", "sef_groups" => "0", "feed_limit" => "10", "feed_email" => "author"); + var $seo = array("sef" => "1", "sef_groups" => "0", "sef_rewrite" => "1", "sef_suffix" => "0", "unicodeslugs" => "0", "sitename_pagetitles" => "0"); +}'; + + /** + * Test setup + * + * @return void + */ + protected function setUp() + { + $data = new stdClass(); + + $data->foo = 1; + $data->bar = null; + + $data->app = new stdClass(); + $data->app->application_env = "development"; + $data->app->editor = "ckeditor"; + $data->app->list_limit = 25; + $data->app->helpurl = "English (GB) - HUBzero help"; + $data->app->debug = 1; + $data->app->debug_lang = 0; + $data->app->sef = 1; + $data->app->sef_rewrite = 1; + $data->app->sef_suffix = 0; + $data->app->sef_groups = 0; + $data->app->feed_limit = 10; + $data->app->feed_email = "author"; + + $data->seo = new stdClass(); + $data->seo->sef = 1; + $data->seo->sef_groups = 0; + $data->seo->sef_rewrite = 1; + $data->seo->sef_suffix = 0; + $data->seo->unicodeslugs = 0; + $data->seo->sitename_pagetitles = 0; + + $this->obj = $data; + $this->arr = array( + //'foo' => '1', + //'bar' => '', + 'app' => (array)$data->app, + 'seo' => (array)$data->seo + ); + + $this->processor = new Php(); + + parent::setUp(); + } + + /** + * Tests the getSupportedExtensions() method. + * + * @covers \Hubzero\Config\Processor\Php::getSupportedExtensions + * @return void + **/ + public function testGetSupportedExtensions() + { + $extensions = $this->processor->getSupportedExtensions(); + + $this->assertTrue(is_array($extensions)); + $this->assertCount(1, $extensions); + $this->assertTrue(in_array('php', $extensions)); + } + + /** + * Tests the canParse() method. + * + * @covers \Hubzero\Config\Processor\Php::canParse + * @return void + **/ + public function testCanParse() + { + $this->assertFalse($this->processor->canParse($this->str)); + } + + /** + * Tests the parse() method. + * + * @covers \Hubzero\Config\Processor\Php::parse + * @return void + **/ + public function testParse() + { + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.php'); + $this->assertEquals($this->arr, $result); + } + + /** + * Test a PHP file containing a callable + * + * @covers \Hubzero\Config\Processor\Php::parse + * @return void + **/ + public function testParseCallable() + { + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'testCallable.php'); + $this->assertEquals($this->arr, $result); + } + + /** + * Test that an exception is thrown and caught + * + * @covers \Hubzero\Config\Processor\Php::parse + * @return void + **/ + public function testParseException() + { + $this->setExpectedException(ParseException::class); + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'testException.php'); + } + + /** + * Tests the parse() method throws an Exception for a bad PHP file. + * + * @covers \Hubzero\Config\Processor\Php::parse + * @return void + **/ + public function testParseEmptyFile() + { + $this->setExpectedException(UnsupportedFormatException::class); + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'testEmpty.php'); + } + + /** + * Tests the objectToString() method. + * + * @covers \Hubzero\Config\Processor\Php::objectToString + * @covers \Hubzero\Config\Processor\Php::getArrayString + * @return void + **/ + public function testObjectToString() + { + // Test that a string is returned as-is + $result = $this->processor->objectToString($this->str); + + $this->assertEquals($this->str, $result); + + // Test object to string conversion + $result = $this->processor->objectToString($this->obj, array( + 'format' => 'array' + )); + + $this->assertEquals($this->str, $result); + + // Test object to string conversion + $result = $this->processor->objectToString($this->obj, array( + 'format' => 'object' + )); + + $this->assertEquals($this->strObject, $result); + } + + /** + * Tests the stringToObject() method. + * + * @covers \Hubzero\Config\Processor\Php::stringToObject + * @return void + **/ + public function testStringToObject() + { + $result = $this->processor->stringToObject($this->str); + + $this->assertTrue($result); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/Processor/XmlTest.php b/core/libraries/Hubzero/Config/Tests/Processor/XmlTest.php new file mode 100644 index 00000000000..4650e6f51e1 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Processor/XmlTest.php @@ -0,0 +1,211 @@ + + + + development + ckeditor + 25 + English (GB) - HUBzero help + 1 + 0 + 1 + 1 + 0 + 0 + 10 + author + + 500.1 + 5000.7 + + + + 1 + 0 + 1 + 0 + 0 + 0 + +'; + + /** + * Test setup + * + * @return void + */ + protected function setUp() + { + $data = new stdClass(); + + $data->app = new stdClass(); + $data->app->application_env = "development"; + $data->app->editor = "ckeditor"; + $data->app->list_limit = 25; + $data->app->helpurl = "English (GB) - HUBzero help"; + $data->app->debug = 1; + $data->app->debug_lang = 0; + $data->app->sef = 1; + $data->app->sef_rewrite = 1; + $data->app->sef_suffix = 0; + $data->app->sef_groups = 0; + $data->app->feed_limit = 10; + $data->app->feed_email = "author"; + $data->app->ratelimit = array( + 'short' => 500.1, + 'long' => 5000.7 + ); + + $data->seo = new stdClass(); + $data->seo->sef = 1; + $data->seo->sef_groups = 0; + $data->seo->sef_rewrite = 1; + $data->seo->sef_suffix = 0; + $data->seo->unicodeslugs = 0; + $data->seo->sitename_pagetitles = 0; + + $this->obj = $data; + $this->arr = array( + 'app' => (array)$data->app, + 'seo' => (array)$data->seo + ); + + $this->processor = new Xml(); + + parent::setUp(); + } + + /** + * Tests the getSupportedExtensions() method. + * + * @covers \Hubzero\Config\Processor\Xml::getSupportedExtensions + * @return void + **/ + public function testGetSupportedExtensions() + { + $extensions = $this->processor->getSupportedExtensions(); + + $this->assertTrue(is_array($extensions)); + $this->assertCount(1, $extensions); + $this->assertTrue(in_array('xml', $extensions)); + } + + /** + * Tests the canParse() method. + * + * @covers \Hubzero\Config\Processor\Xml::canParse + * @return void + **/ + public function testCanParse() + { + $this->assertFalse($this->processor->canParse('Cras justo odio, dapibus ac facilisis in, egestas eget quam.')); + $this->assertFalse($this->processor->canParse('{"application_env":"development","editor":"ckeditor","list_limit":"25"}')); + $this->assertTrue($this->processor->canParse($this->str)); + } + + /** + * Tests the parse() method. + * + * @covers \Hubzero\Config\Processor\Xml::parse + * @return void + **/ + public function testParse() + { + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.xml'); + + $this->assertEquals($this->arr, $result); + + $this->setExpectedException('Hubzero\Config\Exception\ParseException'); + + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.ini'); + } + + /** + * Tests the objectToString() method. + * + * @covers \Hubzero\Config\Processor\Xml::objectToString + * @covers \Hubzero\Config\Processor\Xml::getXmlChildren + * @return void + **/ + public function testObjectToString() + { + // Test that a string is returned as-is + $result = $this->processor->objectToString($this->str); + + $this->assertEquals($this->str, $result); + + // Test object to string conversion + $result = $this->processor->objectToString($this->obj, array( + 'name' => 'config', + 'nodeName' => 'setting' + )); + + $str = str_replace(array("\n", "\t"), '', $this->str); + $str = str_replace('', "\n", $str); + + $this->assertEquals($str, trim($result)); + } + + /** + * Tests the stringToObject() method. + * + * @covers \Hubzero\Config\Processor\Xml::stringToObject + * @covers \Hubzero\Config\Processor\Xml::getValueFromNode + * @return void + **/ + public function testStringToObject() + { + // Test that an object is returned as-is + $result = $this->processor->stringToObject($this->obj); + + $this->assertEquals($this->obj, $result); + + // Test that a string gets converted as expected + $result = $this->processor->stringToObject($this->str); + + $this->assertEquals($this->obj, $result); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/Processor/YamlTest.php b/core/libraries/Hubzero/Config/Tests/Processor/YamlTest.php new file mode 100644 index 00000000000..1e46ef481f2 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/Processor/YamlTest.php @@ -0,0 +1,191 @@ +app = new stdClass(); + $data->app->application_env = "development"; + $data->app->editor = "ckeditor"; + $data->app->list_limit = 25; + $data->app->helpurl = "English (GB) - HUBzero help"; + $data->app->debug = 1; + $data->app->debug_lang = 0; + $data->app->sef = 1; + $data->app->sef_rewrite = 1; + $data->app->sef_suffix = 0; + $data->app->sef_groups = 0; + $data->app->feed_limit = 10; + $data->app->feed_email = "author"; + + $data->seo = new stdClass(); + $data->seo->sef = 1; + $data->seo->sef_groups = 0; + $data->seo->sef_rewrite = 1; + $data->seo->sef_suffix = 0; + $data->seo->unicodeslugs = 0; + $data->seo->sitename_pagetitles = 0; + + $this->obj = $data; + $this->arr = array( + 'app' => (array)$data->app, + 'seo' => (array)$data->seo + ); + + $this->processor = new Yaml(); + + parent::setUp(); + } + + /** + * Tests the getSupportedExtensions() method. + * + * @covers \Hubzero\Config\Processor\Yaml::getSupportedExtensions + * @return void + **/ + public function testGetSupportedExtensions() + { + $extensions = $this->processor->getSupportedExtensions(); + + $this->assertTrue(is_array($extensions)); + $this->assertCount(2, $extensions); + $this->assertTrue(in_array('yml', $extensions)); + $this->assertTrue(in_array('yaml', $extensions)); + } + + /** + * Tests the canParse() method. + * + * @covers \Hubzero\Config\Processor\Yaml::canParse + * @return void + **/ + public function testCanParse() + { + $this->assertFalse($this->processor->canParse("foo:\n bar")); + $this->assertTrue($this->processor->canParse($this->str)); + } + + /** + * Tests the parse() method. + * + * @covers \Hubzero\Config\Processor\Yaml::parse + * @return void + **/ + public function testParse() + { + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.yaml'); + + $this->assertEquals($this->arr, $result); + + $this->setExpectedException('Hubzero\Config\Exception\ParseException'); + + $result = $this->processor->parse(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Files' . DIRECTORY_SEPARATOR . 'test.xml'); + } + + /** + * Tests the objectToString() method. + * + * @covers \Hubzero\Config\Processor\Yaml::objectToString + * @covers \Hubzero\Config\Processor\Yaml::asArray + * @return void + **/ + public function testObjectToString() + { + // Test that a string is returned as-is + $result = $this->processor->objectToString($this->str); + + $this->assertEquals($this->str, $result); + + // Test object to string conversion + $result = $this->processor->objectToString($this->obj); + + $this->assertEquals($this->str, $result); + } + + /** + * Tests the stringToObject() method. + * + * @covers \Hubzero\Config\Processor\Yaml::stringToObject + * @covers \Hubzero\Config\Processor\Yaml::toObject + * @return void + **/ + public function testStringToObject() + { + // Test that an object is returned as-is + $result = $this->processor->stringToObject($this->obj); + + $this->assertEquals($this->obj, $result); + + // Test that a string gets converted as expected + $result = $this->processor->stringToObject($this->str); + + $this->assertEquals($this->obj, $result); + + // Test that an unparsable string throws an exception + $this->setExpectedException('Hubzero\Config\Exception\ParseException'); + + $result = $this->processor->stringToObject("foo:\n bar"); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/ProcessorTest.php b/core/libraries/Hubzero/Config/Tests/ProcessorTest.php new file mode 100644 index 00000000000..d9382074b05 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/ProcessorTest.php @@ -0,0 +1,88 @@ +assertCount(5, $instances); + + foreach ($instances as $instance) + { + $this->assertInstanceOf(Processor::class, $instance); + } + } + + /** + * Tests the instance() method + * + * @covers \Hubzero\Config\Processor::instance + * @return void + **/ + public function testInstance() + { + foreach (array('ini', 'yaml', 'json', 'php', 'xml') as $type) + { + $result = Processor::instance($type); + + $this->assertInstanceOf(Processor::class, $result); + } + + $this->setExpectedException('Hubzero\\Error\\Exception\\InvalidArgumentException'); + + $result = Processor::instance('py'); + } + + /** + * Tests getSupportedExtensions() + * + * @covers \Hubzero\Config\Processor::getSupportedExtensions + * @return void + **/ + public function testGetSupportedExtensions() + { + $stub = $this->getMockForAbstractClass('Hubzero\Config\Processor'); + $stub->expects($this->any()) + ->method('getSupportedExtensions') + ->will($this->returnValue(array())); + + $this->assertEquals(array(), $stub->getSupportedExtensions()); + } + + /** + * Tests parse() + * + * @covers \Hubzero\Config\Processor::parse + * @return void + **/ + public function testParse() + { + $stub = $this->getMockForAbstractClass('Hubzero\Config\Processor'); + $stub->expects($this->any()) + ->method('parse') + ->with($this->isType('string')) + ->will($this->returnValue(array())); + + $this->assertEquals(array(), $stub->parse(__DIR__ . '/Tests/Files/test.json')); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/RegistryTest.php b/core/libraries/Hubzero/Config/Tests/RegistryTest.php new file mode 100644 index 00000000000..b7cdff0bde6 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/RegistryTest.php @@ -0,0 +1,559 @@ +assertEquals($data->get('foo'), null); + $this->assertEquals($data->get('foo', 'one'), 'one'); + $this->assertEquals($data->get('lorem.ipsum.dolor', 'baz'), 'baz'); + + // Test correct value is returned + $data->set('foo', 'bar'); + + $this->assertEquals($data->get('foo'), 'bar'); + + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $this->assertEquals($data->get('lorem.ipsum'), 'sham'); + + $data['foo'] = 'lorem'; + + $this->assertEquals($data->get('', 'lorem'), 'lorem'); + $this->assertEquals($data->get('foo'), 'lorem'); + $this->assertEquals($data['foo'], 'lorem'); + $this->assertEquals($data->get('fake.path', 'lorem'), 'lorem'); + + $data['lorem.ipsum'] = 'ipsum'; + + $this->assertEquals($data->get('lorem.ipsum'), 'ipsum'); + $this->assertEquals($data['lorem.ipsum'], 'ipsum'); + $this->assertEquals($data->get('lorem.dolor', 'mit'), 'mit'); + + $data['lorem'] = array('ipsum' => 'dolor'); + + $this->assertEquals($data->get('lorem.ipsum'), 'dolor'); + + $data->set('lorem.ipsum', array('dolor' => 'mit')); + + $this->assertEquals($data->get('lorem.ipsum.dolor'), 'mit'); + + $data->set('lorem', array('ipsum' => 'dolor')); + $data->set('lorem.dolor.foo', 'bar'); + + $this->assertEquals($data->get('lorem.dolor.foo'), 'bar'); + + $data = new Registry(); + $data->set('dinosaur', new stdClass); + $data->set('dinosaur.therapod.tyrannosaurid', 'rex'); + $data->set('dinosaur.therapod.raptor', ''); + + $this->assertInstanceOf('stdClass', $data->get('dinosaur.therapod')); + $this->assertEquals($data->get('dinosaur.therapod.tyrannosaurid'), 'rex'); + $this->assertEquals($data->get('dinosaur.therapod.raptor', 'velociraptor'), 'velociraptor'); + } + + /** + * Tests the has() method + * + * @covers \Hubzero\Config\Registry::has + * @covers \Hubzero\Config\Registry::offsetExists + * @return void + **/ + public function testHas() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + + $this->assertTrue($data->has('foo')); + $this->assertFalse($data->has('bar')); + + $this->assertTrue(isset($data['foo'])); + $this->assertFalse(isset($data['bar'])); + } + + /** + * Tests the def() method + * + * @covers \Hubzero\Config\Registry::def + * @return void + **/ + public function testDef() + { + $data = new Registry(); + + $data->def('foo', 'bar'); + + $this->assertEquals($data->get('foo'), 'bar'); + + $data->set('bar', 'foo'); + $data->def('bar', 'oop'); + + $this->assertEquals($data->get('bar'), 'foo'); + } + + /** + * Tests the reset() method + * + * @covers \Hubzero\Config\Registry::reset + * @return void + **/ + public function testReset() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + + $data->reset(); + + $this->assertFalse($data->has('foo')); + $this->assertFalse($data->has('bar')); + } + + /** + * Tests the offsetUnset() method + * + * @covers \Hubzero\Config\Registry::offsetUnset + * @return void + **/ + public function testOffsetUnset() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + + unset($data['foo']); + + $this->assertFalse($data->has('foo')); + } + + /** + * Tests the count() method + * + * @covers \Hubzero\Config\Registry::count + * @return void + **/ + public function testCount() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + + $this->assertEquals($data->count(), 2); + + $data->set('lorem', 'ipsum'); + + $this->assertEquals($data->count(), 3); + + $data->reset(); + + $this->assertEquals($data->count(), 0); + } + + /** + * Tests the toString() method + * + * @covers \Hubzero\Config\Registry::toString + * @covers \Hubzero\Config\Registry::__toString + * @return void + **/ + public function testToString() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $str = $data->toString(); + + $this->assertEquals($str, '{"foo":"bar","bar":"foo","lorem":{"ipsum":"sham"}}'); + + $str = (string)$data; + + $this->assertEquals($str, '{"foo":"bar","bar":"foo","lorem":{"ipsum":"sham"}}'); + } + + /** + * Tests the toObject() method + * + * @covers \Hubzero\Config\Registry::toObject + * @return void + **/ + public function testToObject() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $obj = $data->toObject(); + + $this->assertInstanceOf('stdClass', $obj); + $this->assertTrue(isset($obj->bar)); + $this->assertEquals($obj->foo, 'bar'); + $this->assertTrue(isset($obj->lorem->ipsum)); + } + + /** + * Tests the toArray() method + * + * @covers \Hubzero\Config\Registry::toArray + * @covers \Hubzero\Config\Registry::asArray + * @return void + **/ + public function testToArray() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $arr = $data->toArray(); + + $this->assertTrue(is_array($arr)); + $this->assertTrue(isset($arr['bar'])); + $this->assertTrue(isset($arr['lorem']['ipsum'])); + $this->assertEquals($arr['lorem']['ipsum'], 'sham'); + } + + /** + * Tests the flatten() method + * + * @covers \Hubzero\Config\Registry::flatten + * @covers \Hubzero\Config\Registry::toFlatten + * @return void + **/ + public function testFlatten() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $arr = $data->flatten(); + + $this->assertTrue(is_array($arr)); + $this->assertTrue(isset($arr['bar'])); + $this->assertTrue(isset($arr['lorem.ipsum'])); + $this->assertEquals($arr['lorem.ipsum'], 'sham'); + } + + /** + * Tests the jsonSerialize() method + * + * @covers \Hubzero\Config\Registry::jsonSerialize + * @return void + **/ + public function testJsonSerialize() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $result = $data->jsonSerialize(); + + $this->assertInstanceOf('stdClass', $result); + + $result = json_encode($data); + + $this->assertEquals($result, '{"foo":"bar","bar":"foo","lorem":{"ipsum":"sham"}}'); + } + + /** + * Tests the getIterator() method + * + * @covers \Hubzero\Config\Registry::getIterator + * @return void + **/ + public function testGetIterator() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + + $result = $data->getIterator(); + + $this->assertInstanceOf('ArrayIterator', $result); + } + + /** + * Tests the processors() method + * + * @covers \Hubzero\Config\Registry::processors + * @return void + **/ + public function testProcessors() + { + $data = new Registry(); + + $results = $data->processors(); + + $this->assertTrue(is_array($results)); + $this->assertTrue(count($results) > 0); + + foreach ($results as $result) + { + $this->assertInstanceOf(Processor::class, $result); + } + } + + /** + * Tests the processor() method + * + * @covers \Hubzero\Config\Registry::processor + * @return void + **/ + public function testProcessor() + { + $data = new Registry(); + + foreach (array('ini', 'yaml', 'json', 'php', 'xml') as $type) + { + $result = $data->processor($type); + + $this->assertInstanceOf(Processor::class, $result); + + $supported = $result->getSupportedExtensions(); + + $this->assertTrue(in_array($type, $supported)); + + $this->assertInstanceOf('\\Hubzero\\Config\\Processor\\' . ucfirst($type), $result); + } + } + + /** + * Tests the extract() method + * + * @covers \Hubzero\Config\Registry::extract + * @return void + **/ + public function testExtract() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $extracted = $data->extract('lorem'); + + $this->assertInstanceOf(Registry::class, $extracted); + + $this->assertTrue(isset($extracted['ipsum'])); + $this->assertEquals($extracted['ipsum'], 'sham'); + + $extracted = $data->extract('dolor'); + + $this->assertEquals($extracted, null); + } + + /** + * Tests the merge() method + * + * @covers \Hubzero\Config\Registry::merge + * @covers \Hubzero\Config\Registry::bind + * @return void + **/ + public function testMerge() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $data2 = new Registry(); + $data2->set('bar', 'newfoo'); + $data2->set('lorem', 'dolor'); + + $fake = null; + $result = $data->merge($fake); + + $this->assertFalse($result); + + $result = $data->merge($data2); + + $this->assertTrue($result); + $this->assertEquals($data->get('bar'), 'newfoo'); + $this->assertEquals($data->get('lorem'), 'dolor'); + + $data3 = array( + 'lorem' => array('ipsum' => 'mit'), + 'cullen' => 'didae' + ); + + $result = $data->merge($data3, true); + + $this->assertTrue($result); + $this->assertEquals($data->get('lorem.ipsum'), 'mit'); + $this->assertTrue($data->has('cullen')); + + // Test that empty values are discarded + $data = new Registry(); + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + + $data2 = new Registry(); + $data2->set('bar', 'newfoo'); + $data2->set('lorem', ''); + + $result = $data->merge($data2); + + $this->assertTrue($result); + $this->assertEquals($data->get('lorem'), null); + } + + /** + * Tests the parse() method + * + * @covers \Hubzero\Config\Registry::__construct + * @covers \Hubzero\Config\Registry::parse + * @covers \Hubzero\Config\Registry::read + * @return void + **/ + public function testParse() + { + // Parse from a string + $data = new Registry(); + + // `toObject()` returns the `$data` property which is set in the constructor + $this->assertTrue(is_object($data->toObject())); + $this->assertInstanceOf('stdClass', $data->toObject()); + + $json = '{"one":"bar","bar":"foo","lorem":{"ipsum":"sham"}}'; + + $data->set('one', 'blue'); + $data->set('two', 'shoe'); + + $data->parse($json); + + $this->assertEquals($data->get('one'), 'bar'); + $this->assertEquals($data->get('lorem.ipsum'), 'sham'); + + // Parse from an array + $data = new Registry(); + + $arr = array('one' => 'bar', 'bar' => 'foo', 'lorem' => array('ipsum' => 'sham')); + + $data->set('one', 'blue'); + $data->set('two', 'shoe'); + + $data->parse($arr); + + $this->assertEquals($data->get('one'), 'bar'); + $this->assertEquals($data->get('lorem.ipsum'), 'sham'); + + // Test parsing from a file + $data = new Registry(); + $result = $data->parse(__DIR__ . '/Files/test.json'); + + $this->assertTrue($result); + $this->assertEquals($data->get('app.application_env'), 'development'); + + // Try parsing from an unsupported format + $data = new Registry(); + $result = $data->parse(__DIR__ . '/Files/test.md'); + + $this->assertFalse($result); + + // Test parsing from constructor + $arr = array('one' => 'bar', 'bar' => 'foo', 'lorem' => array('ipsum' => 'sham')); + + $data = new Registry($arr); + + $this->assertEquals($data->get('one'), 'bar'); + $this->assertEquals($data->get('lorem.ipsum'), 'sham'); + + $json = '{"three":"jelly","four":"jam","hair":{"head":"eyebrows"}}'; + + $data = new Registry($json); + + $this->assertEquals($data->get('three'), 'jelly'); + $this->assertEquals($data->get('hair.head'), 'eyebrows'); + + // Try parsing from an unsupported format + $data = new Registry(); + $result = $data->parse(__DIR__ . '/Files/test.md'); + + $this->assertFalse($result); + + // Try reading a nonexistant file + $data = new Registry(); + + $this->setExpectedException('Hubzero\\Error\\Exception\\InvalidArgumentException'); + + $data->read(__DIR__ . '/Fles/test.md'); + } + + /** + * Tests the __clone() method + * + * @covers \Hubzero\Config\Registry::__clone + * @return void + **/ + public function testClone() + { + $data = new Registry(); + + $data->set('foo', 'bar'); + $data->set('bar', 'foo'); + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $expected = $data->toString(); + + $evilclone = clone $data; + + $this->assertInstanceOf(Registry::class, $evilclone); + + $this->assertTrue(isset($evilclone['lorem'])); + $this->assertEquals($evilclone->get('lorem.ipsum'), 'sham'); + + $this->assertEquals($evilclone->toString(), $expected); + } +} diff --git a/core/libraries/Hubzero/Config/Tests/RepositoryTest.php b/core/libraries/Hubzero/Config/Tests/RepositoryTest.php new file mode 100644 index 00000000000..79e84ecaac6 --- /dev/null +++ b/core/libraries/Hubzero/Config/Tests/RepositoryTest.php @@ -0,0 +1,122 @@ +assertEquals($data->getClient(), 'site'); + + $data->setClient('api'); + + $this->assertEquals($data->getClient(), 'api'); + + // Test that a default loader was set + $this->assertInstanceOf('Hubzero\Config\FileLoader', $data->getLoader()); + + // Test setting a loader + $path = __DIR__ . '/Files/Repository'; + $loader = new FileLoader($path); + + // Set by method + $data->setLoader($loader); + + $this->assertInstanceOf('Hubzero\Config\FileLoader', $data->getLoader()); + $this->assertEquals($path, $data->getLoader()->getDefaultPath()); + + // Set by constructor + $data = new Repository('files', $loader); + + $this->assertInstanceOf('Hubzero\Config\FileLoader', $data->getLoader()); + $this->assertEquals($path, $data->getLoader()->getDefaultPath()); + } + + /** + * Tests get() + * + * @covers \Hubzero\Config\Repository::load + * @covers \Hubzero\Config\Repository::get + * @return void + **/ + public function testSetAndGet() + { + $loader = new FileLoader(__DIR__ . '/Files/Repository'); + + $data = new Repository('site', $loader); + + // Test that default value is returned + $this->assertEquals($data->get('foo'), null); + $this->assertEquals($data->get('foo', 'one'), 'one'); + $this->assertEquals($data->get('lorem.ipsum.dolor', 'baz'), 'baz'); + $this->assertEquals($data->get('app.application_env'), 'development'); + $this->assertEquals($data->get('application_env'), 'development'); + + $loader = new FileLoader(__DIR__ . '/Files/Repository'); + + $data = new Repository('api', $loader); + $this->assertEquals($data->get('app.application_env'), 'production'); + + // Test correct value is returned + $data->set('foo', 'bar'); + + $this->assertEquals($data->get('foo'), 'bar'); + + $data->set('lorem', new stdClass); + $data->set('lorem.ipsum', 'sham'); + + $this->assertEquals($data->get('lorem.ipsum'), 'sham'); + + $data['foo'] = 'lorem'; + + $this->assertEquals($data->get('', 'lorem'), 'lorem'); + $this->assertEquals($data->get('foo'), 'lorem'); + $this->assertEquals($data['foo'], 'lorem'); + $this->assertEquals($data->get('fake.path', 'lorem'), 'lorem'); + + $data['lorem.ipsum'] = 'ipsum'; + + $this->assertEquals($data->get('lorem.ipsum'), 'ipsum'); + $this->assertEquals($data['lorem.ipsum'], 'ipsum'); + $this->assertEquals($data->get('lorem.dolor', 'mit'), 'mit'); + + $data['lorem'] = array('ipsum' => 'dolor'); + + $this->assertEquals($data->get('ipsum'), 'dolor'); + + $data->set('lorem.ipsum', array('dolor' => 'mit')); + + $this->assertEquals($data->get('lorem.ipsum.dolor'), 'mit'); + + $data->set('lorem', array('ipsum' => 'dolor')); + $data->set('lorem.dolor.foo', 'bar'); + + $this->assertEquals($data->get('lorem.dolor.foo'), 'bar'); + } +} diff --git a/core/libraries/Hubzero/Console/Arguments.php b/core/libraries/Hubzero/Console/Arguments.php new file mode 100644 index 00000000000..ecbba977b8c --- /dev/null +++ b/core/libraries/Hubzero/Console/Arguments.php @@ -0,0 +1,356 @@ +raw = $arguments; + self::registerNamespace(__NAMESPACE__ . '\\Command'); + } + + /** + * Simple getter for class properties + * + * Throws invalid property exception if property isn't found + * + * @param string $var The property to retrieve + * @return void + **/ + public function get($var) + { + if (isset($this->{$var})) + { + return $this->{$var}; + } + else + { + throw new InvalidPropertyException("Property {$var} does not exists."); + } + } + + /** + * Getter for those additional options that a given command may use + * + * @param string $key Option name to retieve value for + * @param mixed $default Default value for option + * @return void + **/ + public function getOpt($key, $default = false) + { + return (isset($this->opts[$key])) ? $this->opts[$key] : $default; + } + + /** + * Get all opts + * + * @return array + **/ + public function getOpts() + { + return $this->opts; + } + + /** + * Setter for additional options for a given command + * + * @param string $key The argument to set + * @param mixed $value The argument value to give it + * @return void + **/ + public function setOpt($key, $value) + { + $this->opts[$key] = $value; + } + + /** + * Delete option + * + * @param string $key The argument to remove + * @return void + **/ + public function deleteOpt($key) + { + unset($this->opts[$key]); + } + + /** + * Parse the raw arguments into command, task, and additional options + * + * @return void + **/ + public function parse() + { + if (isset($this->raw) && count($this->raw) > 0) + { + $class = isset($this->raw[1]) ? $this->raw[1] : 'help'; + $task = (isset($this->raw[2]) && substr($this->raw[2], 0, 1) != "-") ? $this->raw[2] : 'execute'; + + $this->class = self::routeCommand($class); + $this->task = self::routeTask($class, $this->class, $task); + + // Parse the remaining args for command options/arguments + for ($i = 2; $i < count($this->raw); $i++) + { + // Ignore the second arg if we used it above as task + if ($i == 2 && substr($this->raw[$i], 0, 1) != "-") + { + continue; + } + + // Args with an "=" will use the value before as key and the value after as value + if (strpos($this->raw[$i], "=") !== false) + { + $parts = explode("=", $this->raw[$i], 2); + $key = preg_replace("/^([-]{1,2})/", "", $parts[0]); + $value = ($parts[1]); + + if (isset($this->opts[$key])) + { + $this->opts[$key] = (array)$this->opts[$key]; + array_push($this->opts[$key], $value); + } + else + { + $this->opts[$key] = $value; + } + + continue; + } + // Args with a dash but no equals sign will be considered TRUE if present + elseif (substr($this->raw[$i], 0, 1) == '-') + { + // Try to catch clumped arguments (ex: -if as shorthand for -i -f) + if (preg_match("/^-([[:alpha:]]{2,})/", $this->raw[$i], $matches)) + { + if (isset($matches[1])) + { + foreach (str_split($matches[1], 1) as $k) + { + $this->opts[$k] = true; + } + } + + continue; + } + else + { + $key = preg_replace("/^([-]{1,2})/", "", $this->raw[$i]); + $value = true; + } + } + // Otherwise, we'll just save the arg as a single word and individual commands may use them + else + { + $key = $i; + $value = $this->raw[$i]; + } + + $this->opts[$key] = $value; + } + } + } + + /** + * Registers a location to look for commands + * + * @param string $namespace The namespace location to use + * @param array $paths Optional paths to load from + * @return $this + **/ + public static function registerNamespace($namespace, $paths = array()) + { + self::$commandNamespaces[$namespace] = (array)$paths; + } + + /** + * Routes command to the proper file based on the input given + * + * @param string $command The command to route + * @return void + **/ + public static function routeCommand($command = 'help') + { + // Aliases take precedence, so parse for them first + if ($aliases = Config::get('aliases')) + { + if (array_key_exists($command, $aliases)) + { + if (strpos($aliases->$command, '::') !== false) + { + $bits = explode('::', $aliases->$command); + $command = $bits[0]; + $aliasTask = $bits[1]; + } + else + { + $command = $aliases->$command; + } + } + } + + foreach (self::$commandNamespaces as $namespace => $paths) + { + // Check if we're targeting a namespaced command + $bits = []; + if (strpos($command, ':')) + { + $bits = explode(':', $command); + } + else + { + $bits[] = $command; + } + + $bits = array_map('ucfirst', $bits); + + // Replace any inset placeholders + for ($i = 0; $i < count($bits); $i++) + { + $loc = $i + 1; + if (strpos($namespace, "{\$$loc}")) + { + $namespace = str_replace("{\$$loc}", $bits[$i], $namespace); + if (!empty($paths)) + { + foreach ($paths as $p => $path) + { + $paths[$p] = str_replace("{\$$loc}", $bits[$i], $path); + } + } + unset($bits[$i]); + } + } + + // Add any remaining bits to the end of the command namespace + if (count($bits) > 0) + { + $namespace .= '\\' . implode('\\', $bits); + + if (!empty($paths)) + { + foreach ($paths as $p => $path) + { + $paths[$p] .= '/' . implode('/', $bits); + } + } + } + + // Check for existence + if (!class_exists($namespace) && !empty($paths)) + { + foreach ($paths as $path) + { + $path = strtolower($path); + if (file_exists($path . '.php')) + { + require_once $path . '.php'; + break; + } + } + } + + if (class_exists($namespace)) + { + $class = $namespace; + break; + } + } + + if (!isset($class)) + { + throw new UnsupportedCommandException("Unknown command: {$command}."); + } + + return $class; + } + + /** + * Routes task to the proper method based on the input given + * + * @param string $command The command to route + * @param string $class The class deduced from routeCommand + * @param string $task The task to route + * @return void + **/ + public static function routeTask($command, $class, $task = 'execute') + { + // Aliases take precedence, so parse for them first + if ($aliases = Config::get('aliases')) + { + if (array_key_exists($command, $aliases)) + { + if (strpos($aliases->$command, '::') !== false) + { + $bits = explode('::', $aliases->$command); + $task = $bits[1]; + } + } + } + + // Make sure task exists + if (!method_exists($class, $task)) + { + throw new UnsupportedTaskException("{$class} does not support the {$task} method."); + } + + return $task; + } +} diff --git a/core/libraries/Hubzero/Console/ArgumentsServiceProvider.php b/core/libraries/Hubzero/Console/ArgumentsServiceProvider.php new file mode 100644 index 00000000000..278c3030dcb --- /dev/null +++ b/core/libraries/Hubzero/Console/ArgumentsServiceProvider.php @@ -0,0 +1,73 @@ +app['arguments'] = function($app) + { + global $argv; + + // Register namespace for App commands and component commands + if (defined('PATH_APP')) + { + Arguments::registerNamespace('\App\Commands', [ + PATH_APP . '/commands', + ]); + Arguments::registerNamespace('\Components\{$1}\Commands', [ + PATH_APP . '/components/com_{$1}/commands', + PATH_CORE . '/components/com_{$1}/commands' + ]); + } + + return new Arguments($argv); + }; + } + + /** + * Handle request in stack + * + * @param object $request Request + * @return mixed + */ + public function handle(Request $request) + { + $response = $this->next($request); + + try + { + $this->app->get('arguments')->parse(); + } + catch (UnsupportedCommandException $e) + { + $this->app->get('output')->error($e->getMessage()); + } + catch (UnsupportedTaskException $e) + { + $this->app->get('output')->error($e->getMessage()); + } + + return $response; + } +} diff --git a/core/libraries/Hubzero/Console/Command/App.php b/core/libraries/Hubzero/Console/Command/App.php new file mode 100644 index 00000000000..57ace359774 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/App.php @@ -0,0 +1,43 @@ +run(); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Helper for the app directory' + ) + ->addTasks($this); + } +} diff --git a/core/libraries/Hubzero/Console/Command/App/Package.php b/core/libraries/Hubzero/Console/Command/App/Package.php new file mode 100644 index 00000000000..662b4341080 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/App/Package.php @@ -0,0 +1,159 @@ +output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + + /** + * Show packages + * + * @museDescription Shows a list of active packages + * + * @return void + **/ + public function show() + { + $package = $this->arguments->getOpt('package'); + if (!empty($package)) + { + $versions = Composer::findRemotePackages($package, '*'); + $this->output->addRawFromAssocArray($versions); + } + else + { + $installed = Composer::getLocalPackages(); + $this->output->addRawFromAssocArray($installed); + } + } + + /** + * Show available packages + * + * @museDescription Shows a list of available remote packages + * + * @return void + **/ + public function available() + { + $package = $this->arguments->getOpt('package'); + if (!empty($package)) + { + $versions = Composer::findRemotePackages($package, '*'); + $this->output->addRawFromAssocArray($versions); + } + else + { + $available = Composer::getAvailablePackages(); + $this->output->addRawFromAssocArray($available); + } + } + + /** + * Add a package + * + * @museDescription Installs a package + * + * @return void + **/ + public function install() + { + if ($this->arguments->getOpt('package')) + { + $package = $this->arguments->getOpt('package'); + } + if ($this->arguments->getOpt('version')) + { + $version = $this->arguments->getOpt('version'); + } + if (!isset($package) || !isset($version)) + { + $this->output->error('A package name and version is required'); + } + + try + { + Composer::installPackage($package, $version); + } + catch (Exception $e) + { + + } + $this->output->addLine("Done. $package($version) installed."); + } + + /** + * Update a package + * + * @museDescription Updates a package according to version constraints + * + * @return void + **/ + public function update() + { + $package = $this->arguments->getOpt('package'); + if (empty($package)) + { + $this->output->error('A package name is required'); + } + + Composer::updatePackage($package); + $this->output->addLine("Done. $package updated to latest version"); + } + + /** + * Remove a package + * + * @museDescription Removes a package + * + * @return void + **/ + public function remove() + { + $package = $this->arguments->getOpt('package'); + if (empty($package)) + { + $this->output->error('A package name is required'); + } + + Composer::removePackage($package); + $this->output->addLine("Done. $package has been removed."); + } + + /** + * Shows help text for package command + * + * @return void + **/ + public function help() + { + $this->output->addOverview('Add, remove, and update packages') + ->addTasks($this); + } +} diff --git a/core/libraries/Hubzero/Console/Command/App/Repository.php b/core/libraries/Hubzero/Console/Command/App/Repository.php new file mode 100644 index 00000000000..b5eb6a8f6eb --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/App/Repository.php @@ -0,0 +1,81 @@ +output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + + /** + * Show packages + * + * @museDescription Shows a list of active repositories + * + * @return void + **/ + public function show() + { + $repositories = Composer::getRepositoryConfigs(); + $this->output->addRawFromAssocArray($repositories); + } + + /** + * Add a repository + * + * @museDescription Adds a repository + * + * @return void + **/ + public function add() + { + //Add via composer.json for now + } + + /** + * Remove a repository + * + * @museDescription Removes a repository + * + * @return void + **/ + public function remove() + { + //Remove via composer.json for now + } + + /** + * Shows help text for repository command + * + * @return void + **/ + public function help() + { + $this->output->addOverview('Add, remove, and update repositories for packages') + ->addTasks($this); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Base.php b/core/libraries/Hubzero/Console/Command/Base.php new file mode 100644 index 00000000000..2451a784e9a --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Base.php @@ -0,0 +1,44 @@ +output = $output; + $this->arguments = $arguments; + } +} diff --git a/core/libraries/Hubzero/Console/Command/Cache.php b/core/libraries/Hubzero/Console/Command/Cache.php new file mode 100644 index 00000000000..bbb3028efdb --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Cache.php @@ -0,0 +1,88 @@ +help(); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this->output + ->getHelpOutput() + ->addOverview('Cache Management') + ->addTasks($this) + ->render(); + } + + /** + * Clear all Cache + * + * @museDescription Clears all cached items in document root cache directory + * + * @return void + */ + public function clear() + { + // Path to cache folder + $cacheDir = PATH_APP . DS . 'cache' . DS . '*'; + + // Remove recursively + foreach (glob($cacheDir) as $cacheFileOrDir) + { + $readable = str_replace(PATH_APP . DS, '', $cacheFileOrDir); + if (is_dir($cacheFileOrDir)) + { + if (!Filesystem::deleteDirectory($cacheFileOrDir)) + { + $this->output->addLine('Unable to delete cache directory: ' . $readable, 'error'); + } + else + { + $this->output->addLine($readable . ' deleted', 'success'); + } + } + else + { + // Don't delete index.html + if ($cacheFileOrDir != PATH_APP . DS . 'cache' . DS . 'index.html') + { + if (!Filesystem::delete($cacheFileOrDir)) + { + $this->output->addLine('Unable to delete cache file: ' . $readable, 'error'); + } + else + { + $this->output->addLine($readable . ' deleted', 'success'); + } + } + } + } + + $this->output->addLine('Clear cache complete', 'success'); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Cache/Css.php b/core/libraries/Hubzero/Console/Command/Cache/Css.php new file mode 100644 index 00000000000..6b1079f854a --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Cache/Css.php @@ -0,0 +1,75 @@ +help(); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this->output + ->getHelpOutput() + ->addOverview('Cache Management') + ->render(); + } + + /** + * Clear Site.css & Site.less.cache files + * + * @return void + */ + public function clear() + { + $cacheDir = PATH_APP . DS . 'cache'; + $files = array('site.css', 'site.less.cache'); + + // Remove each file + foreach ($files as $file) + { + if (!is_file($cacheDir . DS . $file)) + { + $this->output->addLine($file . ' does not exist', 'warning'); + continue; + } + + if (!Filesystem::delete($cacheDir . DS . $file)) + { + $this->output->addLine('Unable to delete cache file: ' . $file, 'error'); + } + else + { + $this->output->addLine($file . ' deleted', 'success'); + } + } + + // success! + $this->output->addLine('All CSS cache files removed!', 'success'); + } +} diff --git a/core/libraries/Hubzero/Console/Command/CommandInterface.php b/core/libraries/Hubzero/Console/Command/CommandInterface.php new file mode 100644 index 00000000000..ae0c68f33d8 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/CommandInterface.php @@ -0,0 +1,42 @@ +set(); + } + + /** + * Sets a configuration option + * + * Sets/updates config vars, creating .muse config file as needed + * + * @museDescription Sets the defined key/value pair and saves it into the user's configuration + * + * @return void + **/ + public function set() + { + $options = $this->arguments->getOpts(); + + if (empty($options)) + { + if ($this->output->isInteractive()) + { + $options = array(); + $option = $this->output->getResponse('What do you want to configure [name|email|etc...] ?'); + + if (is_string($option) && !empty($option)) + { + $options[$option] = $this->output->getResponse("What do you want your {$option} to be?"); + } + else if (empty($option)) + { + $this->output->error("Please specify what option you want to set."); + } + else + { + $this->output->error("The {$option} option is not currently supported."); + } + } + else + { + $this->output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + } + + if (Config::save($options)) + { + $this->output->addLine('Saved new configuration!', 'success'); + } + else + { + $this->output->error('Failed to save configuration'); + } + } + + /** + * Shows help text for configure command + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Store shared configuration variables used by the command line tool. + These will, for example, be used to fill in docblock stubs when + using the scaffolding command.' + ) + ->addTasks($this) + ->addArgument( + '--{keyName}', + 'Sets the variable keyName to the given value.', + 'Example: --name="John Doe"' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Configuration/Aliases.php b/core/libraries/Hubzero/Console/Command/Configuration/Aliases.php new file mode 100644 index 00000000000..0e545dc413b --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Configuration/Aliases.php @@ -0,0 +1,64 @@ +output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + + /** + * Adds a new console alias + * + * @return void + **/ + public function add() + { + // Get the alias we're setting + $name = $this->arguments->getOpt(3); + $path = $this->arguments->getOpt(4); + + // Delete the primary args so they aren't added as top level config values + $this->arguments->deleteOpt(3); + $this->arguments->deleteOpt(4); + + // Set the new aliases argument + $this->arguments->setOpt('aliases', array($name => $path)); + + // Redirect back to the basic configuration set method + App::get('client')->call('configuration', 'set', $this->arguments, $this->output); + } + + /** + * Shows help text for aliases command + * + * @return void + **/ + public function help() + { + $this->output->addOverview('Add and remove user-specific command line aliases.'); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Configuration/Hooks.php b/core/libraries/Hubzero/Console/Command/Configuration/Hooks.php new file mode 100644 index 00000000000..49c4cc8eb79 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Configuration/Hooks.php @@ -0,0 +1,64 @@ +output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + + /** + * Adds a new console hook + * + * @return void + **/ + public function add() + { + // Get the hook we're setting + $trigger = $this->arguments->getOpt(3); + $hook = $this->arguments->getOpt(4); + + // Delete the primary args so they aren't added as top level config values + $this->arguments->deleteOpt(3); + $this->arguments->deleteOpt(4); + + // Set the new hooks argument + $this->arguments->setOpt('hooks', array($trigger => array($hook))); + + // Redirect back to the basic configuration set method + App::get('client')->call('configuration', 'set', $this->arguments, $this->output); + } + + /** + * Shows help text for hooks command + * + * @return void + **/ + public function help() + { + $this->output->addOverview('Add and remove user-specific command line hooks.'); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Database.php b/core/libraries/Hubzero/Console/Command/Database.php new file mode 100644 index 00000000000..685ce10280d --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Database.php @@ -0,0 +1,187 @@ +output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + } + + /** + * Dump the database + * + * @museDescription Dumps the current site database into a file in the users home directory + * + * @return void + **/ + public function dump() + { + $tables = App::get('db')->getTableList(); + $prefix = App::get('db')->getPrefix(); + $excludes = []; + $now = new Date; + $exclude = ''; + $includes = (array)$this->arguments->getOpt('include-table', []); + + if (!$this->arguments->getOpt('all-tables')) + { + $this->output->addLine('Dumping database with all prefixed tables included'); + foreach ($tables as $table) + { + if (strpos($table, $prefix) !== 0 && !in_array(str_replace('#__', $prefix, $table), $includes)) + { + $excludes[] = Config::get('db') . '.' . $table; + } + elseif (in_array(str_replace('#__', $prefix, $table), $includes)) + { + $this->output->addLine('Also including `' . $table . '`'); + } + } + + // Build exclude list string + $exclude = '--ignore-table=' . implode(' --ignore-table=', $excludes); + } + else + { + $this->output->addLine('Dumping database with all tables included'); + } + + // Add save location option + + $home = getenv('HOME'); + $hostname = gethostname(); + $filename = tempnam($home, "{$hostname}.mysql.dump." . $now->format('Y.m.d') . ".sql."); + + // Build command + $cmd = "mysqldump -u " . Config::get('user') . " -p'" . Config::get('password') . "' " . Config::get('db') . " --routines {$exclude} > {$filename}"; + + exec($cmd); + + // Print out location of file + $this->output->addLine('File saved to: ' . $filename, 'success'); + } + + /** + * Load a database dump + * + * @museDescription Loads the provided database into the hubs currently configured database + * + * @return void + **/ + public function load() + { + if (!$infile = $this->arguments->getOpt(3)) + { + $this->output->error('Please provide an input file'); + } + else + { + if (!is_file($infile)) + { + $this->output->error("'{$infile}' does not appear to be a valid file"); + } + } + + // First, set some things aside that we need to reapply after the update + $params = []; + $params['com_system'] = \Component::params('com_system'); + $params['com_tools'] = \Component::params('com_tools'); + $params['com_usage'] = \Component::params('com_usage'); + $params['com_members'] = \Component::params('com_members'); + $params['plg_projects_databases'] = \Plugin::params('projects', 'databases'); + + $tables = App::get('db')->getTableList(); + + // See if we should drop all tables first + if ($this->arguments->getOpt('drop-all-tables')) + { + $this->output->addLine('Dropping all tables...'); + foreach ($tables as $table) + { + App::get('db')->dropTable($table); + } + } + + // Craft the command to be executed + $infile = escapeshellarg($infile); + $cmd = "mysql -u " . Config::get('user') . " -p'" . Config::get('password') . "' -D " . Config::get('db') . " < {$infile}"; + + $this->output->addLine('Loading data from ' . $infile . '...'); + + // Now push the big red button + exec($cmd); + + $migration = new Migration(App::get('db')); + + // Now load some things back up + foreach ($params as $k => $v) + { + if (!empty($v)) + { + $migration->saveParams($k, $v); + } + } + + $this->output->addLine('Load complete!', 'success'); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Database utility functions for migrating and restoring database backups. + The necessity for this function arose primarily from the need to copy + a production database down to development environments without overwriting + certain development configuration with inappropriate production values.' + ) + ->addTasks($this) + ->addArgument( + '--include-table: Include a specific table', + 'Specify a given table to be included in the dump. This primarily + would be used to include a given table from the non-prefixed namespace.', + 'Example: --include-table=migration' + ) + ->addArgument( + '--all-tables: Include all tables', + 'By default, the database dump does not include non-prefixed tables + (example: host, display, etc...). This option can be used to include + these tables. Use with caution when planning to eventually load this + data into another host (ex: dev) as it rarely makes sense to reload + tool sessions into another environment.' + ) + ->addArgument( + '--drop-all-tables: Drop all tables', + 'When loading in a database dump, this option will drop all tables + prior to loading in the given dump. This is often helpful when the + applied dump is divergent in schema from the current database being + overwritten.' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Environment.php b/core/libraries/Hubzero/Console/Command/Environment.php new file mode 100644 index 00000000000..7c24b11560e --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Environment.php @@ -0,0 +1,40 @@ +output->addLine('Current user : ' . Config::get('user_name') . ' <' . Config::get('user_email') . '>'); + $this->output->addLine('Current database : ' . \Config::get('db')); + } + + /** + * Help output + * + * @return void + **/ + public function help() + { + $this->output->addOverview('Environment display/management functions'); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Extension.php b/core/libraries/Hubzero/Console/Command/Extension.php new file mode 100644 index 00000000000..a0e8ecdb85f --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Extension.php @@ -0,0 +1,274 @@ +output->isInteractive()) + { + $actions = array('add', 'delete', 'install', 'enable', 'disable'); + $action = $this->output->getResponse('What do you want to do? ['.implode("|", $actions).']'); + + if (in_array($action, $actions)) + { + $this->$action(); + } + else + { + $this->output->error('Sorry, I don\'t know how to do that.'); + } + } + else + { + $this->output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + } + + /** + * Add an entry to the extension table + * + * @museDescription Adds a new extension to the extensions table + * + * @return void + **/ + public function add() + { + $this->alter('add'); + } + + /** + * Delete an entry from the extension table + * + * @museDescription Deletes an existing entry from the extensions table + * + * @return void + **/ + public function delete() + { + $this->alter('delete'); + } + + /** + * Install an extension + * + * @museDescription Installs an extension, adding it if it hasn't been already + * + * @return void + **/ + public function install() + { + $this->alter('install'); + } + + /** + * Enable an extension + * + * @museDescription Enables an existing extension + * + * @return void + **/ + public function enable() + { + $this->alter('enable'); + } + + /** + * Disable an extension + * + * @museDescription Disables an existing extension + * + * @return void + **/ + public function disable() + { + $this->alter('disable'); + } + + /** + * Alter extension + * + * @param string $method The method name + * @return void + **/ + private function alter($method) + { + $migration = new Migration(\App::get('db')); + + $name = null; + if ($this->arguments->getOpt('name')) + { + $name = $this->arguments->getOpt('name'); + } + + if (!isset($name)) + { + if ($this->output->isInteractive()) + { + $name = $this->output->getResponse("What extension were you wanting to {$method}?"); + } + else + { + $this->output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + } + + $extensionType = substr($name, 0, 3); + + switch ($extensionType) + { + case 'com': + if ($method == 'add' || $method == 'delete') + { + $extensionName = ucfirst(substr($name, 4)); + $mthd = $method . 'ComponentEntry'; + } + else + { + $extensionName = $name; + $mthd = $method . 'Component'; + } + + if (!method_exists($migration, $mthd)) + { + $this->output->error('Sorry, components do not currently support the ' . $mthd . ' method'); + } + + $migration->$mthd($extensionName); + break; + + case 'mod': + $client = $this->arguments->getOpt('client', 'site'); + + if ($method == 'install') + { + $mthd = $method . 'Module'; + $position = $this->arguments->getOpt('position', null); + + if (!isset($position)) + { + if ($this->output->isInteractive()) + { + $position = $this->output->getResponse("Where should the module be positioned?"); + } + else + { + $this->output->addLine('Please provide a position for the module', 'warning'); + return; + } + } + + if (!method_exists($migration, $mthd)) + { + $this->output->error('Sorry, modules do not currently support the ' . $mthd . ' method'); + } + + $migration->$mthd(str_replace('mod_', '', $name), $position, true, '', (($client == 'admin') ? 1 : 0)); + } + else + { + $mthd = $method . 'Module' . (($method == 'add' || $method == 'delete') ? 'Entry' : ''); + + if (!method_exists($migration, $mthd)) + { + $this->output->error('Sorry, modules do not currently support the ' . $mthd . ' method'); + } + + $migration->$mthd($name, 1, '', (($client == 'admin') ? 1 : 0)); + } + break; + + case 'tpl': + $mthd = $method . 'Template' . (($method == 'add' || $method == 'delete') ? 'Entry' : ''); + + if (!method_exists($migration, $mthd)) + { + $this->output->error('Sorry, templates do not currently support the ' . $mthd . ' method'); + } + + $element = $name = substr($name, 4); + $name = ucwords($name); + $client = $this->arguments->getOpt('client', 'site'); + $isCore = $this->arguments->getOpt('core', false) ? 1 : 0; + + if ($method == 'delete') + { + $migration->$mthd($element, (($client == 'admin') ? 1 : 0)); + } + else if ($method == 'add') + { + $migration->$mthd($element, $name, (($client == 'admin') ? 1 : 0), 1, 0, null, $isCore); + } + else + { + $migration->$mthd($element, $name, (($client == 'admin') ? 1 : 0), null, $isCore); + } + break; + + case 'plg': + preg_match('/plg_([[:alnum:]]+)_([[:alnum:]]*)/', $name, $matches); + + if (!isset($matches[1]) || !isset($matches[2])) + { + $this->output->error('This does not appear to be a valid extension name.'); + } + + $folder = $matches[1]; + $element = $matches[2]; + $mthd = $method . 'Plugin' . (($method == 'add' || $method == 'delete') ? 'Entry' : ''); + + if (!method_exists($migration, $mthd)) + { + $this->output->error('Sorry, plugins do not currently support the ' . $mthd . ' method'); + } + + $migration->$mthd($folder, $element); + break; + + default: + $this->output->error('This does not appear to be a valid extension name.'); + break; + } + + $this->output->addLine("Successfully {$method}" . ((substr($method, -1) == 'e') ? 'd' : 'ed') . " {$name}!", 'success'); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Extension management utility functions.' + ) + ->addTasks($this); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Group.php b/core/libraries/Hubzero/Console/Command/Group.php new file mode 100644 index 00000000000..6ab04dc7ec8 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Group.php @@ -0,0 +1,158 @@ +arguments->getOpt('group')) + { + $group = \Hubzero\User\Group::getInstance($cname); + } + else + { + // Get the current directory + $currentDirectory = getcwd(); + + // Remove web root + $currentDirectory = str_replace(PATH_APP, '', $currentDirectory); + + // Get group upload directory + $groupsConfig = \Component::params('com_groups'); + $groupsDirectory = trim($groupsConfig->get('uploadpath', '/site/groups'), DS); + + // Are we within the groups upload path + if (strpos($currentDirectory, $groupsDirectory)) + { + $gid = str_replace($groupsDirectory, '', $currentDirectory); + $gid = trim($gid, DS); + + // Get group instance + $group = \Hubzero\User\Group::getInstance($gid); + } + } + + // Make sure we have a group & its super! + if (isset($group) && $group && $group->isSuperGroup()) + { + $this->group = $group; + } + else + { + $this->output->error('Error: Provided group is not valid'); + } + } + + /** + * Default (required) command - just executes run + * + * @return void + **/ + public function execute() + { + $this->output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + + /** + * Run super groups scaffolding + * + * @return void + **/ + public function scaffolding() + { + // Get group config + $groupsConfig = \Component::params('com_groups'); + + // Path to group folder + $directory = trim($groupsConfig->get('uploadpath', '/site/groups'), DS); + $directory .= DS . $this->group->get('gidNumber'); + + // Determine what we want to create + $createWhat = ($this->arguments->getOpt(3)) ? $this->arguments->getOpt(3) : 'component'; + + // Set our needed args + $this->arguments->setOpt(3, $createWhat); + $this->arguments->setOpt('install-dir', $directory); + \App::get('client')->call('scaffolding', 'create', $this->arguments, $this->output); + } + + /** + * Run super groups migration + * + * @return void + */ + public function migrate() + { + // Set our group arg & call migration + $this->arguments->setOpt('group', $this->group->get('cn')); + \App::get('client')->call('migration', 'run', $this->arguments, $this->output); + } + + /** + * Update super group code + * + * @return void + */ + public function update() + { + // Get group config + $groupsConfig = \Component::params('com_groups'); + + // Path to group folder + $directory = PATH_APP . DS . trim($groupsConfig->get('uploadpath', '/site/groups'), DS); + $directory .= DS . $this->group->get('gidNumber'); + + // Get task, defaults to update + $task = ($this->arguments->getOpt(3)) ? $this->arguments->getOpt(3) : 'update'; + + // Set our group directory & call update + $this->arguments->setOpt('r', $directory); + \App::get('client')->call('repository', $task, $this->arguments, $this->output); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Super group management commands.' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Help.php b/core/libraries/Hubzero/Console/Command/Help.php new file mode 100644 index 00000000000..d2656586795 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Help.php @@ -0,0 +1,238 @@ +output + ->addLine( + 'Muse: HUBzero Command Line Utility', + array( + 'color' => 'blue', + 'format' => 'underline' + )) + ->addSpacer() + ->addString('Usage: muse ') + ->addString('[command]', array('color'=>'green')) + ->addString('[:namespace] ', array('color'=>'blue')) + ->addString('[task] ', array('color'=>'yellow')) + ->addString('[options]') + ->addSpacer() + ->addSpacer() + ->addLine('Commands'); + + // Process commands + $this->processCommands(); + + $this->output + ->addSpacer() + ->addLine('Type "muse [command] help" to view command level options') + ->addSpacer(); + } + + /** + * Just call execute. Normally this would output our help text, but because this is the + * help command, we don't need a separate help method. + * + * @return void + **/ + public function help() + { + $this->execute(); + } + + /** + * Parse the commands directory in search of available commands + * + * @return void + **/ + private function processCommands() + { + // Get all valid commands + $this->getCommands(); + $commands = $this->_commands; + + // Sort commands + sort($commands); + + // Build dimensional representation of commands + $commands = $this->mergeCommands($commands); + + // Now output commands + $this->addEntries($commands); + } + + /** + * Helper to get files in commands directy. This is used to generate a list of commands. + * + * @param string $root The root directory for commands + * @param string $dir The current directory to search for commands (relative to root) + * @return void + **/ + private function getCommands($root = null, $dir = null) + { + $root = $root ?: __DIR__; + $dir = $dir ?: ''; + $cur = $root . ((!empty($dir)) ? DS . $dir : ''); + + // Get files from command directory to use in list + $files = array_diff(scandir($cur), array('..', '.')); + + foreach ($files as $file) + { + if (is_file($cur . DS . $file) && strpos($file, '.php') !== false) + { + $namespace = str_replace($root . DS, '', $cur . DS . $file); + $namespace = str_replace(DS, '\\', $namespace); + $class = str_replace('.php', '', $namespace); + + // Make sure a valid class exists + if (class_exists(__NAMESPACE__ . '\\' . $class)) + { + $reflection = new \ReflectionClass(__NAMESPACE__ . '\\' . $class); + + // Make sure it implements the Command Interface + if ($reflection->implementsInterface(__NAMESPACE__ . '\CommandInterface')) + { + $comment = $reflection->getDocComment(); + + // Check for help ignore flag + if (!preg_match('/@museIgnoreHelp/', $comment)) + { + $this->_commands[] = $class; + } + } + } + } + else if (is_dir($cur . DS . $file)) + { + $this->getCommands($root, ((!empty($dir)) ? $dir . DS : '') . $file); + } + } + } + + /** + * Merge flat list of commands into dimensional array + * + * @param array $commands The commands to parse + * @return array + **/ + private function mergeCommands($commands) + { + $parsed = array(); + + // Loop through commands + foreach ($commands as $command) + { + $bits = array(); + + // Break up namespaced commands + if (strpos($command, '\\')) + { + $bits = explode('\\', $command); + $command = $bits[count($bits)-1]; + unset($bits[count($bits)-1]); + } + + $aux =& $parsed; + + // Loop through bits and build path to element we want to set + foreach ($bits as $b) + { + $aux =& $aux[$b]; + } + + // Set element + $aux[] = $command; + } + + return $parsed; + } + + /** + * Output command entries + * + * @param mixed $entry Item(s) to output + * @param int $ind Indentation level + * @param string $path Path to current entry + * @return void + **/ + private function addEntries($entry, $ind = 1, $path = '') + { + // If it's an array, loop through it + if (is_array($entry)) + { + foreach ($entry as $element => $entry) + { + // If this is just another directory, output an element + if (is_string($element) && !empty($path)) + { + $this->output->addLine( + $element, + array( + 'color' => 'blue', + 'indentation' => $ind+2 + ) + ); + } + + // Dive deeper - if element is string, add it to our path + $this->addEntries($entry, $ind+2, $path . ((is_string($element)) ? "\\{$element}" : '')); + } + } + else + { + $this->output->addLine( + $entry, + array( + 'color' => (($ind > 3) ? 'blue' : 'green'), + 'indentation' => $ind + ) + ); + + // Increment indentation + $ind += 2; + + // Get this command's methods + $reflection = new \ReflectionClass(__NAMESPACE__ . $path . '\\' . $entry); + $methods = $reflection->getMethods(); + + foreach ($methods as $method) + { + // We're assuming here that all public methods are available to be called + if ($method->isPublic() && !$method->isConstructor() && $method->name != 'execute' && $method->name != 'help') + { + $this->output->addLine( + $method->name, + array( + 'color' => 'yellow', + 'indentation' => $ind + ) + ); + } + } + } + } +} diff --git a/core/libraries/Hubzero/Console/Command/Log.php b/core/libraries/Hubzero/Console/Command/Log.php new file mode 100644 index 00000000000..2ba941fefee --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Log.php @@ -0,0 +1,467 @@ +help(); + } + + /** + * Follow a log + * + * @return void + **/ + public function follow() + { + // This really only makes sense in interactive mode...duh + if (!$this->output->isInteractive()) + { + $this->output->error('This feature is only available in interactive mode!'); + } + + // Set tty icanon so keystrokes are captured without enter + // Save settings first so we can reapply them when all is said and done + exec('stty -g', $settings); + exec('stty -icanon'); + + // Get file to follow and a few other arguments + $log = $this->arguments->getOpt(3); + $filters = (array)$this->arguments->getOpt('filter'); + $threshold = $this->arguments->getOpt('threshold'); + $noBeep = $this->arguments->getOpt('no-beep'); + $prompt = trim($this->arguments->getOpt('prompt', '>')); + $dateFormat = $this->arguments->getOpt('date-format', 'normal'); + + // Get class for this log type + $class = __NAMESPACE__ . '\\Log\\' . ucfirst($log); + + // Make sure class exists + if (!class_exists($class)) + { + $this->output->error('Log does not current support the provided log type!'); + } + else + { + $path = $class::path(); + } + + // Validate given log path + if (!is_file($path) || !is_readable($path)) + { + $this->output->error("{$path} does not appear to be a valid log file"); + } + + // Set log and prompt character + $this->log = $class; + $this->promptChar = $prompt; + + // Parse thresholds + if ($threshold) + { + $threshold = $this->parseThresholds($threshold); + } + + // Parse date format + $dateFormat = $this->mapDateFormat($dateFormat); + + // Set up pipes + $descs = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ); + + // Tail the given log + $proc = proc_open("tail -f {$path}", $descs, $fp); + + // Set streams to be non-blocking so we aren't sitting and waiting + stream_set_blocking(STDIN, 0); + stream_set_blocking($fp[1], 0); + + // Print log info/formatting + $class::format($this->output); + + // Print initial prompt character + $this->printPrompt(); + + // Vars to track state/mode + $pause = false; + $input = false; + + // Loop indefinitely + while (true) + { + // Check for new log entries + $buffer = fgets($fp[1]); + + // If we found one and not paused and not in input mode + if ($buffer !== false && !$pause && !$input) + { + // Parse the log line (log class will handle output) + $class::parse($buffer, $this->output, array('threshold'=>$threshold, 'noBeep'=>$noBeep, 'dateFormat'=>$dateFormat)); + + // Add new prompt character + $this->printPrompt(); + } + + // Listen for keystrokes from stdin + if (!$input) + { + $char = fgetc(STDIN); + if ($char !== false) + { + $newPrompt = true; + + switch (ord($char)) + { + // Line feed and carriage return + case 10: + case 13: + // Do nothing + break; + + // Numbers 0-9 -> toggle individual fields + case 48: + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + $this->output->backspace(); + // Toggle will return true if we have a valid field number + if ($result = $class::toggle($char)) + { + $this->output->addLine($result); + } + else + { + // Unknown field + $this->output->addLine('Unknown identifier', 'warning'); + } + break; + + // b - toggle beep + case 98: + $this->output->backspace(); + $noBeep = ($noBeep) ? false : true; + $this->output->addLine('(b)eep ' . (($noBeep) ? 'off' : 'on')); + break; + + // f - show fields + case 102: + $this->output->backspace(); + $class::format($this->output); + break; + + // h - help info + case 104: + $this->output->backspace(); + $help = "q: quit, h: help, i: input mode, p: pause/play, b: beep on/off, f: fields, r: rerender last 100 lines"; + $this->output->addLine($help); + break; + + // i - input mode + case 105: + $this->output->backspace((2+strlen($prompt))); + $this->output->addLine('You\'re entering input mode. Pausing log streaming. Type "done" or "i" to return.'); + $this->output->addString('input mode >>> '); + + // Set input mode to true and undo icanon setting + $input = true; + exec('stty icanon'); + $newPrompt = false; + break; + + // p - toggle play/pause + case 112: + $this->output->backspace(); + $pause = ($pause) ? false : true; + $this->output->addLine((($pause) ? '(p)ause' : '(p)lay')); + break; + + // q - quit + case 113: + $this->output->backspace(); + $this->output->addLine('(q)uit'); + + // Don't just close the process, terminate it! + proc_terminate($proc); + break 2; + + // r - rerender last 100 lines + case 114: + $this->output->backspace(); + $this->output->addLine('(r)erender last 100 lines'); + + // Grab the lines again + $content = shell_exec("tail -100 {$path}"); + + // Split them up and loop through them + $lines = explode("\n", trim($content)); + foreach ($lines as $line) + { + $this->printPrompt(); + $class::parse($line, $this->output, array('threshold'=>$threshold, 'noBeep'=>$noBeep, 'dateFormat'=>$dateFormat)); + } + break; + + // Default + default: + // Just delete the typed character + $this->output->backspace(1, true); + $newPrompt = false; + break; + } + + // Should we print a new prompt character? + if ($newPrompt) + { + $this->printPrompt(); + } + } + } + // Input mode + else + { + $string = fgets(STDIN); + + if ($string !== false) + { + // Initialize vars + $string = trim($string); + $arg = null; + $parts = array(); + + // If a space is present, we'll assume some sort of command/arguments scenario + if (strpos($string, ' ')) + { + $parts = explode(' ', $string, 2); + $string = $parts[0]; + $arg = isset($parts[1]) ? $parts[1] : null; + } + + switch ($string) + { + // Quit completely + case 'quit': + proc_terminate($proc); + break 2; + + // Set date format + case 'date': + // Make sure arg is set + if (isset($arg)) + { + $dateFormat = $this->mapDateFormat($arg); + $this->output->addLine('Date format changed', 'success'); + } + + $this->output->addLine('You can set the date format using: "date [format]". Options include: full, long, normal/default, and short.'); + $this->output->addString('input mode >>> '); + break; + + // Close input mode + case 'done': + case 'i': + $input = false; + exec('stty -icanon'); + $this->printPrompt(); + break; + + // Show fields + case 'fields': + $this->output->addSpacer(); + $class::format($this->output); + $this->output->addString('input mode >>> '); + break; + + // Show or set threshold value(s) + case 'threshold': + // If arg is set, we're setting threshold + if (isset($arg)) + { + $threshold = $this->parseThresholds($arg); + $this->output->addLine('Threshold(s) set'); + } + else + { + if ($threshold) + { + // Parse threshold array + $printable = array(); + foreach ($threshold as $t) + { + $printable[] = $t['key'] . $t['operator'] . $t['value']; + } + $this->output->addString('Threshold is currently set to ' . implode(', ', $printable) . '. '); + } + else + { + $this->output->addString('No thresholds are currently set. '); + } + + $this->output->addLine('You can set thresholds using the format: "threshold field>value[,field=value]"'); + } + + $this->output->addString('input mode >>> '); + break; + + // Hide/show fields + case 'show': + case 'hide': + // Toggle will return false or string indicating fields changed + if ($result = $class::toggle($arg, (($string == 'show') ? true : false))) + { + $this->output->addLine($result, 'success'); + } + else + { + $this->output->addLine("{$arg} is not an available field", 'warning'); + } + + // Default - just show new prompt + default: + $this->output->addString('input mode >>> '); + break; + } + } + } + + // Sleep for a bit so we don't run away with the CPU + usleep(50000); + } + + // Restore tty settings + exec("stty {$settings[0]}"); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this->output + ->getHelpOutput() + ->addOverview('Log management functions') + ->render(); + } + + /** + * Parse thresholds from user input to array + * + * @param string $thresholds The threshold input string to parse + * @return array + **/ + private function parseThresholds($thresholds) + { + $log = $this->log; + + if (strpos($thresholds, ',')) + { + $thresholds = explode(',', $thresholds); + } + else + { + $thresholds = (array)$thresholds; + } + + $threshold = array(); + foreach ($thresholds as $t) + { + preg_match('/([[:alpha:]]+)[\s]*([=|>|<])[\s]*([[:alnum:]\.]+)/', $t, $params); + + if (isset($params[1]) + && isset($params[2]) + && isset($params[3]) + && $log::isField(trim($params[1]))) + { + $threshold[trim($params[1])] = [ + 'key' => $params[1], + 'operator' => $params[2], + 'value' => $params[3] + ]; + } + } + + return $threshold; + } + + /** + * Print prompt character + space + * + * @return void + **/ + private function printPrompt() + { + $this->output->addString($this->promptChar . ' '); + } + + /** + * Map date format keyword to php date format string + * + * @return string + **/ + private function mapDateFormat($name) + { + switch ($name) + { + case 'full': + $log = $this->log; + $format = $log::getDateFormat(); + break; + + case 'long': + $format = "Y-m-d H:i:s"; + break; + + case 'short': + $format = "h:i:sa"; + break; + + default: + $format = "D h:i:sa"; + break; + } + + return $format; + } +} diff --git a/core/libraries/Hubzero/Console/Command/Log/Base.php b/core/libraries/Hubzero/Console/Command/Log/Base.php new file mode 100644 index 00000000000..94c3196d7d7 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Log/Base.php @@ -0,0 +1,292 @@ +addString('The profile log has the following format ('); + $output->addString('* indicates visible field', array('color'=>'blue')); + $output->addLine('):'); + + $i = 0; + foreach (static::$fields as $field => $status) + { + if ($i != 0) + { + $output->addString(' '); + } + + $output->addString('<'); + + if ($i < 10) + { + $output->addString($i . ':'); + } + + if ($status) + { + $output->addString('*' . $field, array('color'=>'blue')); + } + else + { + $output->addString($field); + } + + $output->addString('>'); + $i++; + } + + $output->addSpacer()->addSpacer(); + } + + /** + * Toggle field visibility + * + * @return string|bool + **/ + public static function toggle($field, $status = null) + { + // If we're toggling field based on position in array + if (strlen($field) == 1 && is_numeric($field)) + { + $i = 0; + foreach (static::$fields as $f => $s) + { + if ($i == $field) + { + if (!isset($status)) + { + $status = ($s) ? false : true; + } + + static::$fields[$f] = $status; + + // Return textual description of what happened + return (($status) ? 'Showing ' : 'Hiding ') . $f; + } + $i++; + } + } + // All fields either on or off + else if ($field == 'all') + { + foreach (static::$fields as $f => $s) + { + static::$fields[$f] = $status; + } + + return (($status) ? 'Showing ' : 'Hiding ') . 'all fields'; + } + // Toggling comma-separated list of fields + else if (strpos($field, ',')) + { + $fields = explode(',', $field); + $valid = array(); + + foreach ($fields as $f) + { + $f = trim($f); + if (isset(static::$fields[$f])) + { + $valid[] = $f; + static::$fields[$f] = $status; + } + } + + $return = (($status) ? 'Showing ' : 'Hiding '); + if (empty($valid)) + { + $return = 'No valid fields provided'; + } + else + { + $return .= implode(', ', $valid); + } + + return $return; + } + // Toggling single field + else if (isset(static::$fields[$field])) + { + static::$fields[$field] = $status; + + return (($status) ? 'Showing ' : 'Hiding ') . $field; + } + // Who knows what's going on here! + else + { + return false; + } + } + + /** + * Parse log line + * + * @param string $line Log line + * @param object $output Output object + * @param array $settings Settings to honor + * @return void + **/ + public static function parse($line, $output, $settings) + { + $bits = explode(' ', $line, count(static::$fields)); + $index = 0; + $i_used = 0; + $style = null; + $exceeded = false; + + // First loop through and see if any of our thresholds are exceeded + if (is_array($settings['threshold'])) + { + foreach (static::$fields as $field => $show) + { + if (isset($settings['threshold'][$field])) + { + $operator = $settings['threshold'][$field]['operator']; + $value = $settings['threshold'][$field]['value']; + switch ($operator) + { + case '=': + $statement = (trim($bits[$index]) == $value); + break; + + case '<': + $statement = (trim($bits[$index]) < $value); + break; + + case '>': + default: + $statement = (trim($bits[$index]) > $value); + break; + } + + if ($statement) + { + $exceeded[] = $field; + } + } + + $index++; + } + } + + // Reset index + $index = 0; + + // Loop through and do actual output + foreach (static::$fields as $field => $show) + { + if ($show) + { + if ($exceeded) + { + $style = array('color'=>'red'); + + if (in_array($field, $exceeded)) + { + $style = 'error'; + } + } + + if ($i_used != 0) + { + $output->addString(' '); + } + + $value = trim($bits[$index]); + + // See if we need to change date format + if (isset(static::$dateFormat)) + { + $d = \DateTime::createFromFormat(static::$dateFormat, $value); + if ($d && $d->format(static::$dateFormat) == $value) + { + $value = $d->format($settings['dateFormat']); + } + } + + if (method_exists(get_called_class(), 'parse' . ucfirst($field))) + { + $method = 'parse' . ucfirst($field); + $value = static::$method($value); + } + + $output->addString($value, $style); + + // Increment used count + $i_used++; + } + + // Increment total count + $index++; + } + + $output->addSpacer(); + + // See if the total time exceeds the given threshold + if ($exceeded && !$settings['noBeep']) + { + $output->beep(); + } + } +} diff --git a/core/libraries/Hubzero/Console/Command/Log/Post.php b/core/libraries/Hubzero/Console/Command/Log/Post.php new file mode 100644 index 00000000000..2dd16895efc --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Log/Post.php @@ -0,0 +1,75 @@ + true, + 'uri' => true, + 'referrer' => true, + 'data' => true + ); + + /** + * If dates/times are present, how are they formatted + * + * @var string + **/ + protected static $dateFormat = "Y-m-d\TH:i:s.uP"; + + /** + * Parses + * + * @return void + **/ + public static function parseData($value) + { + $ciphertext = base64_decode($value); + + // Get the IV + $ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv = substr($ciphertext, 0, $ivSize); + + // Get just the cipher without the IV + $ciphertext = substr($ciphertext, $ivSize); + + // Generate key and decrypt + $key = md5(\App::get('config')->get('secret')); + $plaintext = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $ciphertext, MCRYPT_MODE_CBC, $iv); + + return $plaintext; + } + + /** + * Get log path + * + * @return string + **/ + public static function path() + { + $dir = \Config::get('log_path'); + + if (is_dir('/var/log/hubzero-cms')) + { + $dir = '/var/log/hubzero-cms'; + } + + $path = $dir . '/cmspost.log'; + + return $path; + } +} diff --git a/core/libraries/Hubzero/Console/Command/Log/Profile.php b/core/libraries/Hubzero/Console/Command/Log/Profile.php new file mode 100644 index 00000000000..1d93669163f --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Log/Profile.php @@ -0,0 +1,58 @@ + true, + 'hubname' => true, + 'ip' => true, + 'app' => true, + 'uri' => true, + 'query' => true, + 'memory' => true, + 'querycount' => true, + 'timeinqueries' => true, + 'totaltime' => true + ); + + /** + * If dates/times are present, how are they formatted + * + * @var string + **/ + protected static $dateFormat = "Y-m-d\TH:i:s.uP"; + + /** + * Get log path + * + * @return string + **/ + public static function path() + { + $dir = \Config::get('log_path'); + + if (is_dir('/var/log/hubzero-cms')) + { + $dir = '/var/log/hubzero-cms'; + } + + $path = $dir . '/cmsprofile.log'; + + return $path; + } +} diff --git a/core/libraries/Hubzero/Console/Command/Log/Sql.php b/core/libraries/Hubzero/Console/Command/Log/Sql.php new file mode 100644 index 00000000000..d2c0c34365d --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Log/Sql.php @@ -0,0 +1,54 @@ + true, + 'file' => true, + 'line' => true, + 'type' => true, + 'time' => true, + 'query' => true + ); + + /** + * If dates/times are present, how are they formatted + * + * @var string + **/ + protected static $dateFormat = "Y-m-d\TH:i:s.uP"; + + /** + * Get log path + * + * @return string + **/ + public static function path() + { + $dir = \Config::get('log_path'); + + if (is_dir('/var/log/hubzero-cms')) + { + $dir = '/var/log/hubzero-cms'; + } + + $path = $dir . '/sql.log'; + + return $path; + } +} diff --git a/core/libraries/Hubzero/Console/Command/Migration.php b/core/libraries/Hubzero/Console/Command/Migration.php new file mode 100644 index 00000000000..9eea3340637 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Migration.php @@ -0,0 +1,484 @@ +run(); + } + + /** + * Run migration + * + * @museDescription Runs pending migrations according to options provided + * + * @return void + **/ + public function run() + { + // Direction, up or down + $direction = 'up'; + if ($this->arguments->getOpt('d')) + { + if ($this->arguments->getOpt('d') == 'up' || $this->arguments->getOpt('d') == 'down') + { + $direction = $this->arguments->getOpt('d'); + } + else + { + $this->output->error('Error: Direction must be one of "up" or "down"'); + } + } + + // Overriding default document root? + $directory = null; + if ($this->arguments->getOpt('r')) + { + if (is_dir($this->arguments->getOpt('r')) && is_readable($this->arguments->getOpt('r'))) + { + $directory = rtrim($this->arguments->getOpt('r'), DS); + } + else + { + $this->output->error('Error: Provided directory is not valid'); + } + } + + // Migrating a super group + $alternativeDatabase = null; + if ($this->arguments->getOpt('group')) + { + $cname = $this->arguments->getOpt('group'); + $group = \Hubzero\User\Group::getInstance($cname); + if ($group && $group->isSuperGroup()) + { + // Get group config + $groupsConfig = \Component::params('com_groups'); + + // Path to group folder + $directory = PATH_APP . DS . trim($groupsConfig->get('uploadpath', '/site/groups'), DS); + $directory .= DS . $group->get('gidNumber'); + + // make sure we have migrations dir + if (!is_dir($directory . DS . 'migrations') || !is_readable($directory . DS . 'migrations')) + { + $this->output->error('Error: Migrations directory does not exist.'); + } + + // Get group database + $alternativeDatabase = \Hubzero\User\Group\Helper::getDBO(array(), $group->get('cn')); + + // make sure we have a group db + if ($alternativeDatabase->getErrorNum() > 0) + { + $this->output->error('Error: Could not connect to Group Database.'); + } + } + else + { + $this->output->error('Error: Provided group is not valid'); + } + } + + // Forcing update + $force = false; + if ($this->arguments->getOpt('force')) + { + if (!$this->arguments->getOpt('e') && !$this->arguments->getOpt('file')) + { + $this->output->error('Error: You cannot specify the "force" option without specifying a specific extention or file'); + } + else + { + $force = true; + } + } + + // Logging only - record migration + $logOnly = false; + if ($this->arguments->getOpt('m')) + { + if (!$this->arguments->getOpt('e') && !$this->arguments->getOpt('file')) + { + $this->output->error('Error: You cannot specify the "Log only (-m)" option without specifying a specific extention or file'); + } + else + { + $logOnly = true; + } + } + + // Ignore dates + $listAll = false; + if ($this->arguments->getOpt('a') || $this->arguments->getOpt('i')) + { + $listAll = true; + } + + // Specific extension + $extension = null; + if ($this->arguments->getOpt('e')) + { + if (!preg_match('/^com_[[:alnum:]]+$|^mod_[[:alnum:]]+$|^plg_[[:alnum:]]+_[[:alnum:]]+$|^core$/i', $this->arguments->getOpt('e'))) + { + $this->output->error('Error: extension should match the pattern of com_*, mod_*, plg_*_*, or core'); + } + else + { + $extension = $this->arguments->getOpt('e'); + } + } + + // Specific file + $file = null; + if ($this->arguments->getOpt('file')) + { + if (!preg_match('/^Migration[0-9]{14}[[:alnum:]]+\.php$/', $this->arguments->getOpt('file'))) + { + $this->output->error('Error: Provided filename does not appear to be valid'); + } + else + { + $file = $this->arguments->getOpt('file'); + + // Also force "ignore dates mode", as that's somewhat implied by giving a specific filename + $listAll = true; + } + } + + // Dryrun + $dryrun = true; + if ($this->arguments->getOpt('f')) + { + $dryrun = false; + } + + // Email results + $email = false; + if ($this->arguments->getOpt('email')) + { + if (!preg_match('/^[a-zA-Z0-9\.\_\-]+@[a-zA-Z0-9\.]+\.[a-zA-Z]{2,4}$/', $this->arguments->getOpt('email'))) + { + $this->output->error('Error: ' . $this->arguments->getOpt('email') . ' does not appear to be a valid email address'); + } + else + { + $email = $this->arguments->getOpt('email'); + } + } + + // Create migration object + $migration = new \Hubzero\Content\Migration($directory, $alternativeDatabase); + + // Search vendor directories? + if ($this->arguments->getOpt('vendor')) + { + $vendorPath = PATH_APP . DS . 'vendor'; + + if (is_dir($vendorPath)) + { + foreach (scandir($vendorPath) as $namespace) + { + if ($namespace != '.' && $namespace != '..' && is_dir($vendorPath . DS . $namespace)) + { + foreach (scandir($vendorPath . DS . $namespace) as $package) + { + if ($package != '.' && $package != '..' && is_dir($vendorPath . DS . $namespace . DS . $package)) + { + $migrationPath = $vendorPath . DS . $namespace . DS . $package . DS . 'src'; + if (is_dir($migrationPath . DS . 'migrations')) + { + $migration->addSearchPath($migrationPath); + } + } + } + } + } + } + } + + // Make sure we got a migration object + if ($migration === false) + { + $this->output->error('Error: failed to instantiate new migration object.'); + } + + if ($this->output->isInteractive()) + { + // Register callback function for adding lines interactively + $output = $this->output; + $callback = function($message, $type=null) use ($output) + { + $output->addLine($message, $type); + }; + $migration->registerCallback('message', $callback); + + // Add progress callback as well + $progress = $this->output->getProgressOutput(); + $migration->registerCallback('progress', $progress); + } + + // Find migration files + if ($migration->find($extension, $file) === false) + { + // Find failed, do nothing + if (count($migration->get('log')) > 0) + { + $this->output->addLinesFromArray($migration->get('log')); + } + $this->output->error('Migration find failed! See log messages for details.'); + } + else // no errors during 'find', so continue + { + // Run migration itself + if (!$result = $migration->migrate($direction, $force, $dryrun, $listAll, $logOnly)) + { + if (count($migration->get('log')) > 0) + { + $this->output->addLinesFromArray($migration->get('log')); + } + $this->output->error('Migration failed! See log messages for details.'); + } + else + { + if (!$this->output->isInteractive()) + { + if ($this->output->getMode() == 'minimal') + { + if (count($migration->get('log')) > 0) + { + $missed = array(); + $pending = array(); + $complete = array(); + foreach ($migration->get('log') as $log) + { + if (preg_match('/would run up\(\) (.*?)(Migration[0-9]{14}[[:alnum:]_]*\.php)/i', $log['message'], $matches)) + { + $pending[] = $matches[1] . $matches[2]; + } + if (preg_match('/completed up\(\) in (.*?)(Migration[0-9]{14}[[:alnum:]_]*\.php)/i', $log['message'], $matches) + || preg_match('/would ignore up\(\) (.*?)(Migration[0-9]{14}[[:alnum:]_]*\.php)/i', $log['message'], $matches)) + { + $complete[] = $matches[1] . $matches[2]; + } + if (preg_match('/migration up\(\) in (.*?)(Migration[0-9]{14}[[:alnum:]_]*\.php) has not been run/i', $log['message'], $matches)) + { + $missed[] = $matches[1] . $matches[2]; + } + } + + if (count($pending) > 0) + { + $this->output->addLine(array('pending' => $pending)); + } + if (count($missed) > 0) + { + $this->output->addLine(array('missed' => $missed)); + } + if (count($complete) > 0) + { + $this->output->addLine(array('complete' => $complete)); + } + } + } + else + { + $this->output->addLinesFromArray($migration->get('log')); + } + } + + // Final success message + if ($this->output->getMode() != 'minimal') + { + $this->output->addLine('Success: ' . ucfirst($direction) . ' migration complete!', 'success'); + } + } + } + + // Email results if requested (only do so if there's something to report) + if ($email && count($migration->get('affectedFiles')) > 0) + { + $this->output->addLine("Emailing results to: {$email}"); + + $headers = "From: Migrations "; + $subject = "Migration output - " . php_uname("n") . " [" . date("d-M-Y H:i:s") . "]"; + + $message = ""; + foreach ($migration->get('log') as $line) + { + $message .= $line['message'] . "\n"; + } + + // Send the message + if (!mail($email, $subject, $message, $headers)) + { + $this->output->addLine("Error: failed to send message!", 'warning'); + } + } + elseif ($email) + { + $this->output->addLine('Ignoring email as no files were affected in this run.', 'info'); + } + } + + /** + * Report migration run info + * + * @museDescription Shows a history of previously run migrations + * + * @return void + **/ + public function history() + { + $migration = new \Hubzero\Content\Migration(); + $history = $migration->history(); + $items = []; + $maxFile = 0; + $maxUser = 0; + $maxScope = 0; + + + if ($history && count($history) > 0) + { + $items[] = [ + 'File', + 'By', + 'Direction', + 'Date' + ]; + + foreach ($history as $entry) + { + $items[] = [ + $entry->scope . DS . $entry->file, + $entry->action_by, + $entry->direction, + $entry->date + ]; + } + + $this->output->addTable($items, true); + } + else + { + $this->addLine('No history to display.'); + } + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Run a migration. This includes searching for migration files, + depending on the options provided.' + ) + ->addTasks($this) + ->addArgument( + '-d: direction [up|down]', + 'If not specified, defaults to "up".', + 'Example: -d=up or -d=down' + ) + ->addArgument( + '-r: document root', + 'Specify the document root through which the the application + will search for migrations directories. The primary use case + for this is specifying an alternate directory for testing. + By default, it will use the PATH_CORE constant for + the document root.', + 'Example: -r=/www/myhub/unittests/migrations' + ) + ->addArgument( + '-e: extension', + 'Explicity give the extension on which the migration should be run. + This could be one of "com_componentname", "mod_modulename", + or "plg_plugingroup_pluginname". This option is required + when using the force (--force) option and the log only option (-m).', + 'Example: -e=com_courses, -e=plg_members_dashboard' + ) + ->addArgument( + '-a: list all', + 'List all will display all migrations found, not just those needing + to be run. This allows you to see the files that need to be run in the + context of the other files that have already been run. This differs from + the prior -i argument which was needed because, by default, only new + files were considered for a run. Now, all files needing to be run are + included by default, irrespective of whether or not they are dated after + the last run migration.' + ) + ->addArgument( + '-i: ignore dates', + 'DEPRECATED: Now functions as if the -a option were given. + Using this option will scan for and run all migrations that haven\'t + previously been run, irrespective of the date of the migration. + This differs from the default behavior in that normally, only files + dated after the last run date will be eligable to be included in the + migration. This option also differs from force mode (--force) in that it + will find all migrations, but only run those that haven\'t been run + before (whereas --force will run them irrespective of whether or not it + thinks they\'ve already been run). You do not have to use -e with this + option. This option is necessary when needing to run migrations that + have been skipped for one reason or another.' + ) + ->addArgument( + '-f: full run', + 'By default, using the migration command without any options will run + in dry-run mode (meaning no changes will actually be made), displaying + the migrations that would be run, were the command to be fully executed. + Use the "-f" (full run) option to do the full migration run.' + ) + ->addArgument( + '-m: log only', + 'Using this option, a migration will run as normal, and log entries + will be created, but the SQL itself will not be run. As a general + precaution, this should not be run without the extension option (-e). + The primary use case for this option would be marking a migration + as run in the event that it had already been run (manually), yet + not logged in the database.' + ) + ->addArgument( + '--file: run a provided filed', + 'Provide the filename to be run. This and only this file will be run. + This will automatically place the migration in (-i) mode, ignoring dates. + It will not, however, force it to be run, if a log entry for this file + and direction already exists. Use the (--force) option to override this + behavior or run the opposite direction first.', + 'Example: --file=Migration20130101000000ComMigrations.php' + ) + ->addArgument( + '--force: force mode', + 'This option should be used carefully. It will run a migration, + even if it thinks it has already been run. When using this option, + you must also give a specific extension using the (-e) option.' + ) + ->addArgument( + '--email: send email', + 'Specify an email address to receive the output of this run. If no + files are executed during the migration, an email will not be sent.', + 'Example: --email=sampleuser@hubzero.org' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Repository.php b/core/libraries/Hubzero/Console/Command/Repository.php new file mode 100644 index 00000000000..05bc6cf25e2 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Repository.php @@ -0,0 +1,680 @@ +arguments->getOpt('r')) + { + if (is_dir($this->arguments->getOpt('r')) && is_readable($this->arguments->getOpt('r'))) + { + $directory = rtrim($this->arguments->getOpt('r'), DS); + } + else + { + $this->output->error('Error: Provided directory is not valid'); + } + } + + // Try to figure out the mechanism + if (is_dir($directory . DS . '.git')) + { + // Set update source + $source = Config::get('repository_source_name', null); + $source = $this->arguments->getOpt('source', $source); + + if (is_null($source) || preg_match('/^[[:alnum:]\-\_\.]*\/[[:alnum:]\-\_\.]*$/', $source)) + { + $this->mechanism = new Git($directory, $source); + } + else + { + $this->output->error('Sorry, an invalid update mechanism source was provided.'); + } + } + else + { + $this->output->error('Sorry, this command currently only supports setups managed by GIT'); + } + } + + /** + * Default (required) command - just run check command + * + * @return void + **/ + public function execute() + { + if ($this->arguments->getOpt('mechanism')) + { + $this->output->addLine($this->mechanism->getName()); + } + else if ($this->arguments->getOpt('version')) + { + $this->output->addLine(\Hubzero\Version\Version::VERSION); + } + else + { + $this->status(); + } + } + + /** + * Check the current status of the repository + * + * @museDescription Checks the current status of the repository for upgrade eligibility + * + * @return void + **/ + public function status() + { + $mode = $this->output->getMode(); + $status = $this->mechanism->status(); + $message = (!empty($status)) + ? 'This repository is managed by ' . $this->mechanism->getName() . ' and has the following divergence:' + : 'This repository is managed by ' . $this->mechanism->getName() . ' and is clean'; + + if ($mode != 'minimal') + { + $this->output->addLine( + $message, + array( + 'color' => 'blue' + ) + ); + } + + $colorMap = array( + 'added' => 'green', + 'modified' => 'yellow', + 'deleted' => 'cyan', + 'renamed' => 'yellow', + 'copied' => 'yellow', + 'untracked' => 'black', + 'unmerged' => 'red', + 'merged' => 'blue' + ); + + if (is_array($status) && count($status) > 0) + { + foreach ($status as $k => $v) + { + if (count($v) > 0) + { + if ($mode == 'minimal') + { + $this->output->addLine( + array( + $k => $v + ) + ); + } + else + { + $this->output->addSpacer(); + $this->output->addLine(ucfirst($k) . ' files:'); + foreach ($v as $file) + { + $this->output->addLine( + $file, + array( + 'color' => $colorMap[$k], + 'indentation' => 2 + ) + ); + } + } + } + } + } + } + + /** + * Repository log + * + * @museDescription Shows the past and pending changelog for the repository + * + * @return void + **/ + public function log() + { + $mode = $this->output->getMode(); + $length = ($this->arguments->getOpt('length')) ? (int)$this->arguments->getOpt('length') : 20; + $start = ($this->arguments->getOpt('start')) ? (int)$this->arguments->getOpt('start') : null; + $upcoming = $this->arguments->getOpt('include-upcoming'); + $installed = ($this->arguments->getOpt('exclude-installed')) ? false : true; + $search = ($this->arguments->getOpt('search')) ? $this->arguments->getOpt('search') : null; + $format = '%an: %s'; + $count = $this->arguments->getOpt('count'); + + if ($mode == 'minimal') + { + $format = '%H||%an||%ae||%ad||%s'; + } + + $logs = $this->mechanism->log($length, $start, $upcoming, $installed, $search, $format, $count); + + if ($count) + { + $this->output->addLine($logs); + return; + } + + if ($mode != 'minimal') + { + $output = array(); + foreach ($logs as $log) + { + $output[] = array('message'=>$log); + } + + $this->output->addLinesFromArray($output); + } + else + { + if (is_array($logs) && count($logs) > 0) + { + foreach ($logs as $log) + { + $entry = array(); + $parts = explode('||', $log); + $entry[$parts[0]] = array( + 'name' => $parts[1], + 'email' => $parts[2], + 'date' => $parts[3], + 'subject' => $parts[4] + ); + + $this->output->addLine($entry); + } + } + } + } + + /** + * Update the repository + * + * @museDescription Updates the repository, by default performing a dry run + * + * @return void + **/ + public function update() + { + $mode = $this->output->getMode(); + + if ($this->arguments->getOpt('f')) + { + if ($mode != 'minimal') + { + $this->output->addLine( + 'Updating the repository...', + array( + 'color' => 'blue' + ), + false + ); + } + + // Check status and stash as needed + if (!$this->mechanism->isClean()) + { + $this->mechanism->stash(); + } + + // Create rollback point first + $this->mechanism->createRollbackPoint(); + + // Check whether or not we're allowing fast forward pulls only + $allowNonFf = $this->arguments->getOpt('allow-non-ff'); + + // Now do the update + $response = $this->mechanism->update(false, $allowNonFf); + + if ($response['status'] == 'success') + { + // Now, check to see whether or not we need to go ahead and push this merge elsewhere + if ($ref = $this->arguments->getOpt('git-auto-push-ref', false)) + { + $response = $this->mechanism->push($ref); + if ($response['status'] === 'success') + { + if ($mode != 'minimal') + { + $this->output->addLine( + 'complete', + array( + 'color' => 'green' + ) + ); + } + } + else + { + $this->output->addLine( + strtolower($response['message']), + array( + 'color' => 'red' + ) + ); + } + } + else + { + if ($mode != 'minimal') + { + $this->output->addLine( + 'complete', + array( + 'color' => 'green' + ) + ); + } + } + + // Also check to see if we need to update packages + if ($this->arguments->getOpt('install-packages', false)) + { + App::get('client')->call('repository:package', 'install', new Arguments([]), $this->output); + } + } + else if ($response['status'] == 'fatal') + { + $this->output->addLine( + strtolower($response['message']), + array( + 'color' => 'red' + ) + ); + } + else + { + $this->output->addSpacer(); + $this->output->addRaw($response['raw']); + } + } + else + { + $response = $this->mechanism->update(); + + if (!empty($response)) + { + if ($mode != 'minimal') + { + $this->output->addLine('The repository is behind by ' . count($response) . ' update(s):'); + } + $logs = array(); + foreach ($response as $log) + { + if ($mode == 'minimal') + { + $this->output->addLine($log); + } + else + { + $logs[] = array( + 'message' => $log, + 'type' => array( + 'indentation' => 2, + 'color' => 'blue' + ) + ); + } + } + + if ($mode != 'minimal') + { + $this->output->addLinesFromArray($logs); + } + } + else + { + if ($mode != 'minimal') + { + $this->output->addLine('The repository is already up-to-date'); + } + } + } + } + + /** + * Rollback to last (or named) checkpoint + * + * @museDescription Rolls the repository back to the last checkpoint + * + * @return void + **/ + public function rollback() + { + if (!$rollbackPoint = $this->mechanism->getRollbackPoint()) + { + $this->output->error('There are no rollback points currently available'); + } + + $date = date('M jS, Y \a\t g:i:sa', $rollbackPoint); + + if ($this->output->isInteractive()) + { + $proceed = $this->output->getResponse('Are you sure you want to rollback to the snapshot taken on ' . $date . '? [y|n]'); + + if ($proceed == 'y' || $proceed == 'yes') + { + $result = $this->mechanism->rollback($rollbackPoint); + + if ($result) + { + $this->output->addLine('complete', 'success'); + } + else + { + $this->output->error('Rollback failed'); + } + } + else + { + $this->output->addLine('Rollback aborted.', 'warning'); + } + } + else + { + if ($this->arguments->getOpt('f')) + { + $this->mechanism->rollback($rollbackPoint); + } + else + { + $this->output->addLine('Use the -f option to rollback to snapshot taken on ' . $date); + } + } + } + + /** + * Do some repository cleanup + * + * @museDescription Performs cleanup operations including deleting automatic tags and stashes (if applicable) + * + * @return void + **/ + public function clean() + { + if ($this->output->isInteractive()) + { + $performed = 0; + $proceed = $this->output->getResponse('Do you want to purge all rollback points except the latest? [y|n]'); + + if ($proceed == 'y' || $proceed == 'yes') + { + $this->mechanism->purgeRollbackPoints(); + $this->output->addLine('Purging rollback points.'); + $performed++; + } + + $proceed = $this->output->getResponse('Do you want to purge all stashed changes? [y|n]'); + + if ($proceed == 'y' || $proceed == 'yes') + { + $this->mechanism->purgeStash(); + $this->output->addLine('Purging repository stash.'); + $performed++; + } + + $this->output->addLine("Clean up complete. Performed ({$performed}/2) cleanup operations available."); + } + else + { + $didSomething = false; + if ($this->arguments->getOpt('purge-rollback-points')) + { + $this->mechanism->purgeRollbackPoints(); + $this->output->addLine('Purging rollback points.'); + $didSomething = true; + } + + if ($this->arguments->getOpt('purge-stash')) + { + $this->mechanism->purgeStash(); + $this->output->addLine('Purging repository stash.'); + $didSomething = true; + } + + if (!$didSomething) + { + $this->output->addLine('Please specify which cleanup operations to perform'); + } + } + } + + /** + * Run syntax checker on changed files + * + * @museDescription Verifies the validity of the syntax of any pending changes + * + * @return void + **/ + public function syntax() + { + // Get files + $status = $this->mechanism->status(); + $files = (isset($status['added']) || isset($status['modified'])) ? array_merge($status['added'], $status['modified']) : array(); + + // Whether or not to scan untracked files + if (!$this->arguments->getOpt('exclude-untracked')) + { + $files = (isset($status['untracked'])) ? array_merge($files, $status['untracked']) : $files; + } + + // Did we find any files? + if ($files && count($files) > 0) + { + // Base standards directory + if (!$standards = Config::get('repository_standards_dir')) + { + $this->output + ->addSpacer() + ->addLine('You must specify your standards directory first via:') + ->addLine( + 'muse configuration set --repository_standards_dir=/path/to/standards', + array( + 'indentation' => '2', + 'color' => 'blue' + ) + ) + ->addSpacer() + ->error("Error: failed to retrieve standards directory."); + } + else + { + $standards = rtrim($standards, DS) . DS . 'HubzeroCS'; + } + + // See what branch we're on, and set standards directory accordingly + $branch = $this->mechanism->getMechanismVersionName(); + $branch = str_replace('.', '', $branch); + if (!is_dir($standards . $branch)) + { + $this->output->error('A standards directory for the current branch does not exist'); + } + + if ($this->arguments->getOpt('no-linting') && $this->arguments->getOpt('no-sniffing')) + { + $this->output->addLine('No sniffing or linting...that means we\'re not doing anything!', 'warning'); + } + + foreach ($files as $file) + { + $this->output->addString("Scanning {$file}..."); + $passed = true; + $base = $this->mechanism->getBasePath(); + $base = rtrim($base, DS) . DS; + + // Lint files with php extension + if (!$this->arguments->getOpt('no-linting')) + { + if (substr($file, -4) == '.php') + { + $cmd = "php -l {$base}{$file}"; + exec($cmd, $output, $code); + if ($code !== 0) + { + $passed = false; + $this->output->addLine("failed php linter", array('color'=>'red')); + } + } + } + + // Now run them through PHP code sniffer + if (!$this->arguments->getOpt('no-sniffing')) + { + // Append specific standard (with branchname) to command + $cmd = "php " . PATH_CORE . DS . 'bin' . DS . "phpcs --standard={$standards}{$branch}/ruleset.xml -n {$base}{$file}"; + $cmd = escapeshellcmd($cmd); + $result = shell_exec($cmd); + + if (!empty($result)) + { + $passed = false; + $this->output->addLine($result, array('color'=>'red')); + } + } + + // Did it all pass? + if ($passed) + { + $this->output->addLine('clear'); + } + } + } + else + { + $this->output->addLine('No files to scan'); + } + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Repository management functions.' + ) + ->addTasks($this); + } + + /** + * Call composer + * + * @return void + **/ + public function composer() + { + $option = $this->arguments->getOpt('option'); + $task = $this->arguments->getOpt('task'); + $valid_tasks = array("show", "available", "install", "update", "remove", "add"); + if ($option == 'package' && in_array($task, $valid_tasks)) + { + $newCommand = new \Hubzero\Console\Command\App\Package($this->output, $this->arguments); + $newCommand->$task(); + } + if ($option == 'repository' && in_array($task, $valid_tasks)) + { + $newCommand = new \Hubzero\Console\Command\App\Repository($this->output, $this->arguments); + $newCommand->$task(); + } + } + + + /** + * Call cloneRepo + * + * @return void + **/ + public function cloneRepo() + { + $sourceUrl = $this->arguments->getOpt('sourceUrl'); + $repoPath = $this->arguments->getOpt('repoPath'); + + $command = "git clone" . " " . $sourceUrl . " " . $repoPath . ' 2>&1'; + $response = shell_exec($command); + + return $response; + } + + /** + * Call cloneremoveRepoRepo + * + * @return void + **/ + public function removeRepo() + { + $directory = $this->arguments->getOpt('path'); + $local = new Local(); + + $response = $local->deleteDirectory($directory); + return $response; + } + + /** + * Call renameRepo + * + * @return void + **/ + public function renameRepo() + { + $currPath = $this->arguments->getOpt('currPath'); + $targetPath = $this->arguments->getOpt('targetPath'); + + $local = new Local(); + return $local->rename($currPath, $targetPath); + } + + /** + * Call updateRepo + * + * @return void + */ + public function updateRepo() + { + // Set our directory & call update + $this->arguments->setOpt('r', $repoPath ); + + \App::get('client')->call('repository', 'update', $this->arguments, $this->output); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Repository/Flavor.php b/core/libraries/Hubzero/Console/Command/Repository/Flavor.php new file mode 100644 index 00000000000..4ec8f35acac --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Repository/Flavor.php @@ -0,0 +1,358 @@ +help(); + } + + /** + * Set the flavor + * + * @return void + **/ + public function set() + { + if (!$flavor = $this->arguments->getOpt(3)) + { + $this->output->error('Please provide the flavor you would like to use'); + } + + $database = App::get('db'); + $migration = new Migration($database); + + $query = "SELECT `params` FROM `#__template_styles` WHERE `template` = 'welcome';"; + $database->setQuery($query); + $p = $database->loadResult(); + $welcome_params = new \Hubzero\Config\Registry($p); + + switch ($flavor) + { + case 'amazonfull': + + $defaults = array( + '{"module":44,"col":1,"row":1,"size_x":1,"size_y":2}', + '{"module":35,"col":1,"row":3,"size_x":1,"size_y":2}', + '{"module":38,"col":1,"row":5,"size_x":1,"size_y":2}', + '{"module":39,"col":1,"row":7,"size_x":1,"size_y":2}', + '{"module":33,"col":2,"row":1,"size_x":1,"size_y":2}', + '{"module":42,"col":2,"row":3,"size_x":1,"size_y":2}', + '{"module":34,"col":2,"row":5,"size_x":1,"size_y":2}', + '{"module":41,"col":3,"row":1,"size_x":1,"size_y":2}', + '{"module":36,"col":3,"row":3,"size_x":1,"size_y":2}', + '{"module":37,"col":3,"row":5,"size_x":1,"size_y":2}' + ); + + $params = array( + "allow_customization" => "1", + "position" => "memberDashboard", + "defaults" => '[' . implode(',', $defaults) . ']' + ); + + $migration->savePluginParams('members', 'dashboard', $params); + $this->output->addLine('Updating default members dashboard configuration'); + + // Set amazon param in welcome template + $welcome_params->set('flavor', 'amazon'); + if ($welcome_params->get('template', '') == '') + { + $welcome_params->set('template', 'hubbasic2013'); + } + $query = "UPDATE `#__template_styles` SET `params`=".$database->quote(json_encode($welcome_params->toArray()))." WHERE `template`='welcome';"; + $database->setQuery($query); + $database->query(); + + $this->output->addLine('Setting amazon flavor flag in welcome template'); + + // Set amazon template as home + $this->output->addLine('Setting amazon template for welcome page'); + $query = "UPDATE `#__template_styles` SET `home` = 1 where `template` = 'welcome' and `client_id` = 0"; + $database->setQuery($query); + $database->query(); + $query = "UPDATE `#__template_styles` SET `home` = 0 where `template` != 'welcome' and `client_id` = 0"; + $database->setQuery($query); + $database->query(); + + // Update default content page(s) + //$this->output->addLine('Updating default content pages'); + //$this->output->addLine('Updating content page id (22)'); + //$query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/discover.css\"}\r\n
\r\n
\r\n

Do More

\r\n
\r\n\r\n
\r\n
\r\n

Resources

\r\n

Find the latest cutting-edge research in our resources.

\r\n
\r\n
\r\n
\r\n
\r\n

Citations

\r\n

See who has cited our content in their work.

\r\n
\r\n
\r\n
\r\n
\r\n

Tags

\r\n

Explore all our content through tags or even tag content yourself.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n
\r\n

Blog

\r\n

Read the latest entry or browse the archive for articles of interest.

\r\n
\r\n
\r\n
\r\n
\r\n

Wiki

\r\n

Browse our user-generated wiki pages or write your own.

\r\n
\r\n
\r\n
\r\n
\r\n

Feedback

\r\n

Like something? Having trouble? Let us know what you think!

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n

Services

\r\n
\r\n
\r\n
\r\n

Upload

\r\n

Publish your own tools, seminars, and other content on this site.

\r\n
\r\n
\r\n
\r\n
\r\n

Store

\r\n

Purchase items such as t-shirts using points you earn by helping out.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n

What\'s Happening

\r\n
\r\n
\r\n
\r\n

Events

\r\n

Find information about the many upcoming public meetings and scientific symposia.

\r\n
\r\n
\r\n
\r\n
\r\n

What\'s New

\r\n

Find the latest content posted on the site with our What\'s New section.

\r\n
\r\n
\r\n
\r\n
\r\n

Poll

\r\n

Respond to our poll questions and see what everyone else is thinking.

\r\n
\r\n
\r\n
'"; + //query .= " WHERE `id` = '22' AND `alias` = 'discover'"; + //$database->setQuery($query); + //$database->query(); + + //$this->output->addLine('Updating content page id (32)'); + //$query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/gettingstarted.css\"}\r\n\r\n
\r\n
\r\n

Getting To Know Your Hub

\r\n\r\n
\r\n
\r\n
\r\n

Utilize

\r\n\r\n

View tutorials and read about how to use the numerous features of a HUB. We show you how to add content, customize your dashboard, create groups, and more.

\r\n\r\n

User documentation

\r\n
\r\n
\r\n
\r\n
\r\n

Manage

\r\n\r\n

Read the manual for managing the content and functionality of a HUB. It progresses step-by-step through various common tasks and familiarizes you with the administrative interface.

\r\n\r\n

Manager documentation

\r\n
\r\n
\r\n
\r\n
\r\n

Extend

\r\n\r\n

Build your own extensions and discover how to extend or tailor the existing ones to your needs. We try to guide you through the creation steps and provide examples for download.

\r\n\r\n

Developer documentation

\r\n
\r\n
\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n\r\n

Setting Up Your Hub

\r\n

We\'ve provided a default set of content with a menu of commonly used extensions and pages but there is still some setup and spots needing filled in that we, unfortunately, couldn\'t do for you.

\r\n\r\n
\r\n
\r\n

To-Do List

\r\n
    \r\n
  • \r\n

    About You

    \r\n

    Here\'s a page for telling your story and letting your visitors know all about you. You just have to fill in the details!

    \r\n
  • \r\n
  • \r\n

    How to Contact

    \r\n

    Sometimes your visitors will need to get ahold of you. We\'ve provided a page to detail contact information to help make that happen.\"

    \r\n
  • \r\n
  • \r\n

    Terms of Use

    \r\n

    Although we provided a generic Terms of Use, it will need some information filled in and further refinement for your hub.

    \r\n
  • \r\n
\r\n
\r\n
\r\n

Recommended

\r\n
    \r\n
  • \r\n

    Facebook, Google, LinkedIn

    \r\n

    Every hub comes with the ability for users to authenticate with popular services like Facebook, Google, or LinkedIn. Turn one or all of these on with a click of a button!

    \r\n
  • \r\n
  • \r\n

    Set up Analytics

    \r\n

    We highly recommend setting up Google Analytics on your hub. Luckily, we provide a module for doing just that.

    \r\n
  • \r\n
  • \r\n

    Use ReCAPTCHA

    \r\n

    While we provide basic image and text CAPTCHAs to help guard against spam bots, we recommend ReCAPTCHA for stronger protection and larger feature set.

    \r\n
  • \r\n\r\n
  • \r\n

    Email

    Hubs use outgoing email for account setup and a couple other things. Outgoing email from an Amazon hosted server often gets marked as SPAM by many email providers. It is often necessary to setup your hub to use external email services (such as Mandrill) to ensure that email makes it to users\' inboxes.

    \r\n
  • \r\n\r\n\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n\r\n
\r\n

Ready to go? Jump to the administration or learn how to change this page.

\r\n
\r\n\r\n\r\n
\r\n
\r\n\r\n

Where to Find Help

\r\n\r\n

We try to make using and configuring a hub a simple, smooth process. Sometimes, however, you may have questions or issues not easily answered by the available documentation. If you need help or support while developing your hub, feel free to contact us.

\r\n\r\n
\r\n
\r\n
\r\n

I have a question!

\r\n\r\n

Have a question on how to do something? If the documentation doesn\'t seem to be of help, you can try asking the community.

\r\n
\r\n
\r\n
\r\n
\r\n

I have an idea!

\r\n\r\n

Think something can be done better or is missing? Post your ideas or feature requests. We'd love to hear from you.

\r\n
\r\n
\r\n
\r\n
\r\n

I have an error!

\r\n\r\n

We continually test and refine the code for an error-free experience but, sadly, we can make mistakes. If you found a bug, let us know.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
'"; + //$query .= " WHERE `id` = '32' AND `alias` = 'gettingstarted'"; + //$database->setQuery($query); + //$database->query(); + + // disable components + $this->output->addLine('Disabling com_usage'); + $migration->disableComponent('com_usage'); + + $this->output->addLine('Disabling com_store'); + $migration->disableComponent('com_store'); + break; + + case 'amazoncmsonly': + // Disable com_tools + $migration->disableComponent('com_tools'); + $this->output->addLine('Disabling com_tools'); + + // Disable tool-related modules + $migration->disableModule('mod_mytools'); + $this->output->addLine('Disabling mod_mytools'); + $migration->disableModule('mod_mycontributions'); + $this->output->addLine('Disabling mod_contributions'); + $migration->disableModule('mod_mysessions'); + $this->output->addLine('Disabling mod_mysessions'); + + $defaults = array( + '{"module":44,"col":1,"row":1,"size_x":1,"size_y":2}', + '{"module":35,"col":1,"row":3,"size_x":1,"size_y":2}', + '{"module":38,"col":1,"row":5,"size_x":1,"size_y":2}', + '{"module":39,"col":1,"row":7,"size_x":1,"size_y":2}', + '{"module":33,"col":2,"row":1,"size_x":1,"size_y":2}', + '{"module":42,"col":2,"row":3,"size_x":1,"size_y":2}', + '{"module":34,"col":2,"row":5,"size_x":1,"size_y":2}', + '{"module":37,"col":3,"row":1,"size_x":1,"size_y":2}' + ); + + $params = array( + "allow_customization" => "1", + "position" => "memberDashboard", + "defaults" => '[' . implode(',', $defaults) . ']' + ); + + $migration->savePluginParams('members', 'dashboard', $params); + $this->output->addLine('Updating default members dashboard configuration'); + + // Update kb articles + $query = "UPDATE `#__faq_categories` SET `state` = 2 WHERE `alias` = 'tools'"; + $database->setQuery($query); + $database->query(); + $query = "UPDATE `#__faq` SET `state` = 2 WHERE `alias` = 'webdav'"; + $database->setQuery($query); + $database->query(); + $this->output->addLine('Deleting tool and webdav related KB articles'); + + // Set amazon param in welcome template + $welcome_params->set('flavor', 'amazon'); + if ($welcome_params->get('template', '') == '') + { + $welcome_params->set('template', 'hubbasic2013'); + } + $query = "UPDATE `#__template_styles` SET `params`=".$database->quote(json_encode($welcome_params->toArray()))." WHERE `template`='welcome';"; + $database->setQuery($query); + $database->query(); + $this->output->addLine('Setting amazon flavor flag in welcome template'); + + // Set amazon template as home + $this->output->addLine('Setting amazon template for welcome page'); + $query = "UPDATE `#__template_styles` SET `home` = 1 where `template` = 'welcome' and `client_id` = 0"; + $database->setQuery($query); + $database->query(); + $query = "UPDATE `#__template_styles` SET `home` = 0 where `template` != 'welcome' and `client_id` = 0"; + $database->setQuery($query); + $database->query(); + + // Delete tools resource type + $query = "DELETE FROM `#__resource_types` WHERE `alias` = 'tools'"; + $database->setQuery($query); + $database->query(); + $this->output->addLine('Deleting tools resource type'); + + // Update default content page(s) + $this->output->addLine('Updating default content pages'); + $this->output->addLine('Updating content page id (22)'); + $query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/discover.css\"}\r\n
\r\n
\r\n

Do More

\r\n
\r\n\r\n
\r\n
\r\n

Resources

\r\n

Find the latest cutting-edge research in our resources.

\r\n
\r\n
\r\n
\r\n
\r\n

Citations

\r\n

See who has cited our content in their work.

\r\n
\r\n
\r\n
\r\n
\r\n

Tags

\r\n

Explore all our content through tags or even tag content yourself.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n
\r\n

Blog

\r\n

Read the latest entry or browse the archive for articles of interest.

\r\n
\r\n
\r\n
\r\n
\r\n

Wiki

\r\n

Browse our user-generated wiki pages or write your own.

\r\n
\r\n
\r\n
\r\n
\r\n

Feedback

\r\n

Like something? Having trouble? Let us know what you think!

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n

Services

\r\n
\r\n
\r\n
\r\n

Upload

\r\n

Publish your own tools, seminars, and other content on this site.

\r\n
\r\n
\r\n
\r\n
\r\n

Store

\r\n

Purchase items such as t-shirts using points you earn by helping out.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n

What\'s Happening

\r\n
\r\n
\r\n
\r\n

Events

\r\n

Find information about the many upcoming public meetings and scientific symposia.

\r\n
\r\n
\r\n
\r\n
\r\n

What\'s New

\r\n

Find the latest content posted on the site with our What\'s New section.

\r\n
\r\n
\r\n
\r\n
\r\n

Poll

\r\n

Respond to our poll questions and see what everyone else is thinking.

\r\n
\r\n
\r\n
'"; + $query .= " WHERE `id` = '22' AND `alias` = 'discover'"; + $database->setQuery($query); + $database->query(); + + $this->output->addLine('Updating content page id (32)'); + $query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/gettingstarted.css\"}\r\n\r\n
\r\n
\r\n

Getting To Know Your Hub

\r\n\r\n
\r\n
\r\n
\r\n

Utilize

\r\n\r\n

View tutorials and read about how to use the numerous features of a HUB. We show you how to add content, customize your dashboard, create groups, and more.

\r\n\r\n

User documentation

\r\n
\r\n
\r\n
\r\n
\r\n

Manage

\r\n\r\n

Read the manual for managing the content and functionality of a HUB. It progresses step-by-step through various common tasks and familiarizes you with the administrative interface.

\r\n\r\n

Manager documentation

\r\n
\r\n
\r\n
\r\n
\r\n

Extend

\r\n\r\n

Build your own extensions and discover how to extend or tailor the existing ones to your needs. We try to guide you through the creation steps and provide examples for download.

\r\n\r\n

Developer documentation

\r\n
\r\n
\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n\r\n

Setting Up Your Hub

\r\n

We\'ve provided a default set of content with a menu of commonly used extensions and pages but there is still some setup and spots needing filled in that we, unfortunately, couldn\'t do for you.

\r\n\r\n
\r\n
\r\n

To-Do List

\r\n
    \r\n
  • \r\n

    About You

    \r\n

    Here\'s a page for telling your story and letting your visitors know all about you. You just have to fill in the details!

    \r\n
  • \r\n
  • \r\n

    How to Contact

    \r\n

    Sometimes your visitors will need to get ahold of you. We\'ve provided a page to detail contact information to help make that happen.\"

    \r\n
  • \r\n
  • \r\n

    Terms of Use

    \r\n

    Although we provided a generic Terms of Use, it will need some information filled in and further refinement for your hub.

    \r\n
  • \r\n
\r\n
\r\n
\r\n

Recommended

\r\n
    \r\n
  • \r\n

    Facebook, Google, LinkedIn

    \r\n

    Every hub comes with the ability for users to authenticate with popular services like Facebook, Google, or LinkedIn. Turn one or all of these on with a click of a button!

    \r\n
  • \r\n
  • \r\n

    Set up Analytics

    \r\n

    We highly recommend setting up Google Analytics on your hub. Luckily, we provide a module for doing just that.

    \r\n
  • \r\n
  • \r\n

    Use ReCAPTCHA

    \r\n

    While we provide basic image and text CAPTCHAs to help guard against spam bots, we recommend ReCAPTCHA for stronger protection and larger feature set.

    \r\n
  • \r\n\r\n
  • \r\n

    Email

    Hubs use outgoing email for account setup and a couple other things. Outgoing email from an Amazon hosted server often gets marked as SPAM by many email providers. It is often necessary to setup your hub to use external email services (such as Mandrill) to ensure that email makes it to users\' inboxes.

    \r\n
  • \r\n\r\n\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n\r\n
\r\n

Ready to go? Jump to the administration or learn how to change this page.

\r\n
\r\n\r\n\r\n
\r\n
\r\n\r\n

Where to Find Help

\r\n\r\n

We try to make using and configuring a hub a simple, smooth process. Sometimes, however, you may have questions or issues not easily answered by the available documentation. If you need help or support while developing your hub, feel free to contact us.

\r\n\r\n
\r\n
\r\n
\r\n

I have a question!

\r\n\r\n

Have a question on how to do something? If the documentation doesn\'t seem to be of help, you can try asking the community.

\r\n
\r\n
\r\n
\r\n
\r\n

I have an idea!

\r\n\r\n

Think something can be done better or is missing? Post your ideas or feature requests. We'd love to hear from you.

\r\n
\r\n
\r\n
\r\n
\r\n

I have an error!

\r\n\r\n

We continually test and refine the code for an error-free experience but, sadly, we can make mistakes. If you found a bug, let us know.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
'"; + $query .= " WHERE `id` = '32' AND `alias` = 'gettingstarted'"; + $database->setQuery($query); + $database->query(); + + // disable components + $this->output->addLine('Disabling com_usage'); + $migration->disableComponent('com_usage'); + + $this->output->addLine('Disabling com_store'); + $migration->disableComponent('com_store'); + break; + + case 'default': + case 'vanilla': + case 'grape': + // Enable com_tools + $migration->enableComponent('com_tools'); + $this->output->addLine('Enabling com_tools'); + + // Enable tool-related modules + $migration->enableModule('mod_mytools'); + $this->output->addLine('Enabling mod_mytools'); + $migration->enableModule('mod_mycontributions'); + $this->output->addLine('Enabling mod_mycontributions'); + $migration->enableModule('mod_mysessions'); + $this->output->addLine('Enabling mod_mysessions'); + + $defaults = array( + '{"module":44,"col":1,"row":1,"size_x":1,"size_y":2}', + '{"module":35,"col":1,"row":3,"size_x":1,"size_y":2}', + '{"module":38,"col":1,"row":5,"size_x":1,"size_y":2}', + '{"module":39,"col":1,"row":7,"size_x":1,"size_y":2}', + '{"module":33,"col":2,"row":1,"size_x":1,"size_y":2}', + '{"module":42,"col":2,"row":3,"size_x":1,"size_y":2}', + '{"module":34,"col":2,"row":5,"size_x":1,"size_y":2}', + '{"module":41,"col":3,"row":1,"size_x":1,"size_y":2}', + '{"module":36,"col":3,"row":3,"size_x":1,"size_y":2}', + '{"module":37,"col":3,"row":5,"size_x":1,"size_y":2}' + ); + + $params = array( + "allow_customization" => "1", + "position" => "memberDashboard", + "defaults" => '[' . implode(',', $defaults) . ']' + ); + + $migration->savePluginParams('members', 'dashboard', $params); + $this->output->addLine('Restoring default members dashboard configuration'); + + // Update kb articles + $query = "UPDATE `#__faq_categories` SET `state` = 1 WHERE `alias` = 'tools'"; + $database->setQuery($query); + $database->query(); + $query = "UPDATE `#__faq` SET `state` = 1 WHERE `alias` = 'webdav'"; + $database->setQuery($query); + $database->query(); + $this->output->addLine('Restoring tool and webdav related KB articles'); + + // Set flavor param in welcome template + $welcome_params->set('flavor', ''); + if ($welcome_params->get('template', '') == '') + { + $welcome_params->set('template', 'hubbasic2013'); + } + $query = "UPDATE `#__template_styles` SET `params`=".$database->quote(json_encode($welcome_params->toArray()))." WHERE `template`='welcome';"; + $database->setQuery($query); + $database->query(); + $this->output->addLine('Unsetting flavor flag in welcome template'); + + // Set amazon template as default + $this->output->addLine('Setting amazon template for welcome page'); + $query = "UPDATE `#__template_styles` SET `home` = 1 where template = 'hubbasic2013' and `client_id` = 0"; + $database->setQuery($query); + $database->query(); + $query = "UPDATE `#__template_styles` SET `home` = 0 where template != hubbasic2013' and `client_id` = 0"; + $database->setQuery($query); + $database->query(); + + + // Add back tools resource type + $query = "SELECT * FROM `#__resource_types` WHERE `alias` = 'tools'"; + $database->setQuery($query); + if (!$database->loadObjectList()) + { + $query = "INSERT INTO `#__resource_types` VALUES ('7', 'tools', 'Tools', '27',"; + $query .= "'Simulation and modeling tools that can be accessed via a web browser.', '1',"; + $query .= "'poweredby=Powered by=textarea=0\ncredits=Credits=textarea=0\nsponsoredby=Sponsored by=textarea=0\nreferences=References=textarea=0',"; + $query .= "'plg_citations=1\nplg_questions=1\nplg_recommendations=1\nplg_related=1\nplg_reviews=1\nplg_usage=1\nplg_versions=1\nplg_favorite=1\nplg_share=1\nplg_wishlist=1\nplg_supportingdocs=1\nplg_about=0\nplg_abouttool=1')"; + $database->setQuery($query); + $database->query(); + $this->output->addLine('Adding tools resource type'); + } + + // Update default content page(s) + $this->output->addLine('Updating default content pages'); + $this->output->addLine('Updating content page id (22)'); + $query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/discover.css\"}\r\n
\r\n
\r\n

Do More

\r\n
\r\n\r\n
\r\n
\r\n

Resources

\r\n

Find the latest cutting-edge research in our resources.

\r\n
\r\n
\r\n
\r\n
\r\n

Citations

\r\n

See who has cited our content in their work.

\r\n
\r\n
\r\n
\r\n
\r\n

Tags

\r\n

Explore all our content through tags or even tag content yourself.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n
\r\n

Blog

\r\n

Read the latest entry or browse the archive for articles of interest.

\r\n
\r\n
\r\n
\r\n
\r\n

Wiki

\r\n

Browse our user-generated wiki pages or write your own.

\r\n
\r\n
\r\n
\r\n
\r\n

Feedback

\r\n

Like something? Having trouble? Let us know what you think!

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n

Services

\r\n
\r\n
\r\n
\r\n

Upload

\r\n

Publish your own tools, seminars, and other content on this site.

\r\n
\r\n
\r\n
\r\n
\r\n

Tool Forge

\r\n

The development area for simulation tools. Sign up and manage your own software project!

\r\n
\r\n
\r\n
\r\n
\r\n

Store

\r\n

Purchase items such as t-shirts using points you earn by helping out.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n

What\'s Happening

\r\n
\r\n
\r\n
\r\n

Events

\r\n

Find information about the many upcoming public meetings and scientific symposia.

\r\n
\r\n
\r\n
\r\n
\r\n

What\'s New

\r\n

Find the latest content posted on the site with our What\'s New section.

\r\n
\r\n
\r\n
\r\n
\r\n

Poll

\r\n

Respond to our poll questions and see what everyone else is thinking.

\r\n
\r\n
\r\n
'"; + $query .= " WHERE `id` = '22' AND `alias` = 'discover'"; + $database->setQuery($query); + $database->query(); + + $this->output->addLine('Updating content page id (32)'); + $query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/gettingstarted.css\"}\r\n\r\n
\r\n
\r\n

Getting To Know Your Hub

\r\n\r\n
\r\n
\r\n
\r\n

Utilize

\r\n\r\n

View tutorials and read about how to use the numerous features of a HUB. We show you how to add content, customize your dashboard, create groups, and more.

\r\n\r\n

User documentation

\r\n
\r\n
\r\n
\r\n
\r\n

Manage

\r\n\r\n

Read the manual for managing the content and functionality of a HUB. It progresses step-by-step through various common tasks and familiarizes you with the administrative interface.

\r\n\r\n

Manager documentation

\r\n
\r\n
\r\n
\r\n
\r\n

Extend

\r\n\r\n

Build your own extensions and discover how to extend or tailor the existing ones to your needs. We try to guide you through the creation steps and provide examples for download.

\r\n\r\n

Developer documentation

\r\n
\r\n
\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n\r\n

Setting Up Your Hub

\r\n

We\'ve provided a default set of content with a menu of commonly used extensions and pages but there is still some setup and spots needing filled in that we, unfortunately, couldn\'t do for you.

\r\n\r\n
\r\n
\r\n

To-Do List

\r\n
    \r\n
  • \r\n

    About You

    \r\n

    Here\'s a page for telling your story and letting your visitors know all about you. You just have to fill in the details!

    \r\n
  • \r\n
  • \r\n

    How to Contact

    \r\n

    Sometimes your visitors will need to get ahold of you. We\'ve provided a page to detail contact information to help make that happen.\"

    \r\n
  • \r\n
  • \r\n

    Terms of Use

    \r\n

    Although we provided a generic Terms of Use, it will need some information filled in and further refinement for your hub.

    \r\n
  • \r\n
\r\n
\r\n
\r\n

Recommended

\r\n
    \r\n
  • \r\n

    Facebook, Google, LinkedIn

    \r\n

    Every hub comes with the ability for users to authenticate with popular services like Facebook, Google, or LinkedIn. Turn one or all of these on with a click of a button!

    \r\n
  • \r\n
  • \r\n

    Set up Analytics

    \r\n

    We highly recommend setting up Google Analytics on your hub. Luckily, we provide a module for doing just that.

    \r\n
  • \r\n
  • \r\n

    Use ReCAPTCHA

    \r\n

    While we provide basic image and text CAPTCHAs to help guard against spam bots, we recommend ReCAPTCHA for stronger protection and larger feature set.

    \r\n
  • \r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n\r\n
\r\n

Ready to go? Jump to the administration or learn how to change this page.

\r\n
\r\n\r\n\r\n
\r\n
\r\n\r\n

Where to Find Help

\r\n\r\n

We try to make using and configuring a hub a simple, smooth process. Sometimes, however, you may have questions or issues not easily answered by the available documentation. If you need help or support while developing your hub, feel free to contact us.

\r\n\r\n
\r\n
\r\n
\r\n

I have a question!

\r\n\r\n

Have a question on how to do something? If the documentation doesn\'t seem to be of help, you can try asking the community.

\r\n
\r\n
\r\n
\r\n
\r\n

I have an idea!

\r\n\r\n

Think something can be done better or is missing? Post your ideas or feature requests. We'd love to hear from you.

\r\n
\r\n
\r\n
\r\n
\r\n

I have an error!

\r\n\r\n

We continually test and refine the code for an error-free experience but, sadly, we can make mistakes. If you found a bug, let us know.

\r\n
\r\n
\r\n
\r\n\r\n
\r\n
'"; + $query .= " WHERE `id` = '32' AND `alias` = 'gettingstarted'"; + $database->setQuery($query); + $database->query(); + + // disable/enable components + $this->output->addLine('Enabling com_usage'); + $migration->enableComponent('com_usage'); + + $this->output->addLine('Enabling com_store'); + $migration->enableComponent('com_store'); + + break; + + default: + $this->output->error('Flavor provided is unknown.'); + break; + } + + $this->output->addLine("Successfully updated to the {$flavor} flavor!", 'success'); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this->output + ->getHelpOutput() + ->addOverview( + 'Repository management functions used to set the "flavor" of the hub. + Use this command to setup/convert your hub to one of the predefined + flavors. This often includes configuration changes and enabling/disabling + certain components based on the needs and limitations of the given + environement.' + ) + ->noArgsSection() + ->addSection('Usage') + ->addArgument( + 'muse repository:flavor set [flavor_name]' + ) + ->addSpacer() + ->addSection('Flavors') + ->addArgument( + 'amazon', + 'This flavor customizes the hub uniquely for use in the Amazon EC2 + environement. This primarily includes disabling tools and tool related + functions and content.' + ) + ->addArgument( + 'default', + 'This is the default hub install.' + ) + ->render(); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Repository/Package.php b/core/libraries/Hubzero/Console/Command/Repository/Package.php new file mode 100644 index 00000000000..e1a196f8899 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Repository/Package.php @@ -0,0 +1,161 @@ +output = $this->output->getHelpOutput(); + $this->help(); + $this->output->render(); + return; + } + + /** + * Installs the package repository + * + * @museDescription Installs and/or updates packages required by the composer.lock file + * @museArgument 3 The installation environment [development|production] + * @museArgument github-user The GitHub username to use when configuring the development environment + * + * @return void + **/ + public function install() + { + $configuration = $this->arguments->getOpt(3, 'production'); + + $args = []; + + switch ($configuration) + { + case 'development': + case 'staging': + $args[] = '--prefer-source'; + break; + + case 'production': + default: + $args[] = '--no-dev'; + break; + } + + // Composer install + if ($this->output->getMode() != 'minimal') + { + $this->output->addString('Installing any missing libraries from composer...', 'info'); + } + + $cmd = 'php ' . PATH_CORE . DS . 'bin' . DS . 'composer --working-dir=' . PATH_CORE . ' install ' . implode(' ', $args) . ' 2>&1'; + exec($cmd, $output, $status); + + // Composer install + if ($this->output->getMode() != 'minimal') + { + if ($status === 0) + { + $this->output->addLine('complete', 'success'); + } + else + { + $this->output->error('failed'); + } + } + else + { + if ($status !== 0) + { + $this->output->error('Failed to update package repository!'); + } + } + + if ($configuration == 'development' || $configuration == 'staging') + { + $this->configure(); + } + + // Composer install + if ($this->output->getMode() != 'minimal') + { + $this->output->addLine('Installation complete!', 'success'); + } + } + + /** + * Configures the package repository setup for use in a given environment + * + * @museDescription Configures the package repository for use in a given environment + * @museArgument github-user The GitHub username to use when configuring the development environment + * + * @return void + **/ + public function configure() + { + $gitHubUser = $this->arguments->getOpt('github-user', null); + + // Offer suggestion if username wasn't provided + if (is_null($gitHubUser)) + { + $gitHubUser = exec('whoami'); + + if ($this->output->getMode() != 'minimal') + { + $this->output->addLine("Assuming {$gitHubUser} as your GitHub username. To override, please specify the '--github-user' flag", 'info'); + } + } + else + { + if ($this->output->getMode() != 'minimal') + { + $this->output->addLine("Using the provided GitHub username: {$gitHubUser}", 'info'); + } + } + + // Escape user input + $gitHubUser = escapeshellarg($gitHubUser); + + // Update GIT config within vendor to point to developer fork of primary repo + if ($this->output->getMode() != 'minimal') + { + $this->output->addLine('Updating the framework repository to point to your GitHub fork', 'success'); + } + + $workTree = PATH_CORE . DS . 'vendor' . DS . 'hubzero' . DS . 'framework'; + $dir = $workTree . DS . '.git'; + $cmd = "git --git-dir={$dir} --work-tree={$workTree} remote set-url --push origin git@github.com:{$gitHubUser}/framework.git 2>&1"; + $result = shell_exec($cmd); + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Repository management functions for composer packages.' + ) + ->addTasks($this); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding.php b/core/libraries/Hubzero/Console/Command/Scaffolding.php new file mode 100644 index 00000000000..dbd8789b347 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding.php @@ -0,0 +1,508 @@ +type = $this->arguments->getOpt(3); + } + + /** + * Default (required) command + * + * Generates list of available commands and their respective tasks + * + * @return void + **/ + public function execute() + { + $this->help(); + } + + /** + * Help doc for scaffolding command + * + * @return void + **/ + public function help() + { + if ($this->type) + { + $class = __NAMESPACE__ . '\\Scaffolding\\' . ucfirst($this->type); + $obj = new $class($this->output, $this->arguments); + + // Call the help method + $obj->help(); + } + else + { + $this->output = $this->output->getHelpOutput(); + $this + ->output + ->addOverview( + 'Create a new item scaffold. There are currently no arguments available. + Type "muse scaffolding help [scaffolding type]" for more details.'); + $this->output->render(); + } + } + + /** + * Create a new item from scaffolding templates + * + * @return void + **/ + public function create() + { + $class = __NAMESPACE__ . '\\Scaffolding\\' . ucfirst($this->type); + + if (class_exists($class)) + { + $obj = new $class($this->output, $this->arguments); + } + else + { + if (empty($this->type)) + { + $this->output->error('Error: Sorry, scaffolding can\'t create nothing/everything. Try telling it what you want to create.'); + } + else + { + $this->output->error('Error: Sorry, scaffolding doesn\'t know how to create a ' . $this->type); + } + } + + // Get author name and email - we'll go ahaed and retrieve for all create calls + $user_name = Config::get('user_name'); + $user_email = Config::get('user_email'); + + if (!$user_name || !$user_email) + { + $this->output + ->addSpacer() + ->addLine('You can specify your name and email via:') + ->addLine( + 'muse configuration set --user_name="John Doe"', + array( + 'indentation' => '2', + 'color' => 'blue' + ) + ) + ->addLine( + 'muse configuration set --user_email=john.doe@gmail.com', + array( + 'indentation' => '2', + 'color' => 'blue' + ) + ) + ->addSpacer() + ->error("Error: failed to retrieve author name and/or email."); + } + + $obj->addReplacement('author_name', $user_name) + ->addReplacement('author_email', $user_email); + + // Call the construct method + $obj->construct(); + } + + /** + * Copy item and attempt to rename appropriatly + * + * @return void + **/ + public function copy() + { + $class = __NAMESPACE__ . '\\Scaffolding\\' . ucfirst($this->type); + + if (class_exists($class)) + { + $obj = new $class($this->output, $this->arguments); + } + else + { + if (empty($this->type)) + { + $this->output->error('Error: Sorry, scaffolding can\'t copy nothing. Try telling it what you want to copy.'); + } + else + { + $this->output->error('Error: Sorry, scaffolding doesn\'t know how to copy a ' . $this->type); + } + } + + if (!method_exists($obj, 'doCopy')) + { + $this->output->error('Error: scaffolding doesn\'t know how to copy a ' . $this->type); + } + + // Do the actual copy + $obj->doCopy(); + } + + /** + * Get the type of template we're making + * + * @return string + **/ + protected function getType() + { + return $this->type; + } + + /** + * Set blind replacement var + * + * @return $this + **/ + protected function doBlindReplacements() + { + $this->doBlindReplacements = true; + + return $this; + } + + /** + * Make template + * + * @return void + **/ + protected function make() + { + if (count($this->templateFiles) > 0) + { + foreach ($this->templateFiles as $template) + { + if (is_dir($template['path'])) + { + if (!Filesystem::copyDirectory($template['path'], $template['destination'])) + { + $this->output->error("Error: an problem occured copying {$template['path']} to {$template['destination']}."); + } + + // Get folder contents + $this->scanFolder($template['destination']); + } + elseif (is_file($template['path'])) + { + if (!copy($template['path'], $template['destination'])) + { + $this->output->error("Error: an problem occured copying {$template['path']} to {$template['destination']}."); + } + + // Get template contents + $contents = file_get_contents($template['destination']); + $contents = $this->doReplacements($contents); + + // Write file + $this->putContents($template['destination'], $contents); + } + } + } + } + + /** + * Add a new replacement for the template + * + * @param string $key The replacement key + * @param string $value The replacement value + * @return $this + **/ + protected function addReplacement($key, $value) + { + $this->replacements[$key] = $value; + + return $this; + } + + /** + * Add a new template file + * + * @param string $filename The template filename + * @param string $destination Final location of template file after making + * @param bool $fullPath True if full path is given + * @return $this + **/ + protected function addTemplateFile($filename, $destination, $fullPath = false) + { + $this->templateFiles[] = array( + 'path' => ((!$fullPath) ? __DIR__ . DS . 'Scaffolding' . DS . 'Templates' . DS . $filename : $filename), + 'destination' => $destination + ); + + return $this; + } + + /** + * Make replacements in a given content string + * + * @param string $contents Incoming content + * @return string + **/ + private function doReplacements($contents) + { + // Replace variables + if (isset($this->replacements) && count($this->replacements) > 0) + { + foreach ($this->replacements as $k => $v) + { + if (is_array($v)) + { + foreach ($v as $replacement) + { + foreach ($replacement as $key => $value) + { + $contents = preg_replace("/%={$key}=%/", $value, $contents, 1); + } + } + } + else + { + // See if there are any instances of our key with special qualifiers + if (preg_match_all("/%={$k}\+([[:alpha:]]+)=%/", $contents, $matches) && isset($matches[1])) + { + // Remove complete match + unset($matches[0]); + foreach ($matches[1] as $match) + { + switch ($match) + { + // Upper case word + case 'uc': + $value = strtoupper($v); + break; + // Upper case first character + case 'ucf': + $value = ucfirst($v); + break; + // Upper case first character and plural + case 'ucfp': + $value = ucfirst(Inflector::pluralize($v)); + break; + // Plural form + case 'p': + $value = Inflector::pluralize($v); + break; + } + + $contents = str_replace("%={$k}+{$match}=%", $value, $contents); + } + } + + // Now do all basic replacements + $contents = str_replace("%={$k}=%", $v, $contents); + + if ($this->doBlindReplacements) + { + $contents = str_replace($k, $v, $contents); + } + } + } + } + + // Now parse for nested templates + preg_match_all('/\$\^[[:alpha:]\.]*\^\$/', $contents, $matches, PREG_SET_ORDER); + + if (isset($matches) && count($matches) > 0) + { + foreach ($matches as $match) + { + foreach ($match as $key) + { + // Get template contents + $keyReal = preg_replace('/\$\^([[:alpha:]\.]*)\^\$/', '$1', $key); + $subPath = __DIR__ . DS . 'Scaffolding' . DS . 'Templates' . DS . $this->getType() . '.' . $keyReal . '.tmpl'; + + if (!is_file($subPath)) + { + continue; + } + + $subTmpl = file_get_contents($subPath); + + if (isset($this->replacements[$keyReal]) && is_array($this->replacements[$keyReal])) + { + $count = count($this->replacements[$keyReal]); + $repeat = str_repeat($key, $count); + $contents = str_replace($key, $repeat, $contents); + } + + $contents = str_replace($key, $subTmpl, $contents); + $contents = $this->doReplacements($contents); + } + } + } + + return $contents; + } + + /** + * Write contents out to file + * + * @param string $path Location of file to put contents + * @param string $contents Contents to write to file + * @return void + **/ + private function putContents($path, $contents) + { + file_put_contents($path, $contents); + + // See if we have a .tmpl at the end that we need to remove. + if (substr($path, -5) == '.tmpl') + { + rename($path, substr($path, 0, -5)); + $path = substr($path, 0, -5); + } + + $info = pathinfo($path); + + // See if we need to do var replacement in actual filename + if (preg_match("/%=([[:alpha:]_]*)(\+[[:alpha:]]+)?=%/", $info['filename'], $matches) && isset($this->replacements[$matches[1]])) + { + $newfile = str_replace($matches[0], $this->replacements[$matches[1]], $info['filename']); + + if (isset($matches[2])) + { + $modifier = substr($matches[2], 1); + switch ($modifier) + { + // Upper case word + case 'uc': + $value = strtoupper($v); + break; + // Upper case first character + case 'ucf': + $newfile = ucfirst($newfile); + break; + // Upper case first character and plural + case 'ucfp': + $newfile = ucfirst(Inflector::pluralize($newfile)); + break; + // Plural form + case 'p': + $newfile = Inflector::pluralize($newfile); + break; + } + } + + rename($path, $info['dirname'] . DS . $newfile . '.' . $info['extension']); + + $path = $info['dirname'] . DS . $newfile . '.' . $info['extension']; + } + + $this->output->addLine("Creating {$path}", 'success'); + } + + /** + * Scan template folder for files to iterate through + * + * @param string $path Path of folder to scan + * @return void + **/ + private function scanFolder($path) + { + $files = array_diff(scandir($path), array('.', '..', '.DS_Store')); + + if ($files && count($files) > 0) + { + foreach ($files as $file) + { + if (is_file($path . DS . $file)) + { + $contents = file_get_contents($path . DS . $file); + $contents = $this->doReplacements($contents); + + $this->putContents($path . DS . $file, $contents); + } + else + { + // See if we need to do var replacement in directory name + if (preg_match("/%=([[:alpha:]_]*)(\+[[:alpha:]]+)?=%/", $path . DS . $file, $matches) && isset($this->replacements[$matches[1]])) + { + $newfile = str_replace($matches[0], $this->replacements[$matches[1]], $file); + + if (isset($matches[2])) + { + $modifier = substr($matches[2], 1); + switch ($modifier) + { + // Upper case word + case 'uc': + $value = strtoupper($v); + break; + // Upper case first character + case 'ucf': + $newfile = ucfirst($newfile); + break; + // Upper case first character and plural + case 'ucfp': + $newfile = ucfirst(Inflector::pluralize($newfile)); + break; + // Plural form + case 'p': + $newfile = Inflector::pluralize($newfile); + break; + } + } + + rename($path. DS . $file, $path . DS . $newfile); + $file = $newfile; + } + + $this->scanFolder($path . DS . $file); + } + } + } + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Command.php b/core/libraries/Hubzero/Console/Command/Scaffolding/Command.php new file mode 100644 index 00000000000..47d4670bdae --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Command.php @@ -0,0 +1,77 @@ +arguments->getOpt('n') || $this->arguments->getOpt('name') || $this->arguments->getOpt(4)) + { + // Set name, according to priority of inputs + $name = ($this->arguments->getOpt(4)) ? $this->arguments->getOpt(4) : $name; + $name = ($this->arguments->getOpt('n')) ? $this->arguments->getOpt('n') : $name; + $name = ($this->arguments->getOpt('name')) ? $this->arguments->getOpt('name') : $name; + $name = strtolower($name); + } + else + { + // If name wasn't provided, and we're in interactive mode...ask for it + if ($this->output->isInteractive()) + { + $name = $this->output->getResponse('What do you want the command name to be?'); + } + else + { + $this->output->error("Error: a command name should be provided."); + } + } + + // Define our install directory or get it from args + $dest = PATH_CORE . DS . 'libraries' . DS . 'Hubzero' . DS . 'Console' . DS . 'Command' . DS . ucfirst($name) . '.php'; + + // Make command + $this->addTemplateFile("{$this->getType()}.tmpl", $dest) + ->addReplacement('command_name', $name) + ->make(); + } + + /** + * Help doc for command scaffolding class + * + * @return void + **/ + public function help() + { + $this->output + ->addOverview( + 'Create a new console command.' + ) + ->addArgument( + '-n, --name: command name', + 'Give the command name. The command name can also be provided + as the next word following the command as shown here: + "muse scaffolding create command awesome"', + 'Example: -n=awesome, --name=awesomer' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Component.php b/core/libraries/Hubzero/Console/Command/Scaffolding/Component.php new file mode 100644 index 00000000000..e9192fcb869 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Component.php @@ -0,0 +1,101 @@ +arguments->getOpt('n') || $this->arguments->getOpt('name') || $this->arguments->getOpt(4)) + { + // Set name, according to priority of inputs + $name = ($this->arguments->getOpt(4)) ? $this->arguments->getOpt(4) : $name; + $name = ($this->arguments->getOpt('n')) ? $this->arguments->getOpt('n') : $name; + $name = ($this->arguments->getOpt('name')) ? $this->arguments->getOpt('name') : $name; + $name = strtolower($name); + } + else + { + // If name wasn't provided, and we're in interactive mode...ask for it + if ($this->output->isInteractive()) + { + $name = $this->output->getResponse('What do you want the component name to be?'); + } + else + { + $this->output->error("Error: a component name should be provided."); + } + } + + // Define our install directory or get it from args + $install_dir = PATH_CORE . DS . 'components'; + if ($this->arguments->getOpt('install-dir') && strlen(($this->arguments->getOpt('install-dir'))) > 0) + { + // @FIXME: need to be able to distinguish between path_app and path_core here + $install_dir = PATH_CORE . DS . trim($this->arguments->getOpt('install-dir'), DS) . DS . 'components'; + } + + if (substr($name, 0, 4) == 'com_') + { + $name = substr($name, 4); + } + + // Make sure component doesn't already exist + if (is_dir($install_dir . DS . 'com_' . $name)) + { + $this->output->error("Error: the component name provided ({$name}) seems to already exists."); + } + + // Make component + $this->addTemplateFile("{$this->getType()}.tmpl", $install_dir . DS . 'com_' . $name) + ->addReplacement('component_name', $name) + ->addReplacement('option', 'com_' . $name) + ->make(); + } + + /** + * Help doc for component scaffolding class + * + * @return void + **/ + public function help() + { + $this->output + ->addOverview( + 'Create a new component.' + ) + ->addArgument( + '-n, --name: component name', + 'Give the component name. The component name can also be provided + as the next word following the command as shown here: + "muse scaffolding create component awesome"', + 'Example: -n=awesome, --name=awesomer' + ) + ->addArgument( + '--install-dir: installation directory', + 'Directory in which the component should be installed. Can be helpful + when installing a component in some sort of subsite or alternate + configuration. Scaffolding with use PATH_CORE as the default.', + 'Example: --install-dir=site/groups/1987' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Migration.php b/core/libraries/Hubzero/Console/Command/Scaffolding/Migration.php new file mode 100644 index 00000000000..f4259faaef5 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Migration.php @@ -0,0 +1,311 @@ +arguments->getOpt(4)) + { + case 'for': + $prefix = App::get('db')->getPrefix(); + $tables = App::get('db')->getTableList(); + + if (!$table = $this->arguments->getOpt(5)) + { + $this->output->error('Please specify the table for which a migration is being created'); + } + else if (!in_array($table, $tables)) + { + $this->output->error('Table does not exist'); + } + + $this->addReplacement('description', "creating table {$table}") + ->addReplacement('table_name', str_replace($prefix, '#__', $table)) + ->addReplacement('up', '$^create.table^$') + ->addReplacement('down', '$^drop.table^$') + ->addReplacement('create_table', $this->showCreateTable($table)); + break; + + default: + $this->addReplacement('description', '...') + ->addReplacement('up', '') + ->addReplacement('down', ''); + break; + } + + // Determine our base path + $base = $this->arguments->getOpt('app') ? PATH_APP : PATH_CORE; + $installDir = trim($this->arguments->getOpt('install-dir')); + if ($installDir && strlen($installDir) > 0) + { + if (substr($installDir, 0, 1) == DS) + { + $base = rtrim($installDir, DS); + } + else + { + $base .= DS . trim($installDir, DS); + } + } + + // Install directory is migrations folder within base + $installDir = $base . DS . 'migrations'; + + // Extension + $extension = null; + if ($this->arguments->getOpt('e') || $this->arguments->getOpt('extension')) + { + $extension = ($this->arguments->getOpt('e')) ? $this->arguments->getOpt('e') : $this->arguments->getOpt('extension'); + + if ($extension != 'core' && !$this->isValidExtension($extension, $base) && !$this->arguments->getOpt('i')) + { + $this->output->error("Error: the extension provided ({$extension}) does not appear to be valid."); + } + } + else + { + $this->output->error("Error: an extension should be provided."); + } + + // Editor + $editor = null; + if ($this->arguments->getOpt('editor')) + { + $editor = $this->arguments->getOpt('editor'); + } + else + { + $editor = (getenv('EDITOR')) ? getenv('EDITOR') : 'vi'; + } + + // Create filename varient of extension + $ext = ''; + if (!preg_match('/core/i', $extension)) + { + $parts = explode('_', $extension); + foreach ($parts as $part) + { + $ext .= ucfirst($part); + } + } + else + { + $ext = 'Core'; + } + + // Craft file/classname + $classname = 'Migration' . with(new Date('now'))->format("YmdHis") . $ext; + $destination = $installDir . DS . $classname . '.php'; + + $this->addTemplateFile("{$this->getType()}.tmpl", $destination) + ->addReplacement('class_name', $classname) + ->make(); + + // Open in editor + system("{$editor} {$destination} > `tty`"); + } + + /** + * Simple helper function to check validity of provided extension name + * + * @param string $extension Extension name to evaluate + * @param string $base Directory to examine + * @return bool + **/ + private function isValidExtension($extension, $base) + { + $ext = explode('_', $extension); + $dir = ''; + + switch ($ext[0]) + { + case 'com': + $dir = $base . DS . 'components' . DS . $extension; + break; + case 'mod': + $dir = $base . DS . 'modules' . DS . $extension; + break; + case 'plg': + $dir = $base . DS . 'plugins' . DS . $ext[1] . DS . $ext[2]; + break; + default: + return false; + break; + } + + return (is_dir($dir)) ? true : false; + } + + /** + * Get table creation string + * + * @param string $tableName The table name for which to retrieve create syntax + * @return string + **/ + private function showCreateTable($tableName) + { + $prefix = App::get('db')->getPrefix(); + + $create = App::get('db')->getTableCreate($tableName); + $create = $create[$tableName]; + $create = str_replace("CREATE TABLE `{$prefix}", 'CREATE TABLE `#__', $create); + $create = str_replace("\n", "\n\t\t\t\t", $create); + $create = preg_replace('/(AUTO_INCREMENT=)([0-9]*)/', '${1}0', $create); + + return $create; + } + + /** + * Help doc for migration scaffolding class + * + * @return void + **/ + public function help() + { + $this->output + ->addOverview( + 'Create a migration script from the default template. An + extension must be provided.' + ) + ->addArgument( + '-e, --extension: extension', + 'Specify the extension for which you are creating a migration + script. Those scripts not pertaining to a specific extension + should be given the extension "core"', + 'Example: -e=com_courses, --extension=plg_members_dashboard', + true + ) + ->addArgument( + '-i: ignore validity check', + 'Normally, migrations scaffolding tries to check the validity of the provided + extension name by checking for the existance of a corresponding + directory within the framework. Occasionally, migrations need to be + written for non-existent extensions. This option will override the + validity check and allow you to create the migration anyways.', + 'Example: -i' + ) + ->addArgument( + '--install-dir: installation directory', + 'Installation/base directory within which the migration will be installed. + By default, this will be PATH_CORE. The command will then look for a + directory named "migrations" within the provided installation directory.', + 'Example: --install-dir=/www/myhub' + ) + ->addArgument( + '--app: use app as the base path, rather than core', + 'Use the app, rather than the core directory. This will effect both the + question of whether or not the provided extension appears to be valid, + as well as where the migration will be saved.', + 'Example: --app' + ) + ->addArgument( + '--editor: editor', + 'Specify the editor to use when creating the migration file. + You\'ll be dropped into this editor after scaffolding pre-populates + everything it can', + 'Example: --editor=nano' + ) + ->addSection( + 'Migration methods (available within a migration)' + ) + ->addParagraph( + 'Migrations have several common methods available to the creator of the migration. + These are listed below. The methods below that are intended to + display output should be passed through the callback() function + so that it can make sure the migration is running in interactive + mode and everything is properly set up. The examples indicate + proper use of each method.', + array( + 'indentation' => 2 + ) + ) + ->addSpacer() + ->addArgument( + 'addComponentEntry($name, $option=NULL, $enabled=1, $params=\'\', $createMenuItem=true)', + 'Adds a new component entry to the database, creating it only if + needed. Params should be JSON encoded.', + 'Example: $this->addComponentEntry(\'com_awesome\');' + ) + ->addArgument( + 'addPluginEntry($folder, $element, $enabled=1, $params=\'\')', + 'Adds a new plugin entry to the database, creating it only if + needed. Params should be JSON encoded.', + 'Example: $this->addPluginEntry(\'groups\', \'members\');' + ) + ->addArgument( + 'addModuleEntry($element, $enabled=1, $params=\'\')', + 'Adds a new module entry to the database, creating it only if + needed. Params should be JSON encoded.', + 'Example: $this->addModuleEntry(\'mod_awesome\');' + ) + ->addArgument( + 'deleteComponentEntry($name)', + 'Removes a component entry by name from the database.', + 'Example: $this->deleteComponentEntry(\'com_awesome\');' + ) + ->addArgument( + 'deletePluginEntry($folder, $element=NULL)', + 'Removes a plugin entry by name from the database. Leaving the element + argument empty will delete all plugins for the specified folder.', + 'Example: $this->deleteComponentEntry(\'groups\', \'members\');' + ) + ->addArgument( + 'deleteModuleEntry($element)', + 'Removes a module entry by name from the database.', + 'Example: $this->deleteModuleEntry(\'mod_awesome\');' + ) + ->addArgument( + 'enablePlugin($folder, $element)', + 'Enables (turns on) a plugin.', + 'Example: $this->enablePlugin(\'groups\', \'members\');' + ) + ->addArgument( + 'disablePlugin($folder, $element)', + 'Disables (turns off) a plugin.', + 'Example: $this->disablePlugin(\'groups\', \'members\');' + ) + ->addArgument( + 'progress:init', + 'Initialize a progress tracker. Can provide one argument to the + method giving a message that will be displayed before the + percentage counter.', + 'Example: $this->callback(\'progress\', \'init\', array(\'Running \' . __CLASS__ . \'.php:\'));' + ) + ->addArgument( + 'progress:setProgress', + 'Update the current progress value. Should provide one argument + specifying the current progress value [(int) 1 - 100].', + 'Example: $this->callback(\'progress\', \'setProgress\', array($i));' + ) + ->addArgument( + 'progress:done', + 'Terminate the progress tracker. This will back the cursor up + to the beginning of the line so future text can overwrite it. + In the case of migrations, this will likely mean that the line + indicating successful completion of the file will be shown. + No arguments are expected.', + 'Example: $this->callback(\'progress\', \'done\');' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/SearchPlugin.php b/core/libraries/Hubzero/Console/Command/Scaffolding/SearchPlugin.php new file mode 100644 index 00000000000..01ad31b0b68 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/SearchPlugin.php @@ -0,0 +1,216 @@ +arguments->getOpt(4)) + { + $extension = $this->arguments->getOpt(4); + $this->addReplacement('plugin_name', $extension); + $this->addReplacement('extension', $extension); + } + else + { + $this->output->error("Error: must specify a hubtype."); + } + + // Determine our base path + $base = $this->arguments->getOpt('app') ? PATH_APP : PATH_CORE; + $installDir = trim($this->arguments->getOpt('install-dir')); + if ($installDir && strlen($installDir) > 0) + { + if (substr($installDir, 0, 1) == DS) + { + $base = rtrim($installDir, DS); + } + else + { + $base .= DS . trim($installDir, DS); + } + } + + // Install directory is migrations folder within base + $installDir = $base . DS . 'plugins' . DS . 'search'; + + // Editor + $editor = null; + if ($this->arguments->getOpt('editor')) + { + $editor = $this->arguments->getOpt('editor'); + } + else + { + $editor = (getenv('EDITOR')) ? getenv('EDITOR') : 'vi'; + } + + // @TODO detect previous file, warn about override + + // Make plugin file + $classname = 'plgSearch' . ucfirst($extension); + $destination = $installDir . DS . $extension . DS . $extension. '.php'; + + // Make directory + if (!is_dir($installDir. DS . $extension)) + { + App::get('filesystem')->makeDirectory($installDir . DS . $extension); + } + + $this->addTemplateFile("{$this->getType()}.tmpl", $destination) + ->addReplacement('class_name', $classname) + ->make(); + } + + /** + * Help doc for migration scaffolding class + * + * @return void + **/ + public function help() + { + $this->output + ->addOverview( + 'Create a migration script from the default template. An + extension must be provided.' + ) + ->addArgument( + '-e, --extension: extension', + 'Specify the extension for which you are creating a migration + script. Those scripts not pertaining to a specific extension + should be given the extension "core"', + 'Example: -e=com_courses, --extension=plg_members_dashboard', + true + ) + ->addArgument( + '-i: ignore validity check', + 'Normally, migrations scaffolding tries to check the validity of the provided + extension name by checking for the existance of a corresponding + directory within the framework. Occasionally, migrations need to be + written for non-existent extensions. This option will override the + validity check and allow you to create the migration anyways.', + 'Example: -i' + ) + ->addArgument( + '--install-dir: installation directory', + 'Installation/base directory within which the migration will be installed. + By default, this will be PATH_CORE. The command will then look for a + directory named "migrations" within the provided installation directory.', + 'Example: --install-dir=/www/myhub' + ) + ->addArgument( + '--app: use app as the base path, rather than core', + 'Use the app, rather than the core directory. This will effect both the + question of whether or not the provided extension appears to be valid, + as well as where the migration will be saved.', + 'Example: --app' + ) + ->addArgument( + '--editor: editor', + 'Specify the editor to use when creating the migration file. + You\'ll be dropped into this editor after scaffolding pre-populates + everything it can', + 'Example: --editor=nano' + ) + ->addSection( + 'Migration methods (available within a migration)' + ) + ->addParagraph( + 'Migrations have several common methods available to the creator of the migration. + These are listed below. The methods below that are intended to + display output should be passed through the callback() function + so that it can make sure the migration is running in interactive + mode and everything is properly set up. The examples indicate + proper use of each method.', + array( + 'indentation' => 2 + ) + ) + ->addSpacer() + ->addArgument( + 'addComponentEntry($name, $option=NULL, $enabled=1, $params=\'\', $createMenuItem=true)', + 'Adds a new component entry to the database, creating it only if + needed. Params should be JSON encoded.', + 'Example: $this->addComponentEntry(\'com_awesome\');' + ) + ->addArgument( + 'addPluginEntry($folder, $element, $enabled=1, $params=\'\')', + 'Adds a new plugin entry to the database, creating it only if + needed. Params should be JSON encoded.', + 'Example: $this->addPluginEntry(\'groups\', \'members\');' + ) + ->addArgument( + 'addModuleEntry($element, $enabled=1, $params=\'\')', + 'Adds a new module entry to the database, creating it only if + needed. Params should be JSON encoded.', + 'Example: $this->addModuleEntry(\'mod_awesome\');' + ) + ->addArgument( + 'deleteComponentEntry($name)', + 'Removes a component entry by name from the database.', + 'Example: $this->deleteComponentEntry(\'com_awesome\');' + ) + ->addArgument( + 'deletePluginEntry($folder, $element=NULL)', + 'Removes a plugin entry by name from the database. Leaving the element + argument empty will delete all plugins for the specified folder.', + 'Example: $this->deleteComponentEntry(\'groups\', \'members\');' + ) + ->addArgument( + 'deleteModuleEntry($element)', + 'Removes a module entry by name from the database.', + 'Example: $this->deleteModuleEntry(\'mod_awesome\');' + ) + ->addArgument( + 'enablePlugin($folder, $element)', + 'Enables (turns on) a plugin.', + 'Example: $this->enablePlugin(\'groups\', \'members\');' + ) + ->addArgument( + 'disablePlugin($folder, $element)', + 'Disables (turns off) a plugin.', + 'Example: $this->disablePlugin(\'groups\', \'members\');' + ) + ->addArgument( + 'progress:init', + 'Initialize a progress tracker. Can provide one argument to the + method giving a message that will be displayed before the + percentage counter.', + 'Example: $this->callback(\'progress\', \'init\', array(\'Running \' . __CLASS__ . \'.php:\'));' + ) + ->addArgument( + 'progress:setProgress', + 'Update the current progress value. Should provide one argument + specifying the current progress value [(int) 1 - 100].', + 'Example: $this->callback(\'progress\', \'setProgress\', array($i));' + ) + ->addArgument( + 'progress:done', + 'Terminate the progress tracker. This will back the cursor up + to the beginning of the line so future text can overwrite it. + In the case of migrations, this will likely mean that the line + indicating successful completion of the file will be shown. + No arguments are expected.', + 'Example: $this->callback(\'progress\', \'done\');' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Template.php b/core/libraries/Hubzero/Console/Command/Scaffolding/Template.php new file mode 100644 index 00000000000..0f89a5d65a7 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Template.php @@ -0,0 +1,116 @@ +arguments->getOpt(4) && $this->arguments->getOpt(5) && $this->arguments->getOpt(6)) + { + if ($this->arguments->getOpt(5) == 'to') + { + $from = strtolower($this->arguments->getOpt(4)); + $to = strtolower($this->arguments->getOpt(6)); + } + else if ($this->arguments->getOpt(5) == 'from') + { + $to = strtolower($this->arguments->getOpt(4)); + $from = strtolower($this->arguments->getOpt(6)); + } + } + else + { + // If name wasn't provided, and we're in interactive mode...ask for it + if ($this->output->isInteractive()) + { + $from = $this->output->getResponse('What template to you want to use as the source?'); + $to = $this->output->getResponse('What do you want to call the new template?'); + } + else + { + $this->output->error("Error: please provide the source template and destination template name"); + } + } + + // Normalize source and destination + $to = trim($to, DS); + $from = trim($from, DS); + $pathTo = PATH_CORE; + $pathFrom = PATH_CORE; + + preg_match('/(core|app)\/([[:alnum:]_-]*)/', $to, $matchesTo); + preg_match('/(core|app)\/([[:alnum:]_-]*)/', $from, $matchesFrom); + + if (isset($matchesTo[0])) + { + $to = $matchesTo[2]; + + if ($matchesTo[1] == 'app') + { + $pathTo = PATH_APP; + } + } + if (isset($matchesFrom[0])) + { + $from = $matchesFrom[2]; + + if ($matchesFrom[1] == 'app') + { + $pathFrom = PATH_APP; + } + } + + // Make sure template doesn't already exist + if (is_dir($pathTo . DS . 'templates' . DS . $to)) + { + $this->output->error("Error: the template destination alread exists."); + } + if (!is_dir($pathFrom . DS . 'templates' . DS . $from)) + { + $this->output->error("Error: the template source does not appear to exist."); + } + + // Make component + $this->addTemplateFile($pathFrom . DS . 'templates' . DS . $from, $pathTo . DS . 'templates' . DS . $to, true) + ->addTemplateFile($pathFrom . DS . 'language' . DS . 'en-GB' . DS . 'en-GB.tpl_' . $from . '.ini', $pathTo . DS . 'language' . DS . 'en-GB' . DS . 'en-GB.tpl_' . $to . '.ini', true) + ->addReplacement(strtoupper($from), strtoupper($to)) + ->addReplacement(ucfirst($from), ucfirst($to)) + ->addReplacement($from, $to) + ->doBlindReplacements() + ->make(); + } + + /** + * Help doc for component scaffolding class + * + * @return void + **/ + public function help() + { + $this->output + ->addOverview( + 'Scaffolding for templates' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/%=extension=%.php.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/%=extension=%.php.tmpl new file mode 100644 index 00000000000..b1ddab9f658 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/%=extension=%.php.tmpl @@ -0,0 +1,99 @@ +hubtype) + { + return $this->hubtype; + } + elseif (!isset($type)) + { + return $this->hubtype; + } + } + + /** + * onGetModel + * + * @param string $type + * @access public + * @return void + */ + public function onGetModel($type = '') + { + if ($type == $this->hubtype) + { + return new ; + } + } + + /** + * onProcessFields - Set SearchDocument fields which have conditional processing + * + * @param mixed $type + * @param mixed $row + * @access public + * @return void + */ + public function onProcessFields($type, $row, &$db) + { + if ($type == $this->hubtype) + { + // Instantiate new $fields object + $fields = new stdClass; + + // Format the date for SOLR + $date = Date::of($row->created)->format('Y-m-d'); + $date .= 'T'; + $date .= Date::of($row->created)->format('h:m:s') . 'Z'; + $fields->date = $date; + + // Title is required + $fields->title = $title; + + /** + * Each entity should have an owner. + * Owner type can be a user or a group, + * where the owner is the ID of the user or group + **/ + $fields->owner_type = 'user'; + $fields->owner = $owners; + + /** + * A document should have an access level. + * This value can be: + * public - all users can view + * registered - only registered users can view + * private - only owners (set above) can view + **/ + $fields->access_level = ''; + + // The URL this document is accessible through + $fields->url = ''; + + return $fields; + } + } +} + diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/%=extension=%.xml.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/%=extension=%.xml.tmpl new file mode 100755 index 00000000000..670a877301b --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/%=extension=%.xml.tmpl @@ -0,0 +1,19 @@ + + + %=friendlyGroup=% - %=friendlyExtension=% + %=author_name=% + Copyright (c) 2005-2020 The Regents of the University of California. + http://opensource.org/licenses/MIT MIT + %=description=% + + %=extension=%.php + + + en-GB.plg_%=group=%_%=extension=%.ini + + + + + + + diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/language/en-GB/en-GB.plg_search_%=extension=%.ini.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/language/en-GB/en-GB.plg_search_%=extension=%.ini.tmpl new file mode 100755 index 00000000000..ef154303702 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/SearchPlugin.tmpl/language/en-GB/en-GB.plg_search_%=extension=%.ini.tmpl @@ -0,0 +1,7 @@ +; @package hubzero-cms +; @copyright Copyright (c) 2005-2020 The Regents of the University of California. +; @license http://opensource.org/licenses/MIT MIT + +; Note : All ini files need to be saved as UTF-8 - No BOM + +PLG_SEARCH_MEMBERS="Search - Members" \ No newline at end of file diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/command.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/command.tmpl new file mode 100644 index 00000000000..1d64a9c1746 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/command.tmpl @@ -0,0 +1,37 @@ + + + %=component_name+ucf=% + HUBzero + Copyright (c) 2005-2020 The Regents of the University of California. + http://opensource.org/licenses/MIT MIT + + en-GB.%=option=%.ini + + + + + + + + + %=component_name+ucf=% + + access.xml + config.xml + %=component_name=%.php + %=component_name=%.xml + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/%=component_name=%.php.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/%=component_name=%.php.tmpl new file mode 100644 index 00000000000..fab131a8248 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/%=component_name=%.php.tmpl @@ -0,0 +1,24 @@ +execute(); +$controller->redirect(); diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/controllers/%=component_name=%.php.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/controllers/%=component_name=%.php.tmpl new file mode 100644 index 00000000000..b6032d70ca4 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/controllers/%=component_name=%.php.tmpl @@ -0,0 +1,33 @@ +getErrors() as $error) + { + $this->view->setError($error); + } + + // Output the HTML + $this->view->display(); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/language/en-GB/en-GB.%=option=%.ini.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/language/en-GB/en-GB.%=option=%.ini.tmpl new file mode 100644 index 00000000000..2bb9c7f793b --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/language/en-GB/en-GB.%=option=%.ini.tmpl @@ -0,0 +1,9 @@ +; @package hubzero-cms +; @copyright Copyright (c) 2005-2020 The Regents of the University of California. +; @license http://opensource.org/licenses/MIT MIT + +; Note : All ini files need to be saved as UTF-8 - No BOM + +%=option+uc=%="%=component_name+ucf=%" +%=option+uc=%_CONFIGURATION="%=component_name+ucf=% Configuration" + diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/language/en-GB/en-GB.%=option=%.sys.ini.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/language/en-GB/en-GB.%=option=%.sys.ini.tmpl new file mode 100644 index 00000000000..71108a0fe5a --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/language/en-GB/en-GB.%=option=%.sys.ini.tmpl @@ -0,0 +1,7 @@ +; @package hubzero-cms +; @copyright Copyright (c) 2005-2020 The Regents of the University of California. +; @license http://opensource.org/licenses/MIT MIT + +; Note : All ini files need to be saved as UTF-8 - No BOM + +%=option+uc=%="%=component_name+ucf=%" \ No newline at end of file diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/views/%=component_name=%/tmpl/display.php.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/views/%=component_name=%/tmpl/display.php.tmpl new file mode 100644 index 00000000000..a7d0b80ccc9 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/admin/views/%=component_name=%/tmpl/display.php.tmpl @@ -0,0 +1,13 @@ + + +
+
diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/config/access.xml.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/config/access.xml.tmpl new file mode 100644 index 00000000000..b8f5859b09a --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/config/access.xml.tmpl @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/config/config.xml.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/config/config.xml.tmpl new file mode 100644 index 00000000000..6295f45c056 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/config/config.xml.tmpl @@ -0,0 +1,12 @@ + + + + + +
+
+
diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/models/%=component_name=+p%.php.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/models/%=component_name=+p%.php.tmpl new file mode 100644 index 00000000000..47d188822b4 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/models/%=component_name=+p%.php.tmpl @@ -0,0 +1,20 @@ +execute(); diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/assets/css/%=component_name=%.css.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/assets/css/%=component_name=%.css.tmpl new file mode 100644 index 00000000000..3767934b4f0 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/assets/css/%=component_name=%.css.tmpl @@ -0,0 +1,11 @@ +/** + * @package hubzero-cms + * @copyright Copyright (c) 2005-2020 The Regents of the University of California. + * @license http://opensource.org/licenses/MIT MIT + */ + +/* +-------------------------- +%=option=% CSS +-------------------------- +*/ \ No newline at end of file diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/assets/js/%=component_name=%.js.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/assets/js/%=component_name=%.js.tmpl new file mode 100644 index 00000000000..a9082b091b3 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/assets/js/%=component_name=%.js.tmpl @@ -0,0 +1,13 @@ +/** + * @package hubzero-cms + * @copyright Copyright (c) 2005-2020 The Regents of the University of California. + * @license http://opensource.org/licenses/MIT MIT + */ + +if (!jq) { + var jq = $; +} + +jQuery(document).ready(function (jq) { + var $ = jq; +}); diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/controllers/%=component_name=%.php.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/controllers/%=component_name=%.php.tmpl new file mode 100644 index 00000000000..aa8d485a5fc --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/controllers/%=component_name=%.php.tmpl @@ -0,0 +1,113 @@ +view->%=component_name+p=% = $%=component_name+p=%->paginated()->ordered(); + $this->view->display(); + } + + /** + * New task + * + * @return void + */ + public function newTask() + { + $this->view->setLayout('edit'); + $this->view->task = 'edit'; + $this->editTask(); + } + + /** + * New/Edit function + * + * @return void + */ + public function editTask($%=component_name=%=null) + { + if (!isset($%=component_name=%) || !is_object($%=component_name=%)) + { + $%=component_name=% = %=component_name+ucf=%::oneOrNew(Request::getInt('id')); + } + + // Display + $this->view->row = $%=component_name=%; + $this->view->display(); + } + + /** + * Save new time record and redirect to the records page + * + * @return void + */ + public function saveTask() + { + // Create object + $%=component_name=% = %=component_name+ucf=%::oneOrNew(Request::getInt('id'))->set([]); + + if (!$%=component_name=%->save()) + { + // Something went wrong...return errors + foreach ($%=component_name=%->getErrors() as $error) + { + $this->view->setError($error); + } + + $this->view->setLayout('edit'); + $this->view->task = 'edit'; + $this->editTask($%=component_name=%); + return; + } + + // Set the redirect + $this->setRedirect( + Route::url('index.php?option=' . $this->_option . '&controller=' . $this->_controller), + Lang::txt('COM_%=component_name+uc=%_SAVE_SUCCESSFUL'), + 'passed' + ); + } + + /** + * Delete records + * + * @return void + */ + public function deleteTask() + { + $%=component_name=% = %=component_name+ucf=%::oneOrFail(Request::getInt('id')); + + // Delete %=component_name=% + $%=component_name=%->destroy(); + + // Set the redirect + App::redirect( + Route::url('index.php?option=' . $this->_option . '&controller=' . $this->_controller), + Lang::txt('COM_%=component_name+uc=%_DELETE_SUCCESSFUL'), + 'passed' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/language/en-GB/en-GB.%=option=%.ini.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/language/en-GB/en-GB.%=option=%.ini.tmpl new file mode 100644 index 00000000000..71108a0fe5a --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/language/en-GB/en-GB.%=option=%.ini.tmpl @@ -0,0 +1,7 @@ +; @package hubzero-cms +; @copyright Copyright (c) 2005-2020 The Regents of the University of California. +; @license http://opensource.org/licenses/MIT MIT + +; Note : All ini files need to be saved as UTF-8 - No BOM + +%=option+uc=%="%=component_name+ucf=%" \ No newline at end of file diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/router.php.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/router.php.tmpl new file mode 100644 index 00000000000..a8104ebe64c --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/router.php.tmpl @@ -0,0 +1,76 @@ +css() + ->js(); +?> + +
+ %=component_name+p=% as $%=component_name=%) { ?> + +
diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/views/%=component_name=%/tmpl/edit.php.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/views/%=component_name=%/tmpl/edit.php.tmpl new file mode 100644 index 00000000000..793934aa4e2 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/component.tmpl/site/views/%=component_name=%/tmpl/edit.php.tmpl @@ -0,0 +1,15 @@ +css() + ->js(); +?> + +
+
+
+
diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.create.table.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.create.table.tmpl new file mode 100644 index 00000000000..f0ec6168b66 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.create.table.tmpl @@ -0,0 +1,6 @@ + if (!$this->db->tableExists('%=table_name=%')) + { + $query = "%=create_table=%"; + $this->db->setQuery($query); + $this->db->query(); + } \ No newline at end of file diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.drop.table.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.drop.table.tmpl new file mode 100644 index 00000000000..660687598d1 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.drop.table.tmpl @@ -0,0 +1,6 @@ + if ($this->db->tableExists('%=table_name=%')) + { + $query = "DROP TABLE `%=table_name=%`"; + $this->db->setQuery($query); + $this->db->query(); + } \ No newline at end of file diff --git a/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.tmpl b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.tmpl new file mode 100644 index 00000000000..7314b5c76f5 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Scaffolding/Templates/migration.tmpl @@ -0,0 +1,30 @@ +arguments->getOpt('e') || $this->arguments->getOpt('extension') || $this->arguments->getOpt(4)) + { + // Set extension, according to priority of inputs + $extension = ($this->arguments->getOpt(4)) ? $this->arguments->getOpt(4) : $extension; + $extension = ($this->arguments->getOpt('e')) ? $this->arguments->getOpt('e') : $extension; + $extension = ($this->arguments->getOpt('extension')) ? $this->arguments->getOpt('extension') : $extension; + $extension = strtolower($extension); + } + else + { + // If extension wasn't provided, and we're in interactive mode...ask for it + if ($this->output->isInteractive()) + { + $extension = $this->output->getResponse('What extension do you the test to pertain to?'); + } + else + { + $this->output->error("Error: an extension should be provided."); + } + } + + // Parse the extension and build a real path + $path = PATH_CORE . DS; + $parts = explode('_', $extension); + switch ($parts[0]) + { + case 'lib': + unset($parts[0]); + // Hubzero\Console\Command\Scaffolding = __DIR__ + // Hubzero\Console\Command = dirname(__DIR__) + // Hubzero\Console = dirname(dirname(__DIR__)) + // Hubzero = dirname(dirname(dirname(__DIR__))) + $path = dirname(dirname(dirname(__DIR__))) . DS . implode(DS, $parts) . DS; + + $this->addReplacement('namespace', 'Hubzero\\' . ucfirst($parts[1]) . '\\Tests'); + break; + + default: + $this->output->error('Sorry, that extension type is not currently supported'); + break; + } + + // Make sure the extension exists + if (!is_dir($path)) + { + $this->output->error('Sorry, we couldn\'t find an extension by that name'); + } + + // Add tests dir to path and create it if it's not there + $path .= 'Tests'; + if (!is_dir($path)) + { + mkdir($path); + } + + // Type of test + $type = strtolower($this->arguments->getOpt('type', 'basic')); + + if (!in_array($type, ['basic', 'database'])) + { + $this->output->error('Sorry, test type should be one of either "basic" or "database"'); + } + + // Make test + $this->addTemplateFile("{$this->getType()}.{$type}.tmpl", $path . DS . 'Example' . ucfirst($type) . 'Test.php') + ->make(); + } + + /** + * Help doc for component scaffolding class + * + * @return void + **/ + public function help() + { + $this->output + ->addOverview( + 'Scaffolding for PHPUnit tests' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/SearchMigration.php b/core/libraries/Hubzero/Console/Command/SearchMigration.php new file mode 100644 index 00000000000..16f24b4598c --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/SearchMigration.php @@ -0,0 +1,127 @@ +run(); + } + + /** + * Run migration + * + * @museDescription Adds components to solr index + * + * @return void + **/ + public function run() + { + $newComponents = SearchComponent::getNewComponents(); + $newComponents->save(); + $components = SearchComponent::all(); + if (!$this->arguments->getOpt('all')) + { + $componentArgs = array(); + if ($this->arguments->getOpt('components')) + { + $componentArgs = explode(',', $this->arguments->getOpt('components')); + $componentArgs = array_map('trim', $componentArgs); + } + + if (empty($componentArgs)) + { + $this->output->error('Error: No components specified.'); + } + else + { + $components = $components->whereIn('name', $componentArgs); + } + } + + if (!$this->arguments->getOpt('rebuild')) + { + $components = $components->whereEquals('state', 0); + } + $url = $this->arguments->getOpt('url'); + if (empty($url)) + { + $this->output->error('Error: no URL provided.'); + } + foreach ($components as $compObj) + { + $offset = 0; + $batchSize = $compObj->getBatchSize(); + $batchNum = 1; + $compName = ucfirst($compObj->get('name')); + $startMessage = 'Indexing ' . $compName . '...' . PHP_EOL; + $this->output->addLine($startMessage); + while ($indexResults = $compObj->indexSearchResults($offset, $url)) + { + $batchMessage = 'Indexed ' . $compName . ' batch ' . $batchNum . ' of ' . $batchSize . PHP_EOL; + $this->output->addLine($batchMessage); + $offset = $indexResults['offset']; + $batchNum++; + } + if ($compObj->state != 1) + { + $compObj->set('state', 1); + $compObj->save(); + } + } + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'Run a solr search migration. Searches for and indexes models with the searcable interface.' + ) + ->addTasks($this) + ->addArgument( + '-url: the base url of the site being indexed.', + 'Example: -url=\'https://localhost\'' + ) + ->addArgument( + '-components: component(s) that should be indexed.', + 'If multiple, separate each component name with a comma.', + 'Example: -components=\'blog, content, kb, resources\'' + ) + ->addArgument( + '--all: index all searchable components', + 'Any component that contains a model that implements Searchable will be added to the solr index.', + 'Example: --all' + ) + ->addArgument( + '--rebuild: include components that have already had a full index run previously.', + 'this will overwrite any existing search documents with a new version.', + 'example: --rebuild' + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/Test.php b/core/libraries/Hubzero/Console/Command/Test.php new file mode 100644 index 00000000000..515d3c3581e --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Test.php @@ -0,0 +1,233 @@ +run(); + } + + /** + * Run the tests + * + * @museDescription Runs available tests for the given extension + * + * @return void + **/ + public function run() + { + // Get the extension to test...for now, this is required + if (!$extension = $this->arguments->getOpt(3)) + { + $this->output->error('Please provide a specific extension to test'); + } + + // Parse the extension and build a real path + $core = dirname(dirname(__DIR__)); + $path = 'core'; + if (strstr($extension, ':')) + { + $blocks = explode(':', $extension); + $path = array_shift($blocks); + $extension = implode('', $blocks); + } + $parts = explode('_', $extension); + switch ($parts[0]) + { + case 'tpl': + unset($parts[0]); + $path = ($path == 'app' ? PATH_APP : PATH_CORE) . DS . 'templates' . DS . implode('_', $parts) . DS . 'tests'; + break; + + case 'plg': + unset($parts[0]); + $path = ($path == 'app' ? PATH_APP : PATH_CORE) . DS . 'plugins' . DS . implode(DS, $parts) . DS . 'tests'; + break; + + case 'mod': + $path = ($path == 'app' ? PATH_APP : PATH_CORE) . DS . 'modules' . DS . $extension . DS . 'tests'; + break; + + case 'com': + $path = ($path == 'app' ? PATH_APP : PATH_CORE) . DS . 'components' . DS . $extension . DS . 'tests'; + break; + + case 'lib': + unset($parts[0]); + $path = $core . DS . ucfirst(implode(DS, $parts)) . DS . 'Tests'; + break; + + default: + $this->output->error('Sorry, we were not able to find an extension by that name or that extension type is not currently supported'); + break; + } + + // Make sure the test directory exists + if (!is_dir($path)) + { + $this->output->error('Sorry, we could\'t find a test directory for that extension'); + } + + // Build the command + $cmd = 'php ' . PATH_CORE . DS . 'bin' . DS . 'phpunit --no-globals-backup --bootstrap ' . PATH_CORE . DS . 'bootstrap' . DS . 'test' . DS . 'start.php ' . escapeshellarg($path) . ' 2>&1'; + + // We want to stream the output, so set up what we need to do that + $descriptorspec = [ + 0 => array("pipe", "r"), + 1 => array("pipe", "w"), + 2 => array("pipe", "w") + ]; + + $process = proc_open($cmd, $descriptorspec, $pipes); + + if (is_resource($process)) + { + while (false !== ($c = fgetc($pipes[1]))) + { + print $c; + } + while (false !== ($s = fgets($pipes[1]))) + { + print $s; + } + } + + // Close process + proc_close($process); + } + + /** + * Lists the test suites available to run + * + * @museDescription Shows a list of extensions with available tests + * + * @return void + **/ + public function show() + { + $tests = []; + + $nodes = array( + ['lib', dirname(dirname(__DIR__))], + ['core', PATH_CORE . DS . 'templates'], + ['app', PATH_APP . DS . 'templates'], + ['core', PATH_CORE . DS . 'components'], + ['app', PATH_APP . DS . 'components'], + ['core', PATH_CORE . DS . 'modules'], + ['app', PATH_APP . DS . 'modules'] + ); + + foreach ($nodes as $node) + { + $key = $node[0]; + $base = $node[1]; + + $directories = array_diff(scandir($base), ['.', '..']); + + foreach ($directories as $directory) + { + if (is_dir($base . DS . $directory . DS . 'Tests') + || is_dir($base . DS . $directory . DS . 'tests')) + { + if (basename($base) == 'templates') + { + $directory = 'tpl_' . $directory; + } + $tests[] = $key . ($key == 'lib' ? '_' : ':') . strtolower($directory); + } + } + } + + // Plugins have one extra level of directories + $nodes = array( + ['core', PATH_CORE . DS . 'plugins'], + ['app', PATH_APP . DS . 'plugins'] + ); + + foreach ($nodes as $node) + { + $key = $node[0]; + $base = $node[1]; + + $directories = array_diff(scandir($base), ['.', '..']); + + foreach ($directories as $directory) + { + if (!is_dir($base . DS . $directory)) + { + continue; + } + + $subdirectories = array_diff(scandir($base . DS . $directory), ['.', '..']); + + foreach ($subdirectories as $subdirectory) + { + if (is_dir($base . DS . $directory . DS . $subdirectory . DS . 'Tests') + || is_dir($base . DS . $directory . DS . $subdirectory . DS . 'tests')) + { + $tests[] = $key . ($key == 'lib' ? '_' : ':') . 'plg_' . strtolower($directory) . '_' . strtolower($subdirectory); + } + } + } + } + + if (!count($tests)) + { + $this->output->addLine('There are currently no tests suites available to be run.', 'warning'); + } + else + { + foreach ($tests as $test) + { + $this->output->addLine($test, 'success'); + } + } + } + + /** + * Output help documentation + * + * @return void + **/ + public function help() + { + $this + ->output + ->addOverview( + 'A custom PHPUnit testing wrapper. This helps with setting up the + environment and allowing for specialized options related to testing.' + ) + ->addTasks($this) + ->addArgument( + 'extension', + 'The first option to the "run" command should be a specific extension. + Currently, running the entire suite of tests is not allowed. The command + will search the provided extension for a directory titled "Tests". The + command will parse the provided extension, and expects a name in the format + of com_name, mod_name, plg_folder_element, or lib_name. Prepend "app:" or + "core:" to designate the specific root directory corresponding to ROOT/app + and ROOT/core respectively. Libraries are assumed to be in the core Hubzero + library folder.', + '', + true + ); + } +} diff --git a/core/libraries/Hubzero/Console/Command/User.php b/core/libraries/Hubzero/Console/Command/User.php new file mode 100644 index 00000000000..81c9ca4fa26 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/User.php @@ -0,0 +1,385 @@ +help(); + } + + /** + * Help doc for user command + * + * @return void + **/ + public function help() + { + $this->output + ->getHelpOutput() + ->addOverview('General user functions for manipulating hub users.') + ->addTasks($this) + ->render(); + } + + /** + * Merge two user accounts into one + * + * @TODO: middleware tables? + * + * @museDescription Merges two users together, disabling the source user + * + * @return void + **/ + public function merge() + { + $sourceUser = 0; + $destinationUser = 0; + $directionalIndicator = $this->arguments->getOpt(4); + + if ($directionalIndicator) + { + switch ($directionalIndicator) + { + case 'into': + $sourceUser = (int)$this->arguments->getOpt(3); + $destinationUser = (int)$this->arguments->getOpt(5); + break; + + default: + // Do nothing...can't assume + break; + } + + if ((!$sourceUser || !$destinationUser) && !$this->output->isInteractive()) + { + $this->output->error('Please provide a source and destination user in the format: muse user merge [sourceUserId] into [destUserId]'); + } + } + else + { + if ($this->output->isInteractive()) + { + $destinationUser = (int)$this->output->getResponse('What is the destination user ID (this is the user that will remain after the merge)?'); + $sourceUser = (int)$this->output->getResponse('What is the source user ID (this is the user that will be deleted after the merge)?'); + } + else + { + $this->output->error('Please provide a source and destination user in the format: muse user merge [sourceUserId] into [destUserId]'); + } + } + + $suser = \User::getInstance($sourceUser); + $duser = \User::getInstance($destinationUser); + $dbo = App::get('db'); + $tables = $dbo->getTableList(); + $prefix = $dbo->getPrefix(); + $fields = array( + 'created_by', 'modified_by', 'reviewed_by', 'user_id', 'userid', 'authorid', 'checked_out', 'uid', + 'uidNumber', 'created_user_id', 'modified_user_id', 'object_id', 'follower_id', 'following_id', + 'sent_by', 'redeemed_by', 'userid', 'creator_id', 'addedBy', 'editedBy', 'user_id_to', 'user_id_from', + 'commenter', 'uploaded_by', 'posted_by', 'assigned_to', 'closed_by', 'owned_by_user', 'created_by_user', + 'ran_by', 'foreign_key', 'taggerid', 'actor_id', 'voter', 'proposed_by', 'granted_by', 'assigned', + 'approved_by', 'action_by', 'authorid' + ); + $unames = array( + $prefix . 'event_registration.username', + //$prefix . 'resource_stats_clusters.username', + //$prefix . 'resource_stats_tools_users.user', + //$prefix . 'session_geo.username', + $prefix . 'support_acl_aros.alias', + $prefix . 'support_comments.created_by', + $prefix . 'support_tickets.login', + $prefix . 'tool.team', + $prefix . 'tool.registered_by', + $prefix . 'tool_version.released_by', + $prefix . 'wiki_page.authors' + ); + $excludes = array( + $prefix . 'xprofiles', + $prefix . 'session_log', + $prefix . 'session', + $prefix . 'users_quotas' + ); + $constraints = array( + $prefix . 'collections.object_id' => "AND `object_type` = 'member'", + $prefix . 'collections_following.follower_id' => "AND `follower_type` = 'member'", + $prefix . 'collections_following.following_id' => "AND `following_type` = 'member'", + $prefix . 'support_acl_aros.foreign_key' => "AND `model` = 'user'", + $prefix . 'support_acl_aros.alias' => "AND `model` = 'user'" + ); + + // First, make sure we were given valid user ids + if (!$suser->get('id') || !$duser->get('id')) + { + $this->output->error('User does not appear to be valid'); + } + + // Secondly, make sure this user hasn't been involved in a merge beforehand + $query = "SELECT `id` FROM `#__users_merge_log` WHERE `source` = '{$sourceUser}'"; + $dbo->setQuery($query); + if ($dbo->loadResult()) + { + $this->output->error('This user appears to have already been merged into another user.'); + } + + foreach ($tables as $table) + { + // Ignore a few tables + if (in_array($table, $excludes)) + { + continue; + } + + // Figure out what the table's primary key is + $query = "SHOW INDEX FROM `{$table}` WHERE `Key_name` = 'PRIMARY'"; + $dbo->setQuery($query); + $index = $dbo->loadObject(); + $tablePK = (isset($index->Column_name)) ? $index->Column_name : false; + + // Get the columns + $columns = $dbo->getTableColumns($table); + + // Loop over columns and see if they're in our list from above + foreach ($columns as $column => $type) + { + if (in_array($table.'.'.$column, $unames) || in_array($column, $fields)) + { + $sUserName = $suser->get('username'); + $dUserName = $duser->get('username'); + + // We have a match, now check if there are rows to merge + $query = "SELECT * FROM `{$table}` WHERE `{$column}` = '{$sourceUser}' OR `{$column}` LIKE '%{$sUserName}%'"; + $query .= ((isset($constraints[$table.'.'.$column])) ? ' ' . $constraints[$table.'.'.$column]: ''); + $dbo->setQuery($query); + $results = $dbo->loadObjectList(); + + if ($results && count($results) > 0) + { + $count = count($results); + foreach ($results as $row) + { + if (!$tablePK) + { + $this->output->addLine("Ignoring {$table}.{$column} due to lack of primary key", 'warning'); + continue 2; + } + + if (!$this->arguments->getOpt('dry-run')) + { + try + { + $numericUpdate = true; + if (is_numeric($row->$column)) + { + $query = "UPDATE `{$table}` SET `{$column}` = '{$destinationUser}' WHERE `{$tablePK}` = '{$row->$tablePK}'"; + } + else + { + $numericUpdate = false; + $query = "UPDATE `{$table}` SET `{$column}` = REPLACE({$column}, '{$sUserName}', '{$dUserName}') WHERE `{$tablePK}` = '{$row->$tablePK}'"; + } + + $dbo->setQuery($query); + $dbo->query(); + + // Now log it + $log = new \stdClass(); + $log->source = ($numericUpdate) ? $sourceUser : $sUserName; + $log->destination = ($numericUpdate) ? $destinationUser : $dUserName; + $log->table = $table; + $log->column = $column; + $log->table_pk = $tablePK; + $log->table_id = $row->$tablePK; + $log->logged = with(new Date('now'))->toSql(); + $dbo->insertObject('#__users_merge_log', $log); + } + catch (\Hubzero\Database\Exception\QueryFailedException $e) + { + if ($e->getPrevious()->getCode() == '23000') + { + $this->output->addLine("Ignoring {$table}.{$column} due to integrity constraint violation", 'warning'); + continue 2; + } + else + { + $this->output->error("Error: " . $e->getMessage()); + } + } + } + } + if ($this->arguments->getOpt('dry-run')) + { + $this->output->addLine("Would update ({$count}) item(s) in {$table}.{$column}"); + } + else + { + $this->output->addLine("Updating ({$count}) item(s) in {$table}.{$column}", 'success'); + } + } + } + } + } + + // Lastly, block the user being merged + if (!$this->arguments->getOpt('dry-run')) + { + $suser->set('block', 1); + $suser->save(); + } + } + + /** + * Reverse the merge process (via logs, not by mirroring the merge process) + * + * @museDescription Unmerges a previous merge, reenabling the source user + * + * @return void + **/ + public function unmerge() + { + $sourceUser = 0; + $destinationUser = 0; + $directionalIndicator = $this->arguments->getOpt(4); + + if ($directionalIndicator) + { + switch ($directionalIndicator) + { + case 'from': + $sourceUser = (int)$this->arguments->getOpt(5); + $destinationUser = (int)$this->arguments->getOpt(3); + break; + + default: + // Do nothing...can't assume + break; + } + + if ((!$sourceUser || !$destinationUser) && !$this->output->isInteractive()) + { + $this->output->error('Please provide a source and destination user in the format: muse user unmerge [destUserId] from [sourceUserId]'); + } + } + else + { + if ($this->output->isInteractive()) + { + $destinationUser = (int)$this->output->getResponse('What is the destination user ID (this is the user that was deleted during the initial merge)?'); + $sourceUser = (int)$this->output->getResponse('What is the source user ID (this is the user that was the recipient of the initially merged data)?'); + } + else + { + $this->output->error('Please provide a source and destination user in the format: muse user unmerge [destUserId] from [sourceUserId]'); + } + } + + // Now, make sure a merge between these two actually exists in the logs + $suser = \User::getInstance($sourceUser); + $duser = \User::getInstance($destinationUser); + $dbo = App::get('db'); + + // First, make sure we were given valid user ids + if (!$suser->get('id') || !$duser->get('id')) + { + $this->output->error('User does not appear to be valid'); + } + + $sUserName = $suser->get('username'); + $dUserName = $duser->get('username'); + + $query = "SELECT * FROM `#__users_merge_log`"; + $query .= " WHERE (`source` = '{$destinationUser}' AND `destination` = '{$sourceUser}')"; + $query .= " OR (`source` = '{$dUserName}' AND `destination` = '{$sUserName}')"; + $query .= " ORDER BY `table` ASC, `COLUMN` ASC"; + $dbo->setQuery($query); + if (!$results = $dbo->loadObjectList()) + { + $this->output->error("Sorry, we couldn't find a preexisting merge between these two users to undo"); + } + else + { + if (count($results) > 0) + { + if ($this->output->isInteractive()) + { + $progress = $this->output->getProgressOutput(); + $count = count($results); + $progress->init('Unmerging records: ', 'ratio', $count); + } + + $counter = 0; + foreach ($results as $result) + { + if (is_numeric($result->source)) + { + $query = "UPDATE `{$result->table}` SET `{$result->column}` = '{$result->source}' WHERE `{$result->table_pk}` = '{$result->table_id}'"; + } + else + { + $query = "UPDATE `{$result->table}` SET `{$result->column}` = REPLACE({$result->column}, '{$result->destination}', '{$result->source}') WHERE `{$result->table_pk}` = '{$result->table_id}'"; + } + $dbo->setQuery($query); + if ($dbo->query()) + { + $query = "DELETE FROM `#__users_merge_log` WHERE `id` = '{$result->id}'"; + $dbo->setQuery($query); + $dbo->query(); + + if ($this->output->isInteractive()) + { + ++$counter; + $progress->setProgress($counter, $count); + } + else + { + $this->output->addLine("Unmerging {$result->table}.{$result->column}"); + } + } + } + + if ($this->output->isInteractive()) + { + $progress->done(); + $this->output->addLine("Unmerged ({$counter}/{$count}) records successfully!", 'success'); + } + } + } + + // Unblock the user + $duser->set('block', 0); + $duser->save(); + } + + /** + * Block a user (probably because of spamming) + * + * @museDescription Disables a user completely, without deleting + * + * @return void + **/ + public function disable() + { + $this->output->addLine('Not implemented', 'warning'); + } +} diff --git a/core/libraries/Hubzero/Console/Command/User/Terms.php b/core/libraries/Hubzero/Console/Command/User/Terms.php new file mode 100644 index 00000000000..d27df462922 --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/User/Terms.php @@ -0,0 +1,117 @@ +help(); + } + + /** + * Help doc for user command + * + * @return void + **/ + public function help() + { + $this->output + ->getHelpOutput() + ->addOverview('Functions for updating hub user terms of use.') + ->render(); + } + + /** + * Clear terms of use agreements + * + * @return void + */ + public function clear() + { + // Initialize confirm + $confirm = 'no'; + + if (!$this->output->isInteractive() && !$this->arguments->getOpt('f')) + { + $this->output->addLine('To forcibly clear all terms of use agreements for all users, please provide the -f flag. This action is irreversable.', 'warning'); + return; + } + else if (!$this->output->isInteractive() && $this->arguments->getOpt('f')) + { + $confirm = 'yes'; + } + else if ($this->output->isInteractive()) + { + // Confirm clearing + $confirm = $this->output->getResponse('Are you sure you want to clear Terms of Use for all users? This will also require users to agree to new terms with next login? (yes/no)'); + } + + // Did we get a yes? + if (strtolower($confirm) == 'yes' || strtolower($confirm) == 'y') + { + // Get db object + $dbo = App::get('db'); + + // Update registration config value to require re-agreeing upon next login + $params = \Component::params('com_members'); + $currentTOU = $params->get('registrationTOU', 'RHRH'); + $newTOU = substr_replace($currentTOU, 'R', 3); + $params->set('registrationTOU', $newTOU); + + // Update registration param in db + $query = "UPDATE `#__extensions` SET `params`=" . $dbo->quote($params->toString()) . " WHERE `name`='com_members'"; + $dbo->setQuery($query); + if (!$dbo->query()) + { + $this->output->error('Unable to set registration field TOU to required on next update.'); + } + + // Clear all old TOU states + if ($dbo->tableExists('#__users') && $dbo->tableHasField('#__users', 'usageAgreement')) + { + $dbo->setQuery("UPDATE `#__users` SET `usageAgreement`=0;"); + if (!$dbo->query()) + { + $this->output->error('Unable to clear users terms of use.'); + } + } + + if ($dbo->tableExists('#__xprofiles') && $dbo->tableHasField('#__xprofiles', 'usageAgreement')) + { + $dbo->setQuery("UPDATE `#__xprofiles` SET `usageAgreement`=0;"); + if (!$dbo->query()) + { + $this->output->error('Unable to clear xprofiles terms of use.'); + } + } + + // Output message to let admin know everything went well + $this->output->addLine('Terms of Use successfully cleared & registration param updated!', 'success'); + } + else + { + $this->output->addLine('Operation aborted.'); + } + } +} diff --git a/core/libraries/Hubzero/Console/Command/Utilities/Git.php b/core/libraries/Hubzero/Console/Command/Utilities/Git.php new file mode 100644 index 00000000000..b0c71b0bd6b --- /dev/null +++ b/core/libraries/Hubzero/Console/Command/Utilities/Git.php @@ -0,0 +1,621 @@ +dir = $root . DS . '.git'; + $this->workTree = $root; + $this->baseCmd = "git --git-dir={$this->dir} --work-tree={$this->workTree}"; + + // Save upstream branch name + if (!isset($source)) + { + $this->upstream = $this->call('rev-parse', array('--abbrev-ref', '--symbolic-full-name', '@{u}')); + $this->upstream = trim($this->upstream); + } + else + { + $this->upstream = trim($source); + } + } + + /** + * Just return the name of this utility + * + * @return string + **/ + public function getName() + { + return 'GIT'; + } + + /** + * Return the base path of the repository + * + * @return string + **/ + public function getBasePath() + { + return $this->workTree; + } + + /** + * Get the mechanisms version identifier + * + * @return string + **/ + public function getMechanismVersionName() + { + $name = $this->call('rev-parse', array('--abbrev-ref', 'HEAD')); + $name = trim($name); + + return $name; + } + + /** + * Get the status of the repository + * + * @return string + **/ + public function status() + { + // Use 'porcelain' argument for consistent formatting of output + $arguments = array('--porcelain'); + $status = $this->call('status', $arguments); + $response = ''; + + if (!empty($status)) + { + $status = trim($status); + $lines = explode("\n", $status); + + $response = array( + 'added' => array(), + 'modified' => array(), + 'deleted' => array(), + 'renamed' => array(), + 'copied' => array(), + 'untracked' => array(), + 'unmerged' => array(), + 'merged' => array() + ); + foreach ($lines as $line) + { + $line = trim($line); + preg_match('/([A|D|M|U|R|C|?]{1,2})[ ]{1,2}([[:alnum:]_\-\.\/]*)/', $line, $parts); + + if (strlen($parts[1]) == 2 && $parts[1] != '??' && $parts[1] != 'UU') + { + $parts[1] = 'merged'; + } + + switch ($parts[1]) + { + case 'A': + $response['added'][] = $parts[2]; + break; + case 'D': + $response['deleted'][] = $parts[2]; + break; + case 'M': + $response['modified'][] = $parts[2]; + break; + case 'R': + $response['renamed'][] = $parts[2]; + break; + case 'C': + $response['copied'][] = $parts[2]; + break; + case 'UU': + $response['unmerged'][] = $parts[2]; + break; + case '??': + $response['untracked'][] = $parts[2]; + break; + case 'merged': + $response['merged'][] = $parts[2]; + break; + } + } + } + + return $response; + } + + /** + * Get the log + * + * @param int $length Number of entires to return + * @param int $start Commit number to start at + * @param bool $upcoming Whether or not to include upcoming commits in response + * @param bool $installed Whether or not to include installed commits in response + * @param string $search Filter by search string + * @param string $format Format of response + * @param bool $count Return count of entires + * @return array + **/ + public function log($length = null, $start = null, $upcoming = false, $installed = true, $search = null, $format = '%an: %s', $count = false) + { + $args = array(); + + // Count trumps all, just compute and return + if ($count) + { + return $this->countLogs($installed, $upcoming, $search); + } + + if ($upcoming) + { + $args['upcoming'] = "HEAD..{$this->upstream}"; + } + if (isset($length)) + { + $args['length'] = '-' . (int)$length; + } + if (isset($start)) + { + $args['skip'] = '--skip=' . (int)$start; + } + if (isset($format)) + { + $args['format'] = '--pretty=format:' . escapeshellarg($format); + } + if (isset($search)) + { + $args['case-insensitive'] = '-i'; + $args['search'] = '--grep=' . escapeshellarg($search); + } + + // If upcoming is set, we have to pull those commits first + $upcomingCount = 0; + $upcomingTotal = 0; + if ($upcoming) + { + $upcomingLog = $this->call('log', $args); + + if (isset($upcomingLog)) + { + $upcomingLogs = explode("\n", $upcomingLog); + $upcomingCount = count($upcomingLogs); + } + + $upcomingTotal = $this->countLogs(false, true, $search); + } + + if ($upcomingCount < $length) + { + if (isset($args['upcoming'])) + { + unset($args['upcoming']); + } + $args['length'] = '-' . ($length - $upcomingCount); + $args['skip'] = '--skip=' . ((($start - $upcomingTotal) >= 0) ? ($start - $upcomingTotal) : 0); + $currentLog = $this->call('log', $args); + $currentLogs = (!empty($currentLog)) ? explode("\n", $currentLog) : array(); + } + + $response = array(); + + if ($upcomingCount > 0) + { + foreach ($upcomingLogs as $entry) + { + $response[] = '* ' . $entry; + } + } + if (isset($currentLogs) && count($currentLogs) > 0 && $installed) + { + foreach ($currentLogs as $entry) + { + $response[] = $entry; + } + } + + return $response; + } + + /** + * Count log entries + * + * @param bool $installed Whether or not to include installed entries + * @param bool $upcoming Whether or not to include upcoming entries + * @param string $search A search filter to apply + * @return int + **/ + private function countLogs($installed = true, $upcoming = false, $search = null) + { + $total = 0; + $countArgs = array('--count'); + + if (isset($search)) + { + $countArgs[] = '-i'; + $countArgs[] = '--grep=' . escapeshellarg($search); + } + + if ($installed) + { + $installedArgs = $countArgs; + array_unshift($installedArgs, 'HEAD'); + $total = $this->call('rev-list', $installedArgs); + $total = trim($total); + } + + if ($upcoming) + { + $upcomingArgs = $countArgs; + array_unshift($upcomingArgs, "HEAD..{$this->upstream}"); + $upcomingTotal = $this->call('rev-list', $upcomingArgs); + $upcomingTotal = trim($upcomingTotal); + $total += $upcomingTotal; + } + + return trim($total); + } + + /** + * Pull the latest updates + * + * @param bool $dryRun Whether or not to do the run, or just check what's incoming + * @param bool $allowNonFf Whether or not to allow non fast forward pulls (i.e. merges) + * @param string $source Where this repository should pull from (this should be a valid git remote/branch) + * @return string + **/ + public function update($dryRun = true, $allowNonFf = false, $source = null) + { + + if (!$dryRun) + { + // Move to the working tree dir (git 1.8.5 has a built in option for this...but we're still generally running 1.7.x) + chdir($this->workTree); + $arguments = array(); + + if (isset($source)) + { + $arguments[] = $source; + } + else + { + $arguments[] = $this->upstream; + } + + // Add fast forward only arg if applicable + if (!$allowNonFf) + { + $arguments[] = '--ff-only'; + } + + // Doing some trickery here. + // Newer versions of git prefer you to edit the auto generated message after every merge...we don't want to do this + // There is an option to prevent that (--no-edit), but it doesn't apply to older versions of git + // Thus we're going to use an env variable to prevent the behavior on newer versions, while not effecting older + // versions that are currently unaware of the --no-edit option + + // Start off by seeing if there is an autoedit env var already set + $autoedit = getenv("GIT_MERGE_AUTOEDIT"); + // Now set it to no + putenv("GIT_MERGE_AUTOEDIT=no"); + + // Now do the actual pull + $response = $this->call('merge', $arguments); + + // Now clear the var or reset it if it had been previously set + if ($autoedit) + { + putenv("GIT_MERGE_AUTOEDIT={$autoedit}"); + } + else + { + putenv("GIT_MERGE_AUTOEDIT"); + } + + $response = trim($response); + $return = array(); + + if (substr($response, 0, 5) == 'fatal') + { + $return['status'] = 'fatal'; + $return['message'] = trim(substr($response, stripos($response, 'fatal') + 6)); + } + else if (substr($response, 0, 5) == 'error') + { + $return['status'] = 'fatal'; + $return['message'] = trim(substr($response, stripos($response, 'error') + 6)); + } + else if (stripos($response, 'automatic merge failed') !== false) + { + $return['status'] = 'fatal'; + $return['message'] = $response; + } + else + { + $return['status'] = 'success'; + } + + // Include the raw return for all calls + $return['raw'] = $response; + } + else + { + // Be sure to fetch so we known we're up-to-date + $this->call('fetch'); + $this->call('remote update'); + + // Build arguments + $arguments = array( + '--pretty=format:"%an: \"%s\" (%ar)"', + "HEAD..{$this->upstream}" + ); + + // Make call to get log differences between us and origin + $log = $this->call('log', $arguments); + $log = trim($log); + $return = array(); + $logs = explode("\n", $log); + + if (!empty($log) && count($logs) > 0) + { + foreach ($logs as $log) + { + $return[] = trim($log); + } + } + } + + return $return; + } + + /** + * Pushes the local repository to the remote destination + * + * @param string $ref The ref to push from/to (the same name unless $remoteRef is provided) + * @param string $remote The remote name to push to + * @param string $remoteRef The remote ref if it differs in name from the local one + * @return array + **/ + public function push($ref = 'master', $remote = 'origin', $remoteRef = null) + { + $ref = (isset($remoteRef)) ? $ref . ':' . $remoteRef : $ref; + $response = $this->call('push', array($remote, $ref)); + + $response = trim($response); + $return = array(); + + if (stripos($response, 'error: failed to push some refs') !== false) + { + $return['status'] = 'fatal'; + $return['message'] = $response; + } + else if (stripos($response, 'everything up-to-date') !== false) + { + $return['status'] = 'success'; + $return['message'] = $response; + } + else + { + $return['status'] = 'success'; + } + + return $return; + } + + /** + * Create a rollback point for restoration in the event of a problem during the update + * + * @return string + **/ + public function createRollbackPoint() + { + $tagname = 'cmsrollbackpoint-' . date('U'); + return $this->tag($tagname); + } + + /** + * Get the latest rollback point + * + * @return string|bool + **/ + public function getRollbackPoint() + { + $tagList = $this->call('tag'); + $tags = explode("\n", $tagList); + $rbp = 0; + + if (count($tags) > 0) + { + foreach ($tags as $tag) + { + if (strstr($tag, 'cmsrollbackpoint-') !== false) + { + $tmp_tag = substr($tag, 17); + if ($tmp_tag > $rbp) + { + $rbp = $tmp_tag; + } + } + } + + if ($rbp === 0) + { + return false; + } + + return $rbp; + } + else + { + return false; + } + } + + /** + * Purge rollback points except for the latest + * + * @return void + **/ + public function purgeRollbackPoints() + { + $tagList = $this->call('tag'); + $tags = explode("\n", $tagList); + $rollbackPoints = array(); + + foreach ($tags as $tag) + { + if (strstr($tag, 'cmsrollbackpoint-') !== false) + { + $rollbackPoints[] = $tag; + } + } + + if (count($rollbackPoints) > 1) + { + for ($i=0; $i < (count($rollbackPoints) - 1); $i++) + { + $this->call('tag', array('-d', $rollbackPoints[$i])); + } + } + } + + /** + * Purge stashed changes + * + * @return void + **/ + public function purgeStash() + { + $this->call('stash', array('clear')); + } + + /** + * Perform rollback + * + * @param string $rollbackPoint Tagname of rollback point + * @return bool + **/ + public function rollback($rollbackPoint) + { + $tagname = 'cmsrollbackpoint-'.$rollbackPoint; + + // Make sure the tag exists first + if (substr($this->call('show', array($tagname)), 0, 5 == 'fatal')) + { + return false; + } + + // Do a hard reset - this is destructive! + $this->call('reset', array('--hard', $tagname)); + + return true; + } + + /** + * Create a tag + * + * @param string $tagname The tag name to create + * @return string + **/ + public function tag($tagname) + { + $arguments = array($tagname); + + return $this->call('tag', $arguments); + } + + /** + * Check to see if the repo is clean, at least as far as this mechanism is concerned + * + * @return bool + **/ + public function isClean() + { + $status = $this->status(); + $eligible = true; + + if (!empty($status) && is_array($status)) + { + foreach ($status as $type => $files) + { + if ($type != 'untracked' && !empty($files)) + { + $eligible = false; + break; + } + } + } + + return $eligible; + } + + /** + * Stash local changes + * + * @return + **/ + public function stash() + { + $response = $this->call('stash'); + + return $response; + } + + /** + * Call a git command + * + * @param string $cmd Git command being called + * @param array $args Arguments for the specific command + * @return string + **/ + private function call($cmd, $arguments = array()) + { + $command = "{$this->baseCmd} {$cmd}" . ((!empty($arguments)) ? ' ' . implode(' ', $arguments) : '') . ' 2>&1'; + $response = shell_exec($command); + + return $response; + } +} diff --git a/core/libraries/Hubzero/Console/ComponentServiceProvider.php b/core/libraries/Hubzero/Console/ComponentServiceProvider.php new file mode 100644 index 00000000000..287e4376483 --- /dev/null +++ b/core/libraries/Hubzero/Console/ComponentServiceProvider.php @@ -0,0 +1,30 @@ +app['component'] = function($app) + { + return new Loader($app); + }; + } +} diff --git a/core/libraries/Hubzero/Console/Config.php b/core/libraries/Hubzero/Console/Config.php new file mode 100644 index 00000000000..a1058e831f0 --- /dev/null +++ b/core/libraries/Hubzero/Console/Config.php @@ -0,0 +1,185 @@ +path = $path; + + $this->config = new Registry(); + + // See if there's an existing file + if (is_file($path)) + { + $this->config->parse($path); + } + } + + /** + * Creates a new instance of self + * + * @return static + **/ + public static function getInstance() + { + static $instance; + + if (!isset($instance)) + { + $instance = new self(); + } + + return $instance; + } + + /** + * Gets the specified config var + * + * @param string $key The key to fetch + * @param mixed $default The default to return, should the key not exist + * @return mixed + **/ + public static function get($key, $default = false) + { + $instance = self::getInstance(); + + return (isset($instance->config[$key])) ? $instance->config[$key] : $default; + } + + /** + * Saves the data to the config file + * + * Passed data will be merged with existing data. + * + * @param array $data The data to save + * @return bool + **/ + public static function save($data) + { + $instance = self::getInstance(); + + // Merge and make sure values are unique + $data = $instance->merge($instance->config->toArray(), $data); + $data = $instance->unique($data); + + // Set data back to the instance + $instance->config = new Registry($data); + + // Actually write out the data + $instance->write(); + + return true; + } + + /** + * Writes the data to the configuration file + * + * @return void + **/ + private function write() + { + $this->config->write($this->path, 'yaml'); + } + + /** + * Merge multiple arrays into one, recursively + * + * Dear future developer who comes in and says, "Why, there's a PHP function for that! + * It's called array_merge_recursive". Don't do it! This function works slightly + * differently. Namely, if a nested array is not associative, we want it to append items + * to it, rather than completely overwrite the value of the nested element. + * + * @param array $existing The existing data + * @param array $incoming The new data + * @return array + **/ + private function merge($existing, $incoming) + { + foreach ($incoming as $k => $v) + { + if (is_array($v)) + { + $existing[$k] = isset($existing[$k]) ? $this->merge($existing[$k], $v) : $this->merge(array(), $v); + } + else + { + if (is_numeric($k)) + { + $existing[] = $v; + } + else + { + $existing[$k] = $v; + } + } + } + + return $existing; + } + + /** + * Multi-dimensional array_unique function + * + * @param array $var The array to make unique + * @return array + **/ + private function unique($var) + { + if (is_array($var)) + { + // We only want to get unique items if they aren't associative + if (isset($var[0])) + { + // Serialize vars, unique them, then unserialize + $var = array_map('unserialize', array_unique(array_map('serialize', $var))); + } + + foreach ($var as &$sub) + { + if (is_array($sub)) + { + $sub = $this->unique($sub); + } + } + } + + return $var; + } +} diff --git a/core/libraries/Hubzero/Console/DispatcherServiceProvider.php b/core/libraries/Hubzero/Console/DispatcherServiceProvider.php new file mode 100644 index 00000000000..cb02ca7d61a --- /dev/null +++ b/core/libraries/Hubzero/Console/DispatcherServiceProvider.php @@ -0,0 +1,55 @@ +next($request); + + $class = $this->app->get('arguments')->get('class'); + $task = $this->app->get('arguments')->get('task'); + + $command = new $class($this->app->get('output'), $this->app->get('arguments')); + $shortName = strtolower(with(new \ReflectionClass($command))->getShortName()); + + // Fire default before event + Event::fire($shortName . '.' . 'before' . ucfirst($task)); + + $command->{$task}(); + + // Fire default after event + Event::fire($shortName . '.' . 'after' . ucfirst($task)); + + $this->app->get('output')->render(); + + return $response; + } +} diff --git a/core/libraries/Hubzero/Console/Event.php b/core/libraries/Hubzero/Console/Event.php new file mode 100644 index 00000000000..a3ab0e8c71c --- /dev/null +++ b/core/libraries/Hubzero/Console/Event.php @@ -0,0 +1,58 @@ +response) && count($this->response) > 0) + { + foreach ($this->response as $line) + { + // Echo out the message + echo $line['message']; + + if ($newLine) + { + echo "\n"; + } + } + + // Reset response + $this->response = array(); + } + } + + /** + * Add a new line to the output buffer (not actually a real php output buffer) + * + * @param string $message Text of line + * @param mixed $styles Array of custom styles or string containing predefined term (see formatLine() for posibilities) + * @param bool $newLine Whether or not line should end with a new line + * @return $this + **/ + public function addLine($message, $styles = null, $newLine = true) + { + $this->formatLine($message, $styles); + + $this->response[] = array( + 'message' => $message + ); + + if ($this->isInteractive()) + { + $this->render($newLine); + } + + return $this; + } + + /** + * Add a new string to the output buffer + * + * Main difference between this and addLine() is that this is a shortcut for not + * including a new line at the end of the output + * + * @param string $message Text of string + * @param mixed $styles Array of custom styles or string containing predefined term (see formatLine() for posibilities) + * @return $this + **/ + public function addString($message, $styles = null) + { + $this->addLine($message, $styles, false); + + return $this; + } + + /** + * Add a paragraph to the output buffer. + * This will chop the text up to maintain lines of approximately 80 characters. + * + * @param string $paragraph Text to be chopped into lines and stored + * @param mixed $styles Array of custom styles or string containing predefined term (see formatLine() for posibilities) + * @return $this + **/ + public function addParagraph($paragraph, $styles = array()) + { + // Sanitize the given text of new lines, double spaces, and tabs + $paragraph = str_replace("\n", " ", $paragraph); + $paragraph = str_replace(" ", " ", $paragraph); + $paragraph = str_replace("\t", "", $paragraph); + + // Now check if the paragraph is longer than 70 characters and subdivide as appropriate + do + { + if (strlen($paragraph) > 70 && $break = strpos($paragraph, " ", 70)) + { + $message = substr($paragraph, 0, $break); + $paragraph = trim(substr($paragraph, $break)); + } + else + { + $message = trim($paragraph); + $break = false; + } + + // Add the individual line + $this->addLine($message, $styles); + } + while ($break !== false); + + return $this; + } + + /** + * Renders rows in a table-like structure + * + * @param array $rows The rows of text to render + * @param bool $headers If the first row contains header information + * @return $this + **/ + public function addTable($rows, $headers = false) + { + // Figure out some items we need to know + $maxLengths = []; + + foreach ($rows as $i => $row) + { + foreach ($row as $k => $field) + { + $maxLengths[$k] = isset($maxLengths[$k]) ? $maxLengths[$k] : 0; + $maxLengths[$k] = strlen($field) > $maxLengths[$k] ? strlen($field) : $maxLengths[$k]; + } + } + + // Compute the total length of the table + $width = array_sum($maxLengths) + ((count($row) - 1) * 3) + 2; + + // Add the top border + $this->addLine('/' . str_repeat('-', ($width)) . '\\'); + + // Draw the rows + foreach ($rows as $i => $row) + { + $styles = ($i == 0 && $headers) ? ['format' => 'underline'] : null; + foreach ($row as $k => $field) + { + $padding = $maxLengths[$k] - strlen($field); + $this->addString('| '); + $this->addString($field, $styles); + $this->addString(' ' . str_repeat(' ', $padding)); + } + + $this->addLine('|'); + } + + // Add the bottom border + $this->addLine('\\' . str_repeat('-', ($width)) . '/'); + } + + /** + * Add raw text to output buffer + * + * @param string $text The text to add + * @return $this + **/ + public function addRaw($text) + { + $this->response[] = array( + 'message' => $text + ); + + if ($this->isInteractive()) + { + $this->render(true); + } + } + + /** + * Helper method to add an array of lines to the output buffer. + * + * Here we're expecting an array, with each entry also containing an + * array with at least one key of 'message'. Another key + * can also be provided with a message type, which translates to + * one of the predefined styles used in formatLine(). + * + * @param array $lines Array of lines to add + * @return void + **/ + public function addLinesFromArray($lines) + { + foreach ($lines as $line) + { + $this->addLine($line['message'], ((isset($line['type'])) ? $line['type'] : null)); + } + } + + /** + * Helper method to add an associative array to the output buffer. + * + * @param array $lines The lines to add + * @return void + **/ + public function addRawFromAssocArray($lines, $indentation = 0) + { + foreach ($lines as $key => $value) + { + if (is_array($value)) + { + $this->addLine($key . ': [', array("indentation"=>$indentation)); + $this->addRawFromAssocArray($value, $indentation + 1); + $this->addLine(']', array("indentation"=>$indentation)); + } + else + { + $this->addLine($key . ': ' . $value, array("indentation"=>$indentation)); + } + } + } + + /** + * Add a blank line to the output + * + * @return $this + **/ + public function addSpacer() + { + $this->addLine(''); + + return $this; + } + + /** + * Send beep + * + * @return $this + **/ + public function beep() + { + echo chr(7); + + return $this; + } + + /** + * Send backspace + * + * @param int $spaces Number of spaces to back up + * @param bool $destructive Whether or not to destroy existing chars + * @return $this + **/ + public function backspace($spaces = 1, $destructive = false) + { + echo chr(27) . "[" . (int)$spaces . "D"; + + if ($destructive) + { + for ($i=0; $i < $spaces; $i++) + { + echo chr(32); + } + + echo chr(27) . "[" . (int)$spaces . "D"; + } + + return $this; + } + + /** + * Get response from the user + * + * @param string $prompt Question to ask user + * @return string + **/ + public function getResponse($prompt) + { + $prompt = trim($prompt); + $this->addString("{$prompt} "); + + $response = fgets(STDIN); + $response = trim($response); + + return $response; + } + + /** + * Shortcut function to print an error, render the error, and halt execution + * + * @param string $message Line of text used in error + * @return void + **/ + public function error($message) + { + $this->addLine($message, 'error'); + $this->render(); + exit(1); + } + + /** + * Set the default indentation. This will be used unless an indentation is + * explicitly given while adding a line. + * + * @param int $indentation Intiger of number of spaces to indent lines + * @return void + **/ + public function setDefaultIndentation($indentation) + { + $ind = ''; + for ($i=0; $i < (int) $indentation; $i++) + { + $ind .= ' '; + } + $this->defaultIndentation = $ind; + } + + /** + * Get our output subclass specialized for rendering help doc + * + * @return \Hubzero\Console\Output\Help + **/ + public function getHelpOutput() + { + $class = __NAMESPACE__ . '\\Output\\Help'; + + return new $class(); + } + + /** + * Get our output subclass specialized for a certain format + * + * @param string $format The format to get + * @return object + **/ + public function getOutputFormatter($format) + { + $class = __NAMESPACE__ . '\\Output\\' . ucfirst(strtolower($format)); + + if (class_exists($class)) + { + return new $class(); + } + else + { + return $this; + } + } + + /** + * Get our output subclass specialized for rendering progress tracking + * + * @return \Hubzero\Console\Output\Progress + **/ + public function getProgressOutput() + { + $class = __NAMESPACE__ . '\\Output\\Progress'; + + return new $class(); + } + + /** + * Take line of text and styles and give back a formatted line. + * + * This will also translate textual colors and formatting words + * to bash escape sequences. + * + * @param string $message Raw line of text + * @param mixed $styles String or array of styles + * @return void + **/ + private function formatLine(&$message, $styles) + { + $style = array( + 'format' => '0', + 'color' => '', + 'indentation' => $this->defaultIndentation + ); + + // If array, parse for individual style declarations + if (is_array($styles) && count($styles) > 0) + { + foreach ($styles as $k => $v) + { + switch ($k) + { + case 'color': + $style['color'] = $this->translateColor($v); + break; + + case 'format': + $style['format'] = $this->translateFormat($v); + break; + + case 'indentation': + $style['indentation'] = ''; + for ($i=0; $i < $v; $i++) + { + $style['indentation'] .= ' '; + } + break; + } + } + } + // If string, parse for predefined formatting key words + elseif (is_string($styles)) + { + switch ($styles) + { + case 'warning': + $style['color'] = '43'; + break; + + case 'error': + $style['format'] = '1'; + $style['color'] = '41'; + break; + + case 'info': + $style['color'] = $this->translateColor('blue'); + break; + + case 'success': + $style['color'] = $this->translateColor('green'); + break; + } + } + + if (!Config::get('color', $this->colored)) + { + $message = $style['indentation'] . $message; + } + else + { + $messageStyles = $style['format']; + $messageStyles .= ($style['color']) ? ';' . $style['color'] : ''; + $message = chr(27) . "[" . $messageStyles . "m" . $style['indentation'] . $message . chr(27) . "[0m"; + } + } + + /** + * Make output stream rather than pooled and dumped out at the end when render is called + * + * @return void + **/ + public function makeInteractive() + { + $this->isInteractive = true; + } + + /** + * Make output pooled + * + * @return void + **/ + public function makeNonInteractive() + { + $this->isInteractive = false; + } + + /** + * Check if output is streamed + * + * @return bool + **/ + public function isInteractive() + { + return $this->isInteractive; + } + + /** + * Set the output mode + * + * @return void + **/ + public function setMode($mode) + { + $this->mode = $mode; + } + + /** + * Get the output mode + * + * @return string + **/ + public function getMode() + { + return $this->mode; + } + + /** + * Make output colored + * + * @return void + **/ + public function makeColored() + { + $this->colored = true; + } + + /** + * Make output b&w + * + * @return void + **/ + public function makeUnColored() + { + $this->colored = false; + } + + /** + * Simple translation table to map color words to bash equivalents + * + * @param string $color Human readable color name + * @return string + **/ + private function translateColor($color) + { + $colors = array( + 'black' => '30', + 'red' => '31', + 'green' => '32', + 'yellow' => '33', + 'blue' => '34', + 'purple' => '35', + 'cyan' => '36', + 'white' => '37' + ); + + return $colors[$color]; + } + + /** + * Simple translation table to map formatting key words to bash equivalents + * + * @param string $format Human readable format name + * @return string + **/ + private function translateFormat($format) + { + $formats = array( + 'normal' => '0', + 'bold' => '1', + 'underline' => '4' + ); + + return $formats[$format]; + } +} diff --git a/core/libraries/Hubzero/Console/Output/Help.php b/core/libraries/Hubzero/Console/Output/Help.php new file mode 100644 index 00000000000..6c79c00bd8a --- /dev/null +++ b/core/libraries/Hubzero/Console/Output/Help.php @@ -0,0 +1,203 @@ +hasArgumentsSection = true; + + return $this; + } + + /** + * Add help output overview section + * + * @return $this + **/ + public function addOverview($text) + { + $this + ->addSection('Overview') + ->addParagraph( + $text, + array( + 'indentation' => 2 + ) + ) + ->addSpacer(); + + return $this; + } + + /** + * Adds help output tasks section + * + * @param object $command The command to introspect for tasks + * @return $this + * @since 2.0.0 + **/ + public function addTasks($command) + { + $this->addSection('Tasks'); + + $class = new \ReflectionClass($command); + $tasks = $class->getMethods(\ReflectionMethod::IS_PUBLIC); + + if ($tasks && count($tasks) > 0) + { + $list = []; + $max = 0; + + foreach ($tasks as $task) + { + if (!$task->isConstructor() && $task->name != 'execute' && $task->name != 'help') + { + $comment = $task->getDocComment(); + $description = 'no description available'; + + // Check for help ignore flag + preg_match('/@museDescription ([[:alnum:] ,\.()\-\'\/]*)/', $comment, $matched); + + if ($matched && isset($matched[1])) + { + $description = trim($matched[1]); + } + + $list[] = [ + 'name' => $task->name, + 'description' => $description + ]; + + $max = ($max > strlen($task->name)) ? $max : strlen($task->name); + } + } + + if (count($list) > 0) + { + foreach ($list as $item) + { + $this->addString($item['name'], [ + 'color' => 'blue', + 'indentation' => 2 + ]); + + $this->addString(str_repeat(' ', ($max - strlen($item['name']))) . ' '); + $this->addLine($item['description']); + } + } + else + { + $this->addLine('There are no tasks available for this command', [ + 'color' => 'red', + 'indentation' => 2 + ]); + } + } + + $this->addSpacer(); + + return $this; + } + + /** + * Add an argument entry to the help doc + * + * This is helpful in unifying styles used for help doc + * + * @param string $argument Actual argument + * @param string $details Description of what it does + * @param string $example Usage example + * @param string $required If it's required, we'll style a bit differently + * @return $this + **/ + public function addArgument($argument, $details = null, $example = null, $required = false) + { + if (!$this->hasArgumentsSection) + { + $this->addSection('Arguments'); + $this->hasArgumentsSection = true; + } + + $this->addLine( + $argument . (($required) ? ' (*required)' : ''), + array( + 'color' => (($required) ? 'red' : 'blue'), + 'indentation' => 2 + ) + ); + + if (isset($details)) + { + $this->addParagraph( + $details, + array( + 'indentation' => 4 + ) + ); + + if (!isset($example)) + { + $this->addSpacer(); + } + } + if (isset($example)) + { + $this->addLine( + $example, + array( + 'color' => 'green', + 'indentation' => 4 + ) + ) + ->addSpacer(); + } + + return $this; + } + + /** + * Helper method for adding a new section header to helper doc + * + * @return $this + **/ + public function addSection($text) + { + $this->addLine( + $text . ":", + array( + 'color' => 'yellow', + 'indentation' => 0 + ) + ); + + return $this; + } +} diff --git a/core/libraries/Hubzero/Console/Output/Json.php b/core/libraries/Hubzero/Console/Output/Json.php new file mode 100644 index 00000000000..84a7471b0f8 --- /dev/null +++ b/core/libraries/Hubzero/Console/Output/Json.php @@ -0,0 +1,70 @@ +setMode('minimal'); + $this->makeNonInteractive(); + } + + /** + * Render out stored output to command line + * + * @param bool $newLine Whether or not to include new line with each response (really only applies to interactive output) + * @return void + **/ + public function render($newLine = true) + { + // Make sure there is something there + if (isset($this->response) && count($this->response) > 0) + { + echo json_encode($this->response); + + // Reset response + $this->response = array(); + } + } + + /** + * Add a new line to the output buffer (not actually a real php output buffer) + * + * @param string $message Text of line + * @param mixed $styles Array of custom styles or string containing predefined term (see formatLine() for posibilities) + * @param bool $newLine Whether or not line should end with a new line + * @return $this + **/ + public function addLine($message, $styles = null, $newLine = true) + { + $styles = null; + $newLine = true; + if (is_array($message)) + { + $this->response[key($message)] = current($message); + } + else + { + $this->response[] = $message; + } + + return $this; + } +} diff --git a/core/libraries/Hubzero/Console/Output/Progress.php b/core/libraries/Hubzero/Console/Output/Progress.php new file mode 100644 index 00000000000..dd21a9cc6f4 --- /dev/null +++ b/core/libraries/Hubzero/Console/Output/Progress.php @@ -0,0 +1,115 @@ +makeInteractive(); + + if (isset($initMessage)) + { + // Add the intital message + $this->addString($initMessage); + + // Track some string lengths + $this->initMessageLength = strlen($initMessage); + } + + switch ($type) + { + case 'ratio': + $this->setProgress(0, $total); + break; + case 'percentage': + default: + // Set current progress to 0 + $this->setProgress('0'); + break; + } + } + + /** + * Set the current progress val + * + * @param int $val Progress value + * @param int $tot Total value + * @return void + **/ + public function setProgress($val, $tot = null) + { + if ($this->contentLength > 0) + { + // Back up current length of content + $this->backspace($this->contentLength); + } + + if (!is_null($tot)) + { + $content = "({$val}/{$tot})"; + } + else + { + // Get new content + $content = "{$val}%"; + } + + // Save length of content for next call + $this->contentLength = strlen($content); + + // Add the string + $this->addString($content); + } + + /** + * Finish progress output + * + * @return void + **/ + public function done() + { + // Compute the totall length of the output + $length = $this->contentLength + $this->initMessageLength; + + // Back up all the way + $this->backspace($length, true); + + // In case this gets used again... + $this->contentLength = 0; + $this->initMessageLength = 0; + } +} diff --git a/core/libraries/Hubzero/Console/OutputServiceProvider.php b/core/libraries/Hubzero/Console/OutputServiceProvider.php new file mode 100644 index 00000000000..588e526ff5f --- /dev/null +++ b/core/libraries/Hubzero/Console/OutputServiceProvider.php @@ -0,0 +1,93 @@ +app['output'] = function($app) + { + return new \Hubzero\Console\Output(); + }; + } + + /** + * Handle request in stack + * + * @param object $request Request + * @return mixed + */ + public function handle(Request $request) + { + $response = $this->next($request); + + $arguments = $this->app['arguments']; + $output = $this->app['output']; + + // Check for interactivity flag and set on output accordingly + if ($arguments->getOpt('non-interactive')) + { + $output->makeNonInteractive(); + } + + // Check for color flag and set on output accordingly + if ($arguments->getOpt('no-colors')) + { + $output->makeUnColored(); + } + + // If task is help, set the output to our output class with extra methods for rendering help doc + if ($arguments->get('task') == 'help') + { + $output = $output->getHelpOutput(); + } + + // If the format opt is present, try to use the appropriate output subclass + if ($arguments->getOpt('format')) + { + $output = $output->getOutputFormatter($arguments->getOpt('format')); + } + + // Register any user specific events + if ($hooks = Config::get('hooks')) + { + foreach ($hooks as $trigger => $scripts) + { + foreach ($scripts as $script) + { + Event::register($trigger, function() use ($script, $output) + { + if ($output->getMode() != 'minimal') + { + $output->addLine("Running '{$script}'"); + } + shell_exec(escapeshellcmd($script)); + }); + } + } + } + + // Reset the output stored on the application + $this->app->forget('output'); + $this->app->set('output', $output); + + return $response; + } +} diff --git a/core/libraries/Hubzero/Console/Tests/ArgumentsTest.php b/core/libraries/Hubzero/Console/Tests/ArgumentsTest.php new file mode 100644 index 00000000000..47d2e15caf1 --- /dev/null +++ b/core/libraries/Hubzero/Console/Tests/ArgumentsTest.php @@ -0,0 +1,229 @@ +env = 'environment'; + $aliases->t = 'test::run'; + $aliases->inst = 'repository:package::install'; + $configuration['aliases'] = $aliases; + + $config->shouldReceive('get')->andReturnUsing(function ($key, $default = null) use ($configuration) { + return (isset($configuration[$key])) ? $configuration[$key] : $default; + }); + } + + /** + * Tests to make sure we can parse for a basic command and task + * + * @return void + **/ + public function testParseBasicCommandAndTask() + { + $args = [ + 'muse', + 'repository', + 'update' + ]; + + $arguments = new Arguments($args); + $arguments->parse(); + + $this->assertEquals('Hubzero\Console\Command\Repository', $arguments->get('class'), 'Arguments parser failed to find the proper command'); + $this->assertEquals('update', $arguments->get('task'), 'Arguments parser failed to find the proper task'); + } + + /** + * Tests to make sure we can parse for a basic command and task within an alternate namespace + * + * @return void + **/ + public function testParseBasicCommandAndTaskFromAlternateLocation() + { + $args = [ + 'muse', + 'alternative:subcommand' + ]; + + Arguments::registerNamespace('Hubzero\Console\Tests\Mock\{$1}\Cli\Commands'); + + $arguments = new Arguments($args); + $arguments->parse(); + + $this->assertEquals('Hubzero\Console\Tests\Mock\Alternative\Cli\Commands\Subcommand', $arguments->get('class'), 'Arguments parser failed to find the proper command'); + $this->assertEquals('execute', $arguments->get('task'), 'Arguments parser failed to find the proper task'); + } + + /** + * Tests to make sure we can parse for a command and task within a nested command + * + * @return void + **/ + public function testParseCommandAndTaskWithNamespace() + { + $args = [ + 'muse', + 'repository:package', + 'install' + ]; + + $arguments = new Arguments($args); + $arguments->parse(); + + $this->assertEquals('Hubzero\Console\Command\Repository\Package', $arguments->get('class'), 'Arguments parser failed to find the proper command'); + $this->assertEquals('install', $arguments->get('task'), 'Arguments parser failed to find the proper task'); + } + + /** + * Tests to make sure we can parse for a command and task from an alias + * + * @return void + **/ + public function testParseCommandAndTaskFromAlias() + { + $args = [ + 'muse', + 'env' + ]; + + $arguments = new Arguments($args); + $arguments->parse(); + + $this->assertEquals('Hubzero\Console\Command\Environment', $arguments->get('class'), 'Arguments parser failed to find the proper command'); + $this->assertEquals('execute', $arguments->get('task'), 'Arguments parser failed to find the proper task'); + } + + /** + * Tests to make sure we can parse for a command and task from an alias with task embedded + * + * @return void + **/ + public function testParseCommandAndTaskFromAliasWithTaskEmbedded() + { + $args = [ + 'muse', + 't' + ]; + + $arguments = new Arguments($args); + $arguments->parse(); + + $this->assertEquals('Hubzero\Console\Command\Test', $arguments->get('class'), 'Arguments parser failed to find the proper command'); + $this->assertEquals('run', $arguments->get('task'), 'Arguments parser failed to find the proper task'); + } + + /** + * Tests to make sure we can parse for a command and task from an alias with task embedded and a namespace + * + * @return void + **/ + public function testParseCommandAndTaskFromAliasWithTaskEmbeddedAndNamespace() + { + $args = [ + 'muse', + 'inst' + ]; + + $arguments = new Arguments($args); + $arguments->parse(); + + $this->assertEquals('Hubzero\Console\Command\Repository\Package', $arguments->get('class'), 'Arguments parser failed to find the proper command'); + $this->assertEquals('install', $arguments->get('task'), 'Arguments parser failed to find the proper task'); + } + + /** + * Tests to make sure we get an exception for a bad/unknown command + * + * @expectedException \Hubzero\Console\Exception\UnsupportedCommandException + * @return void + **/ + public function testParseNonExistentCommand() + { + $args = [ + 'muse', + 'blah' + ]; + + $arguments = new Arguments($args); + $arguments->parse(); + } + + /** + * Tests to make sure we can parse options + * + * @return void + **/ + public function testParseOptions() + { + $args = [ + 'muse', + 'repository', + 'update', + '-a', + '--b', + '--c=foo', + '--d=1', + '--d=2', + '-ef', + '-g=bar', + '-h=1', + '-h=2', + '--i-j' + ]; + + $arguments = new Arguments($args); + $arguments->parse(); + + $this->assertTrue($arguments->getOpt('a'), 'Failed to detect parameter "a" as true'); + $this->assertTrue($arguments->getOpt('b'), 'Failed to detect parameter "b" as true'); + $this->assertEquals('foo', $arguments->getOpt('c'), 'Failed to get proper option value for "c"'); + $this->assertEquals(['1', '2'], $arguments->getOpt('d'), 'Failed to get proper option value for "d"'); + $this->assertTrue($arguments->getOpt('e'), 'Failed to detect parameter "e" as true'); + $this->assertTrue($arguments->getOpt('f'), 'Failed to detect parameter "f" as true'); + $this->assertEquals('bar', $arguments->getOpt('g'), 'Failed to get proper option value for "g"'); + $this->assertEquals(['1', '2'], $arguments->getOpt('h'), 'Failed to get proper option value for "h"'); + $this->assertTrue($arguments->getOpt('i-j'), 'Failed to detect parameter "i-j" as true'); + + $arguments->setOpt('i', 'foobar'); + $this->assertEquals('foobar', $arguments->getOpt('i'), 'Failed to get proper option value for "i"'); + + $arguments->deleteOpt('i'); + $this->assertFalse($arguments->getOpt('i'), 'Failed to properly delete option "i"'); + + $this->assertEquals([ + 'a' => true, + 'b' => true, + 'c' => 'foo', + 'd' => ['1', '2'], + 'e' => true, + 'f' => true, + 'g' => 'bar', + 'h' => ['1', '2'], + 'i-j' => true + ], $arguments->getOpts(), 'Failed to properly fetch all options'); + } +} diff --git a/core/libraries/Hubzero/Console/Tests/Mock/Alternative/Cli/Commands/Subcommand.php b/core/libraries/Hubzero/Console/Tests/Mock/Alternative/Cli/Commands/Subcommand.php new file mode 100644 index 00000000000..347716d173c --- /dev/null +++ b/core/libraries/Hubzero/Console/Tests/Mock/Alternative/Cli/Commands/Subcommand.php @@ -0,0 +1,39 @@ +getBuffered(function () use ($output) { + $output->addLine('Hello, friend'); + }); + + $this->assertEquals('\033[0mHello, friend\033[0m\n', $string, 'Output did not have the expected string with trailing new line'); + } + + /** + * Tests to make sure we can output a string + * + * @return void + **/ + public function testOutputString() + { + $output = new Output; + + $string = $this->getBuffered(function () use ($output) { + $output->addString('Hello, friend'); + }); + + $this->assertEquals('\033[0mHello, friend\033[0m', $string, 'Output did not have the expected string without trailing new line'); + } + + /** + * Tests to make sure string is not automatically rendered in non-interactive mode + * + * @return void + **/ + public function testOutputNonInteractiveDoesNotAutmaticallyRender() + { + $output = new Output; + $output->makeNonInteractive(); + + $string = $this->getBuffered(function () use ($output) { + $output->addLine('Hello, friend'); + }); + + $this->assertEquals('', $string, 'Output did not have the expected empty string'); + } + + /** + * Tests to make sure we can output a paragraph, properly limited in line length + * + * @return void + **/ + public function testOutputParagraph() + { + $output = new Output; + + $actual = $this->getBuffered(function () use ($output) { + $paragraph = 'PBR cred distillery, meggings farm-to-table craft beer pop-up before they sold out health goth.'; + $paragraph .= ' Crucifix drinking vinegar polaroid tote bag before they sold out, flexitarian plaid taxidermy.'; + $paragraph .= ' 90\'s cold-pressed pour-over pug asymmetrical small batch. Roof party freegan ennui single-ori'; + $paragraph .= 'gin coffee, Thundercats trust fund PBR&B flexitarian seitan kitsch bespoke taxidermy Pitchfork '; + $paragraph .= 'fixie kogi. Church-key typewriter readymade, Portland 8-bit whatever sriracha tofu blog DIY Aus'; + $paragraph .= 'tin. Street art twee salvia, cray McSweeney\'s put a bird on it trust fund ethical bicycle righ'; + $paragraph .= 'ts pop-up narwhal umami cronut tilde PBR&B. Selfies banjo VHS cardigan farm-to-table.'; + $output->addParagraph($paragraph); + }); + + $expected = '\033[0mPBR cred distillery, meggings farm-to-table craft beer pop-up before they\033[0m\n'; + $expected .= '\033[0msold out health goth. Crucifix drinking vinegar polaroid tote bag before\033[0m\n'; + $expected .= '\033[0mthey sold out, flexitarian plaid taxidermy. 90\'s cold-pressed pour-over\033[0m\n'; + $expected .= '\033[0mpug asymmetrical small batch. Roof party freegan ennui single-origin coffee,\033[0m\n'; + $expected .= '\033[0mThundercats trust fund PBR&B flexitarian seitan kitsch bespoke taxidermy\033[0m\n'; + $expected .= '\033[0mPitchfork fixie kogi. Church-key typewriter readymade, Portland 8-bit whatever\033[0m\n'; + $expected .= '\033[0msriracha tofu blog DIY Austin. Street art twee salvia, cray McSweeney\'s\033[0m\n'; + $expected .= '\033[0mput a bird on it trust fund ethical bicycle rights pop-up narwhal umami\033[0m\n'; + $expected .= '\033[0mcronut tilde PBR&B. Selfies banjo VHS cardigan farm-to-table.\033[0m\n'; + + $this->assertEquals($expected, $actual, 'Output did not have the expected string in appropriate paragraph format'); + } + + /** + * Tests to make sure we can output a cool table + * + * @return void + **/ + public function testOutputTable() + { + $output = new Output; + + $actual = $this->getBuffered(function () use ($output) { + $table = []; + $table[] = ['John', 'Football']; + $table[] = ['Stephen', 'Soccer']; + $table[] = ['Ben', 'Baseball']; + $output->addTable($table); + }); + + $expected = '\033[0m/--------------------\\\033[0m\n'; + $expected .= '\033[0m| \033[0m\033[0mJohn\033[0m\033[0m \033[0m\033[0m| \033[0m\033[0mFootball\033[0m\033[0m \033[0m\033[0m|\033[0m\n'; + $expected .= '\033[0m| \033[0m\033[0mStephen\033[0m\033[0m \033[0m\033[0m| \033[0m\033[0mSoccer\033[0m\033[0m \033[0m\033[0m|\033[0m\n'; + $expected .= '\033[0m| \033[0m\033[0mBen\033[0m\033[0m \033[0m\033[0m| \033[0m\033[0mBaseball\033[0m\033[0m \033[0m\033[0m|\033[0m\n'; + $expected .= '\033[0m\--------------------/\033[0m\n'; + + $this->assertEquals($expected, $actual, 'Output did not have the expected table'); + } +} diff --git a/core/libraries/Hubzero/Container/Container.php b/core/libraries/Hubzero/Container/Container.php new file mode 100644 index 00000000000..1d2bbdb4795 --- /dev/null +++ b/core/libraries/Hubzero/Container/Container.php @@ -0,0 +1,328 @@ +factories = new SplObjectStorage(); + $this->protected = new SplObjectStorage(); + + foreach ($values as $key => $value) + { + $this->offsetSet($key, $value); + } + } + + /** + * Sets a parameter or an object. + * + * Objects must be defined as Closures. + * + * Allowing any PHP callable leads to difficult to debug problems + * as function names (strings) are callable (creating a function with + * the same name as an existing parameter would break your container). + * + * @param string $id The unique identifier for the parameter or object + * @param mixed $value The value of the parameter or a closure to define an object + * @throws \RuntimeException Prevent override of a frozen service + */ + public function offsetSet($id, $value) + { + if (isset($this->frozen[$id])) + { + throw new RuntimeException(sprintf('Cannot override frozen service "%s".', $id)); + } + + $this->values[$id] = $value; + $this->keys[$id] = true; + } + + /** + * Gets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * @return mixed The value of the parameter or an object + */ + public function offsetGet($id) + { + if (!isset($this->keys[$id])) + { + throw new InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); + } + + // If an instance of the type is currently being managed as a raw value, + // protected value, or is a parameter. + if (isset($this->raw[$id]) + || !is_object($this->values[$id]) + || isset($this->protected[$this->values[$id]]) + || !method_exists($this->values[$id], '__invoke')) + { + return $this->values[$id]; + } + + // If an instance of the type is currently being managed as a multiton we'll + // return a new instance. + if (isset($this->factories[$this->values[$id]])) + { + return $this->values[$id]($this); + } + + $raw = $this->values[$id]; + $val = $this->values[$id] = $raw($this); + $this->raw[$id] = $raw; + + $this->frozen[$id] = true; + + return $val; + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id The unique identifier for the parameter or object + * @return bool + */ + public function offsetExists($id) + { + return isset($this->keys[$id]); + } + + /** + * Unsets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + */ + public function offsetUnset($id) + { + if (isset($this->keys[$id])) + { + if (is_object($this->values[$id])) + { + unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]); + } + + unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]); + } + } + + /** + * Sets a parameter or an object. + * + * Objects must be defined as Closures. + * + * Allowing any PHP callable leads to difficult to debug problems + * as function names (strings) are callable (creating a function with + * the same name as an existing parameter would break your container). + * + * @param string $id The unique identifier for the parameter or object + * @param mixed $value The value of the parameter or a closure to define an object + * @throws \RuntimeException Prevent override of a frozen service + */ + public function set($id, $value) + { + return $this->offsetSet($id, $value); + } + + /** + * Gets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * @return mixed The value of the parameter or an object + * @throws \InvalidArgumentException if the identifier is not defined + */ + public function get($id) + { + return $this->offsetGet($id); + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id The unique identifier for the parameter or object + * + * @return bool + */ + public function has($id) + { + return $this->offsetExists($id); + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id The unique identifier for the parameter or object + * + * @return bool + */ + public function forget($id) + { + return $this->offsetUnset($id); + } + + /** + * Marks a callable as being a factory service. + * + * @param callable $callable A service definition to be used as a factory + * @return callable The passed callable + * @throws InvalidArgumentException Service definition has to be a closure of an invokable object + */ + public function factory($callable) + { + if (!is_object($callable) || !method_exists($callable, '__invoke')) + { + throw new InvalidArgumentException('Service definition is not a Closure or invokable object.'); + } + + $this->factories->attach($callable); + + return $callable; + } + + /** + * Protects a callable from being interpreted as a service. + * + * This is useful when you want to store a callable as a parameter. + * + * @param callable $callable A callable to protect from being evaluated + * @return callable The passed callable + * @throws \InvalidArgumentException Service definition has to be a closure of an invokable object + */ + public function protect($callable) + { + if (!is_object($callable) || !method_exists($callable, '__invoke')) + { + throw new InvalidArgumentException('Callable is not a Closure or invokable object.'); + } + + $this->protected->attach($callable); + + return $callable; + } + + /** + * Gets a parameter or the closure defining an object. + * + * @param string $id The unique identifier for the parameter or object + * @return mixed The value of the parameter or the closure defining an object + * @throws \InvalidArgumentException if the identifier is not defined + */ + public function raw($id) + { + if (!isset($this->keys[$id])) + { + throw new InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); + } + + if (isset($this->raw[$id])) + { + return $this->raw[$id]; + } + + return $this->values[$id]; + } + + /** + * Extends an object definition. + * + * Useful when you want to extend an existing object definition, + * without necessarily loading that object. + * + * @param string $id The unique identifier for the object + * @param callable $callable A service definition to extend the original + * @return callable The wrapped callable + * @throws \InvalidArgumentException if the identifier is not defined or not a service definition + */ + public function extend($id, $callable) + { + if (!isset($this->keys[$id])) + { + throw new InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id)); + } + + if (!is_object($this->values[$id]) || !method_exists($this->values[$id], '__invoke')) + { + throw new InvalidArgumentException(sprintf('Identifier "%s" does not contain an object definition.', $id)); + } + + if (!is_object($callable) || !method_exists($callable, '__invoke')) + { + throw new InvalidArgumentException('Extension service definition is not a Closure or invokable object.'); + } + + $factory = $this->values[$id]; + + $extended = function ($c) use ($callable, $factory) + { + return $callable($factory($c), $c); + }; + + if (isset($this->factories[$factory])) + { + $this->factories->detach($factory); + $this->factories->attach($extended); + } + + return $this[$id] = $extended; + } + + /** + * Returns all defined value names. + * + * @return array An array of value names + */ + public function keys() + { + return array_keys($this->values); + } + + /** + * Registers a service provider. + * + * @param object $provider A ServiceProviderInterface instance + * @param array $options An array of values that customizes the provider + * @return static + */ + public function register($provider, $options = array()) + { + $provider->register($this); + + foreach ($options as $key => $value) + { + $this[$key] = $value; + } + + return $this; + } +} diff --git a/core/libraries/Hubzero/Container/ServiceProviderInterface.php b/core/libraries/Hubzero/Container/ServiceProviderInterface.php new file mode 100644 index 00000000000..5fcf28ea3fc --- /dev/null +++ b/core/libraries/Hubzero/Container/ServiceProviderInterface.php @@ -0,0 +1,26 @@ + 'value'); + + $container = new Container($params); + + $this->assertSame($params['param'], $container['param']); + } + + /** + * Test setting and getting a string + * + * @covers \Hubzero\Container\Container::set + * @covers \Hubzero\Container\Container::offsetSet + * @covers \Hubzero\Container\Container::get + * @covers \Hubzero\Container\Container::offsetGet + * @return void + **/ + public function testWithString() + { + $container = new Container(); + $container['param'] = 'value'; + + $this->assertEquals('value', $container['param']); + + $this->assertTrue($container->has('param')); + + $container->set('foo', 'bar'); + + $this->assertEquals('bar', $container->get('foo')); + + $this->setExpectedException('InvalidArgumentException'); + + $container->get('lorem'); + } + + /** + * Test setting and getting a string + * + * @covers \Hubzero\Container\Container::set + * @covers \Hubzero\Container\Container::offsetSet + * @covers \Hubzero\Container\Container::get + * @covers \Hubzero\Container\Container::offsetGet + * @return void + **/ + public function testWithClosure() + { + $container = new Container(); + $container['service'] = function () + { + return new Service(); + }; + + $this->assertInstanceOf(Service::class, $container['service']); + } + + /** + * Test checking for a parameter being set or not + * + * @covers \Hubzero\Container\Container::has + * @covers \Hubzero\Container\Container::offsetExists + * @return void + **/ + public function testHas() + { + $container = new Container(); + $container['param'] = 'value'; + + $this->assertTrue(isset($container['param'])); + + $this->assertFalse(isset($container['foo'])); + + $container->set('foo', 'bar'); + + $this->assertTrue($container->has('foo')); + + $this->assertFalse($container->has('ipsum')); + } + + /** + * Test unsetting a parameter + * + * @covers \Hubzero\Container\Container::forget + * @covers \Hubzero\Container\Container::offsetUnset + * @return void + **/ + public function testForget() + { + $container = new Container(); + $container['param'] = 'value'; + + $this->assertTrue(isset($container['param'])); + + unset($container['param']); + + $this->assertFalse(isset($container['param'])); + + $container->set('foo', 'bar'); + + $this->assertTrue($container->has('foo')); + + $container->forget('foo'); + + $this->assertFalse($container->has('foo')); + } + + /** + * Test getting defined value names + * + * @covers \Hubzero\Container\Container::keys + * @return void + **/ + public function testKeys() + { + $container = new Container(); + $container->set('foo', 'bar'); + $container->set('bar', 'foo'); + + $this->assertEquals(array('foo', 'bar'), $container->keys()); + } + + /** + * Test getting raw value + * + * @covers \Hubzero\Container\Container::raw + * @return void + **/ + public function testRaw() + { + $container = new Container(); + + $service = function () + { + return 'foo'; + }; + + $container['service'] = $service; + + $this->assertSame($service, $container->raw('service')); + + $this->setExpectedException('InvalidArgumentException'); + + $container->raw('lorem'); + } + + /** + * Test that factory services are different + * + * @covers \Hubzero\Container\Container::factory + * @return void + **/ + public function testServicesShouldBeDifferent() + { + $container = new Container(); + + $container['service'] = $container->factory(function () { + return new Service(); + }); + + $serviceOne = $container['service']; + + $this->assertInstanceOf(__NAMESPACE__ . '\Mock\Service', $serviceOne); + + $serviceTwo = $container['service']; + + $this->assertInstanceOf(__NAMESPACE__ . '\Mock\Service', $serviceTwo); + + $this->assertNotSame($serviceOne, $serviceTwo); + } + + /** + * Test that extend() throws an exception when a key is undefined + * + * @covers \Hubzero\Container\Container::extend + * @return void + **/ + public function testExtendThrowsExceptionWithUndefinedKey() + { + $container = new Container(); + + $this->setExpectedException('InvalidArgumentException'); + + $container->extend( + 'lorem', + function () + { + return 'ipsum'; + } + ); + } + + /** + * Test that extend() throws an exception when a definition isn't callable + * + * @covers \Hubzero\Container\Container::extend + * @return void + **/ + public function testExtendThrowsExceptionWithInvalidDefinition() + { + $container = new Container(); + $container['param'] = 'value'; + + $this->setExpectedException('InvalidArgumentException'); + + $container->extend( + 'param', + function () + { + return 'ipsum'; + } + ); + } + + /** + * Test that extend() throws an exception when an extension isn't callable + * + * @covers \Hubzero\Container\Container::extend + * @return void + **/ + public function testExtendThrowsExceptionWithUncallableExtension() + { + $container = new Container(); + $container['param'] = 'value'; + + $this->setExpectedException('InvalidArgumentException'); + + $container->extend( + 'param', + 'ipsum' + ); + } + + /** + * Test extending a service + * + * @covers \Hubzero\Container\Container::extend + * @return void + **/ + public function testExtendingService() + { + $container = new Container(); + $container['foo'] = function () + { + return 'foo'; + }; + + $container['foo'] = $container->extend('foo', function ($foo, $app) + { + return "$foo.bar"; + }); + + $container['foo'] = $container->extend('foo', function ($foo, $app) + { + return "$foo.baz"; + }); + + $this->assertSame('foo.bar.baz', $container['foo']); + } +} diff --git a/core/libraries/Hubzero/Container/Tests/Mock/Service.php b/core/libraries/Hubzero/Container/Tests/Mock/Service.php new file mode 100755 index 00000000000..83fe16d79f5 --- /dev/null +++ b/core/libraries/Hubzero/Container/Tests/Mock/Service.php @@ -0,0 +1,18 @@ +scope = $scope; + + if ($logger) + { + $this->log = $logger; + } + } + + /** + * Set logging + * + * @param object $logger + * @return void + */ + public function setLogger($logger) + { + $this->log = $logger; + } + + /** + * Process data against a series of tests + * + * @param string|array $data + * @return object + */ + public function check($data) + { + $results = array(); + + $data = $this->prepareData($data); + + foreach ($data as $datum) + { + $results[] = array( + 'data' => $datum, + 'tests' => $this->process($datum) + ); + } + + if ($this->log) + { + $this->log($results); + } + + return $results; + } + + /** + * Run the tests against a single data point + * + * @param array $data + * @return array + */ + public function process(array $datum) + { + $results = array(); + + foreach ($this->tests as $key => $tester) + { + $result = $tester->examine($datum); + $result->set('scope', $this->scope); + $result->set('test_id', $key); + + $results[$tester->name()] = $result; + } + + return $results; + } + + /** + * Register a test + * + * @param object $test TestInterface + * @return object Test + * @throws RuntimeException + */ + public function registerTest(Test $test) + { + $key = $this->classSimpleName($test); + + if (isset($this->tests[$key])) + { + throw new RuntimeException( + sprintf('Test [%s] already registered', $key) + ); + } + + $this->tests[$key] = $test; + + return $this; + } + + /** + * Unregister a test + * + * @param string $key + * @return object + */ + public function unregisterTest($key) + { + $key = $this->classSimpleName($key); + + if (isset($this->tests[$key])) + { + unset($this->tests[$key]); + } + + return $this; + } + + /** + * Gets a detector using its detector ID (Class Simple Name) + * + * @param string $key + * @return mixed False or TestInterface + */ + public function getTest($key) + { + if (!isset($this->tests[$key])) + { + return false; + } + + return $this->tests[$key]; + } + + /** + * Gets a list of all spam detectors + * + * @return array + */ + public function getTests() + { + return $this->tests; + } + + /** + * Get summations + * + * @return string + */ + public function getReport() + { + if (!isset($this->report)) + { + $tests = array(); + + foreach ($this->getTests() as $key => $audit) + { + if (!isset($tests[$key])) + { + $tests[$key] = array( + 'name' => $audit->name(), + 'total' => 0, + 'totals' => array( + 'skipped' => 0, + 'passed' => 0, + 'failed' => 0 + ) + ); + } + + $tests[$key]['totals']['skipped'] = Result::all() + ->whereEquals('scope', $this->scope) + ->whereEquals('test_id', $key) + ->whereEquals('status', 0) + ->total(); + + $tests[$key]['totals']['passed'] = Result::all() + ->whereEquals('scope', $this->scope) + ->whereEquals('test_id', $key) + ->whereEquals('status', 1) + ->total(); + + $tests[$key]['totals']['failed'] = Result::all() + ->whereEquals('scope', $this->scope) + ->whereEquals('test_id', $key) + ->whereEquals('status', -1) + ->total(); + + $tests[$key]['total'] += $tests[$key]['totals']['skipped']; + $tests[$key]['total'] += $tests[$key]['totals']['passed']; + $tests[$key]['total'] += $tests[$key]['totals']['failed']; + } + + $this->report = $tests; + } + + return $this->report; + } + + /** + * Used to normalize string before passing + * it to detectors + * + * @param array $data + * @return string + */ + protected function prepareData($data) + { + if (is_string($data)) + { + $data = array($data); + } + + return $data; + } + + /** + * Gets the name of a class (w. Namespaces removed) + * + * @param mixed $class String (class name) or object + * @return string + */ + protected function classSimpleName($class) + { + if (is_object($class)) + { + $class = get_class($class); + } + + return str_replace('\\', '_', $class); + } + + /** + * Log results of the check + * + * @param string $isSpam Spam detection result + * @param array $data Data being checked + * @return void + */ + protected function log($report) + { + if (!$this->log) + { + return; + } + + $this->log->info(json_encode($report)); + } +} diff --git a/core/libraries/Hubzero/Content/Auditor/Result.php b/core/libraries/Hubzero/Content/Auditor/Result.php new file mode 100644 index 00000000000..5e0d94e48e7 --- /dev/null +++ b/core/libraries/Hubzero/Content/Auditor/Result.php @@ -0,0 +1,140 @@ + 'notempty', + 'scope_id' => 'positive|nonzero', + 'test_id' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + **/ + public $always = array( + 'processed' + ); + + /** + * Set processed timestamp + * + * @param array $data the data being saved + * @return string + */ + public function automaticProcessed($data) + { + $dt = new \Hubzero\Utility\Date(); + return $dt->toSql(); + } + + /** + * Did the test pass? + * + * @return bool + */ + public function passed() + { + return ($this->get('status') == 1); + } + + /** + * Did the test fail? + * + * @return bool + */ + public function failed() + { + return ($this->get('status') == -1); + } + + /** + * Was the test skipped? + * + * @return bool + */ + public function skipped() + { + return ($this->get('status') == 0); + } + + /** + * Transform answer + * + * @return string + */ + public function transformStatus() + { + if ($this->skipped()) + { + return 'skipped'; + } + + if ($this->passed()) + { + return 'passed'; + } + + if ($this->failed()) + { + return 'failed'; + } + + return $this->get('status'); + } + + /** + * Load a record by scope and scope_id + * + * @param string $scope + * @param integer $scope_id + * @return object + */ + public static function oneByScope($scope, $scope_id) + { + return self::all() + ->whereEquals('scope', $scope) + ->whereEquals('scope_id', $scope_id) + ->ordered() + ->row(); + } +} diff --git a/core/libraries/Hubzero/Content/Auditor/Test.php b/core/libraries/Hubzero/Content/Auditor/Test.php new file mode 100644 index 00000000000..44639195802 --- /dev/null +++ b/core/libraries/Hubzero/Content/Auditor/Test.php @@ -0,0 +1,22 @@ +getDatapath(), $this->delimiter); + + // iterate over each row + foreach ($iterator as $row => $data) + { + // if we got back null for a row dont count + if ($data !== null) + { + $data = array_map('trim', (array)$data); + + if (array_filter($data)) + { + $this->data_count++; + } + } + } + + // return count + return $this->data_count; + } + + /** + * Get a list of headers + * + * @param object $import + * @return array + */ + public function headers(Import $import) + { + // create iterator + $iterator = new Reader($import->getDatapath(), $this->delimiter); + + return $iterator->headers(); + } + + /** + * Process Import data + * + * @param object $import Import record + * @param array $callbacks Array of Callbacks + * @param bool $dryRun Dry Run mode? + * @return object + */ + public function process(Import $import, array $callbacks, $dryRun) + { + // create new iterator + $iterator = new Reader($import->getDatapath(), $this->delimiter); + + // get the import params + $options = new Parameter($import->get('params')); + + // get the mode + $mode = $import->get('mode', 'UPDATE'); + + // loop through each item + foreach ($iterator as $index => $record) + { + // make sure we have a record + if ($record === null) + { + continue; + } + + // Make sure we didn't get an empty row + $data = array_map('trim', (array)$record); + if (!array_filter($data)) + { + continue; + } + + // do we have a post parse callback ? + $record = $this->map($record, $callbacks['postparse'], $dryRun); + + // convert to resource objects + $entry = $import->getRecord($record, $options->toArray(), $mode); + + // do we have a post map callback ? + $entry = $this->map($entry, $callbacks['postmap'], $dryRun); + + // run resource check & store + $entry->check()->store($dryRun); + + // do we have a post convert callback ? + $entry = $this->map($entry, $callbacks['postconvert'], $dryRun); + + // add to data array + array_push($this->data, $entry); + + // mark record processed + $import->currentRun()->processed(1); + } + + return $this->data; + } + + /** + * Run Callbacks on Record + * + * @param object $record Resource Record + * @param array $callbacks Array of Callbacks + * @param bool $dryRun Dry Run mode? + * @return object Record object + */ + public function map($record, $callbacks, $dryRun) + { + foreach ($callbacks as $callback) + { + if (is_callable($callback)) + { + $record = $callback($record, $dryRun); + } + } + + return $record; + } +} diff --git a/core/libraries/Hubzero/Content/Import/Adapter/Csv/Reader.php b/core/libraries/Hubzero/Content/Import/Adapter/Csv/Reader.php new file mode 100644 index 00000000000..8a9eecff269 --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Adapter/Csv/Reader.php @@ -0,0 +1,204 @@ +file = fopen($file, 'r'); + + ini_set('auto_detect_line_endings', false); + + $this->delimiter = $delimiter; + } + + /** + * Get the first row of headers + * + * @return object Row as a stdClass + */ + public function headers() + { + if (!$this->headers) + { + $this->rewind(); + + $row = fgetcsv($this->file, self::ROW_LENGTH, $this->delimiter); + + $this->position++; + + // store headers for later + if ($this->position == 1) + { + $this->headers = $row; + } + + $this->rewind(); + } + + return $this->headers; + } + + /** + * Get the current row + * + * @return object Row as a stdClass + */ + public function current() + { + $row = fgetcsv($this->file, self::ROW_LENGTH, $this->delimiter); + $this->position++; + + // store headers for later + if ($this->position == 1) + { + $this->headers = $row; + } + + // return null for the first row and last row if empty + // we dont want to count the headings row + if ($this->position == 1 || $row === false) + { + return null; + } + + // map headings + $object = new stdClass; + foreach ($this->headers as $k => $header) + { + $header = trim($header); + $header = trim($header, ':'); + $header = ($header ?: 'COLUMN'); + + // If a column header contains a colon, we break it + // into a sub-object with properties. + // + // Address:street, Address:city + // -> Address => { street = data, city = data } + // + if (strpos($header, ':')) + { + $parts = explode(':', $header); + + // Make sure we have more than one part + if (count($parts) > 1) + { + if (!isset($object->{$parts[0]}) || !is_object($object->{$parts[0]})) + { + $object->{$parts[0]} = new stdClass; + } + $object->{$parts[0]}->{$parts[1]} = $row[$k]; + } + else + { + $object->$header = $row[$k]; + } + } + else + { + $object->$header = $row[$k]; + } + } + + // return as object + return $object; + } + + /** + * Get our current position while iterating + * + * @return integer Current position + */ + public function key() + { + return $this->position; + } + + /** + * Go to the next row that matches our key + * + * @return void + */ + public function next() + { + return !feof($this->file); + } + + /** + * Move to the first row that matches our key + * + * @return void + */ + public function rewind() + { + $this->position = 0; + rewind($this->file); + } + + /** + * Is our current row valid + * + * @return boolean Is valid? + */ + public function valid() + { + if (!$this->next()) + { + fclose($this->file); + return false; + } + return true; + } +} diff --git a/core/libraries/Hubzero/Content/Import/Adapter/Excel.php b/core/libraries/Hubzero/Content/Import/Adapter/Excel.php new file mode 100644 index 00000000000..3ed79fdc7b0 --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Adapter/Excel.php @@ -0,0 +1,164 @@ +getDatapath(), $this->key); + + // return count + return $iterator->total(); + } + + /** + * Get a list of headers + * + * @param object $import + * @return array + */ + public function headers(Import $import) + { + // create iterator + $iterator = new Reader($import->getDataPath(), $this->key); + + return $iterator->headers(); + } + + /** + * Process Import data + * + * @param object $import Import record + * @param array $callbacks Array of Callbacks + * @param bool $dryRun Dry Run mode? + * @return object + */ + public function process(Import $import, array $callbacks, $dryRun) + { + // create new iterator + $iterator = new Reader($import->getDatapath(), $this->key); + + // get the import params + $options = new Parameter($import->get('params')); + + // get the mode + $mode = $import->get('mode', 'UPDATE'); + + // loop through each item + foreach ($iterator as $index => $record) + { + // make sure we have a record + if ($record === null) + { + continue; + } + + // do we have a post parse callback ? + $record = $this->map($record, $callbacks['postparse'], $dryRun); + + // convert to objects + $entry = $import->getRecord($record, $options->toArray(), $mode); + + // do we have a post map callback ? + $entry = $this->map($entry, $callbacks['postmap'], $dryRun); + + // run check & store + $entry->check()->store($dryRun); + + // do we have a post convert callback ? + $entry = $this->map($entry, $callbacks['postconvert'], $dryRun); + + // add to data array + array_push($this->data, $entry); + + // mark record processed + $import->currentRun()->processed(1); + } + + return $this->data; + } + + /** + * Run Callbacks on Record + * + * @param object $record Resource Record + * @param array $callbacks Array of Callbacks + * @param bool $dryRun Dry Run mode? + * @return object Record object + */ + public function map($record, $callbacks, $dryRun) + { + foreach ($callbacks as $callback) + { + if (is_callable($callback)) + { + $record = $callback($record, $dryRun); + } + } + + return $record; + } +} diff --git a/core/libraries/Hubzero/Content/Import/Adapter/Excel/Reader.php b/core/libraries/Hubzero/Content/Import/Adapter/Excel/Reader.php new file mode 100644 index 00000000000..aec9f80e89d --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Adapter/Excel/Reader.php @@ -0,0 +1,184 @@ +position = 0; + $this->file = $file; + + try + { + $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($file); + $reader->setReadDataOnly(true); + $spreadsheet = $reader->load($file); + + $sheet = $spreadsheet->getActiveSheet()->toArray(null, true, true, true); + + $this->headers = array_shift($sheet); + + $this->sheet = $sheet; + + $this->rows = count($this->sheet); + $this->cols = count($this->headers); + } + catch (\Exception $e) + { + die($e->getMessage()); + } + } + + /** + * Get the record total + * + * @return integer + */ + public function total() + { + return $this->rows; + } + + /** + * Get the list of headers + * + * @return array + */ + public function headers() + { + if (!$this->headers) + { + $this->headers = $this->sheet[1]; + } + + return $this->headers; + } + + /** + * Get the current row + * + * @return object Row node as a stdClass + */ + public function current() + { + $headers = $this->headers(); + + $result = new stdClass; + + $currentRow = $this->sheet[$this->position]; + + // Excel documents have alphanumeric columns + foreach ($headers as $col => $column) + { + $result->$column = (isset($currentRow[$col]) ? $currentRow[$col] : ''); + } + + return $result; + } + + /** + * Get our current position while iterating + * + * @return integer Current position + */ + public function key() + { + return $this->position; + } + + /** + * Go to the next Node that matches our key + * + * @return void + */ + public function next() + { + ++$this->position; + } + + /** + * Move to the first node that matches our key + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Is our current node valid + * + * @return boolean Is valid? + */ + public function valid() + { + return ($this->position < $this->rows); + } +} diff --git a/core/libraries/Hubzero/Content/Import/Adapter/Json.php b/core/libraries/Hubzero/Content/Import/Adapter/Json.php new file mode 100644 index 00000000000..49ba0e08da4 --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Adapter/Json.php @@ -0,0 +1,172 @@ +getDatapath(), $this->key); + + // iterate over each row + $this->data_count = iterator_count($iterator); + + // return count + return $this->data_count; + } + + /** + * Get a list of headers + * + * @param object $import + * @return array + */ + public function headers(Import $import) + { + // create iterator + $iterator = new Reader($import->getDataPath(), $this->key); + $iterator->rewind(); + + $headers = array(); + + $row = $iterator->current(); + foreach ($row as $key => $val) + { + $headers[] = $key; + } + + // return count + return $headers; + } + + /** + * Process Import data + * + * @param object $import Import record + * @param array $callbacks Array of Callbacks + * @param bool $dryRun Dry Run mode? + * @return object + */ + public function process(Import $import, array $callbacks, $dryRun) + { + // create new iterator + $iterator = new Reader($import->getDatapath(), $this->key); + + // get the import params + $options = new Parameter($import->get('params')); + + // get the mode + $mode = $import->get('mode', 'UPDATE'); + + // loop through each item + foreach ($iterator as $index => $record) + { + // make sure we have a record + if ($record === null) + { + continue; + } + + // do we have a post parse callback ? + $record = $this->map($record, $callbacks['postparse'], $dryRun); + + // convert to objects + $entry = $import->getRecord($record, $options->toArray(), $mode); + + // do we have a post map callback ? + $entry = $this->map($entry, $callbacks['postmap'], $dryRun); + + // run check & store + $entry->check()->store($dryRun); + + // do we have a post convert callback ? + $entry = $this->map($entry, $callbacks['postconvert'], $dryRun); + + // add to data array + array_push($this->data, $entry); + + // mark record processed + $import->currentRun()->processed(1); + } + + return $this->data; + } + + /** + * Run Callbacks on Record + * + * @param object $record Resource Record + * @param array $callbacks Array of Callbacks + * @param bool $dryRun Dry Run mode? + * @return object Record object + */ + public function map($record, $callbacks, $dryRun) + { + foreach ($callbacks as $callback) + { + if (is_callable($callback)) + { + $record = $callback($record, $dryRun); + } + } + + return $record; + } +} diff --git a/core/libraries/Hubzero/Content/Import/Adapter/Json/Reader.php b/core/libraries/Hubzero/Content/Import/Adapter/Json/Reader.php new file mode 100644 index 00000000000..b3a54c985ea --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Adapter/Json/Reader.php @@ -0,0 +1,105 @@ +position = 0; + $this->file = json_decode(file_get_contents($file), true); + $this->key = $key; + } + + /** + * Get the current XML node + * + * @return object XML node as a stdClass + */ + public function current() + { + if ($this->valid()) + { + return $this->file[$this->position]; + } + return null; + } + + /** + * Get our current position while iterating + * + * @return int Current position + */ + public function key() + { + return $this->position; + } + + /** + * Go to the next Node that matches our key + * + * @return void + */ + public function next() + { + ++$this->position; + } + + /** + * Move to the first node that matches our key + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Is our current node valid + * + * @return bool Is valid? + */ + public function valid() + { + return isset($this->file[$this->position]); + } +} diff --git a/core/libraries/Hubzero/Content/Import/Adapter/Xml.php b/core/libraries/Hubzero/Content/Import/Adapter/Xml.php new file mode 100644 index 00000000000..1f7eba9e289 --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Adapter/Xml.php @@ -0,0 +1,168 @@ +getDatapath(), $this->key); + + // count records + $this->data_count = iterator_count($xmlIterator); + + // return count + return $this->data_count; + } + + /** + * Get a list of headers + * + * @param object $import + * @return array + */ + public function headers(Import $import) + { + // create iterator + $iterator = new Reader($import->getDataPath(), $this->key); + $iterator->rewind(); + + $headers = array(); + + $row = $iterator->current(); + foreach ($row as $key => $val) + { + $headers[] = $key; + } + + // return count + return $headers; + } + + /** + * Process Import data + * + * @param object $import Import record + * @param array $callbacks Array of Callbacks + * @param bool $dryRun Dry Run mode? + * @return object + */ + public function process(Import $import, array $callbacks, $dryRun) + { + // create new xml reader + $iterator = new Reader($import->getDataPath(), $this->key); + + // get the import params + $options = new Parameter($import->get('params')); + + // get the mode + $mode = $import->get('mode', 'UPDATE'); + + // loop through each item + foreach ($iterator as $index => $record) + { + // make sure we have a record + if ($record === null) + { + continue; + } + + // do we have a post parse callback ? + $record = $this->map($record, $callbacks['postparse'], $dryRun); + + // convert to objects + $entry = $import->getRecord($record, $options->toArray(), $mode); + + // do we have a post map callback ? + $entry = $this->map($entry, $callbacks['postmap'], $dryRun); + + // run check & store + $entry->check()->store($dryRun); + + // do we have a post convert callback ? + $entry = $this->map($entry, $callbacks['postconvert'], $dryRun); + + // add to data array + array_push($this->data, $entry); + + // mark record processed + $import->currentRun()->processed(1); + } + + return $this->data; + } + + /** + * Run Callbacks on Record + * + * @param object $record Resource Record + * @param array $callbacks Array of Callbacks + * @param bool $dryRun Dry Run mode? + * @return object Record object + */ + public function map($record, $callbacks, $dryRun) + { + foreach ($callbacks as $callback) + { + if (is_callable($callback)) + { + $record = $callback($record, $dryRun); + } + } + + return $record; + } +} diff --git a/core/libraries/Hubzero/Content/Import/Adapter/Xml/Reader.php b/core/libraries/Hubzero/Content/Import/Adapter/Xml/Reader.php new file mode 100644 index 00000000000..b9173fa031f --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Adapter/Xml/Reader.php @@ -0,0 +1,124 @@ +reader = new XMLReader(); + $this->position = 0; + $this->file = $file; + $this->key = $key; + } + + /** + * Get the current XML node + * + * @return object XML node as a stdClass + */ + public function current() + { + $doc = new DOMDocument(); + $object = simplexml_import_dom($doc->importNode($this->reader->expand(), true)); + return json_decode(json_encode($object)); + } + + /** + * Get our current position while iterating + * + * @return int Current position + */ + public function key() + { + return $this->position; + } + + /** + * Go to the next Node that matches our key + * + * @return void + */ + public function next() + { + if ($this->reader->next($this->key)) + { + ++$this->position; + } + } + + /** + * Move to the first node that matches our key + * + * @return void + */ + public function rewind() + { + // open file with reader + // force UTF-8, validate XML, & substitute entities while reading + $this->reader->open($this->file, 'UTF-8', XMLReader::VALIDATE | XMLReader::SUBST_ENTITIES); + + // fast forward to first record + while ($this->reader->read() && $this->reader->name !== $this->key) + { + // Do nothing... + } + } + + /** + * Is our current node valid + * + * @return bool Is valid? + */ + public function valid() + { + return $this->reader->name === $this->key; + } +} diff --git a/core/libraries/Hubzero/Content/Import/Model/Archive.php b/core/libraries/Hubzero/Content/Import/Model/Archive.php new file mode 100644 index 00000000000..7e926c38391 --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Model/Archive.php @@ -0,0 +1,105 @@ +type = $type; + } + + /** + * Get Instance of Page Archive + * + * @param string $key Instance Key + * @return object + */ + static function &getInstance($key=null) + { + static $instances; + + if (!isset($instances)) + { + $instances = array(); + } + + if (!isset($instances[$key])) + { + $instances[$key] = new static($key); + } + + return $instances[$key]; + } + + /** + * Get a list or count of imports + * + * @param string $rtrn What data to return + * @param array $filters Filters to apply to data retrieval + * @param boolean $boolean Clear cached data? + * @return mixed + */ + public function imports($rtrn = 'list', $filters = array(), $clear = false) + { + $model = Import::all(); + + if (isset($filters['state']) && $filters['state']) + { + if (!is_array($filters['state'])) + { + $filters['state'] = array($filters['state']); + } + $filters['state'] = array_map('intval', $filters['state']); + + $model->whereIn('state', $filters['state']); + } + + if (!isset($filters['type'])) + { + $filters['type'] = $this->type; + } + + if (isset($filters['type']) && $filters['type']) + { + $model->whereEquals('type', $filters['type']); + } + + if (isset($filters['created_by']) && $filters['created_by'] >= 0) + { + $model->whereEquals('created_by', $filters['created_by']); + } + + if (strtolower($rtrn) == 'count') + { + return $model->total(); + } + + return $model->ordered() + ->paginated() + ->rows(); + } +} diff --git a/core/libraries/Hubzero/Content/Import/Model/Hook.php b/core/libraries/Hubzero/Content/Import/Model/Hook.php new file mode 100644 index 00000000000..1631a102185 --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Model/Hook.php @@ -0,0 +1,71 @@ + 'notempty', + 'name' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + **/ + public $initiate = array( + 'created', + 'created_by' + ); + + /** + * Return imports filespace path + * + * @return string + */ + public function fileSpacePath() + { + // build upload path + $uploadPath = PATH_APP . DS . 'site' . DS . 'import' . DS . 'hooks' . DS . $this->get('id'); + + // return path + return $uploadPath; + } +} diff --git a/core/libraries/Hubzero/Content/Import/Model/Hook/Archive.php b/core/libraries/Hubzero/Content/Import/Model/Hook/Archive.php new file mode 100644 index 00000000000..b063fc47d6b --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Model/Hook/Archive.php @@ -0,0 +1,111 @@ +type = $type; + } + + /** + * Get Instance of Archive + * + * @param string $key Instance Key + * @return object + */ + static function &getInstance($key=null) + { + static $instances; + + if (!isset($instances)) + { + $instances = array(); + } + + if (!isset($instances[$key])) + { + $instances[$key] = new static($key); + } + + return $instances[$key]; + } + + /** + * Get a count or list of import hooks + * + * @param string $rtrn What data to return + * @param array $filters Filters to apply to data retrieval + * @param boolean $boolean Clear cached data? + * @return mixed + */ + public function hooks($rtrn = 'list', $filters = array(), $clear = false) + { + $model = Hook::all(); + + if (isset($filters['state']) && $filters['state']) + { + if (!is_array($filters['state'])) + { + $filters['state'] = array($filters['state']); + } + $filters['state'] = array_map('intval', $filters['state']); + + $model->whereIn('state', $filters['state']); + } + + if (!isset($filters['type'])) + { + $filters['type'] = $this->type; + } + + if (isset($filters['type']) && $filters['type']) + { + $model->whereEquals('type', $filters['type']); + } + + if (isset($filters['event']) && $filters['event']) + { + $model->whereEquals('event', $filters['event']); + } + + if (isset($filters['created_by']) && $filters['created_by'] >= 0) + { + $model->whereEquals('created_by', $filters['created_by']); + } + + if (strtolower($rtrn) == 'count') + { + return $model->total(); + } + + return $model->ordered() + ->paginated() + ->rows(); + } +} diff --git a/core/libraries/Hubzero/Content/Import/Model/Import.php b/core/libraries/Hubzero/Content/Import/Model/Import.php new file mode 100644 index 00000000000..587948a00ec --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Model/Import.php @@ -0,0 +1,190 @@ + 'notempty', + 'type' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + **/ + public $initiate = array( + 'created_at', + 'created_by' + ); + + /** + * Generates automatic created field value + * + * @return string + * @since 2.0.0 + **/ + public function automaticCreatedAt() + { + return Date::toSql(); + } + + /** + * Get a list of runs + * + * @return object + */ + public function runs() + { + return $this->oneToMany('Hubzero\Content\Import\Model\Run', 'import_id'); + } + + /** + * Get most current run + * + * @return object + */ + public function currentRun() + { + if (!$this->_currentRun) + { + $this->_currentRun = $this->runs() + ->whereEquals('import_id', $this->get('id')) + ->ordered() + ->row(); + } + + return $this->_currentRun; + } + + /** + * Return raw import data + * + * @return string + */ + public function getData() + { + return file_get_contents($this->getDataPath()); + } + + /** + * Return path to imports data file + * + * @return string + */ + public function getDataPath() + { + // make sure we have file + if (!$file = $this->get('file')) + { + throw new Exception(__METHOD__ . '(); ' . Lang::txt('Missing required data file.')); + } + + // build path to file + $filePath = $this->fileSpacePath() . DS . $file; + + // make sure file exists + if (!file_exists($filePath)) + { + throw new Exception(__METHOD__ . '(); ' . Lang::txt('Data file does not exist at path: %s.', $filePath)); + } + + // make sure we can read the file + if (!is_readable($filePath)) + { + throw new Exception(__METHOD__ . '(); ' . Lang::txt('Data file not readable.')); + } + + return $filePath; + } + + /** + * Return imports filespace path + * + * @return string + */ + public function fileSpacePath() + { + // build upload path + $uploadPath = PATH_APP . DS . 'site' . DS . 'import' . DS . $this->get('id'); + + // return path + return $uploadPath; + } + + /** + * Mark Import Run + * + * @param integer $dryRun Dry run mode + * @return void + */ + public function markRun($dryRun = 1) + { + $run = Run::blank(); + $run->set('import_id', $this->get('id')) + ->set('count', $this->get('count')) + ->set('ran_by', User::get('id')) + ->set('ran_at', Date::toSql()) + ->set('dry_run', $dryRun) + ->save(); + } +} diff --git a/core/libraries/Hubzero/Content/Import/Model/Record.php b/core/libraries/Hubzero/Content/Import/Model/Record.php new file mode 100644 index 00000000000..4c08b70dff7 --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Model/Record.php @@ -0,0 +1,167 @@ +raw = $raw; + $this->_options = $options; + $this->_mode = strtoupper($mode); + + // Create core objects + $this->_database = \App::get('db'); + $this->_user = \User::getInstance(); + + // Create objects + $this->record = new stdClass; + + // Message bags for user + $this->record->errors = array(); + $this->record->notices = array(); + + // Bind data + $this->bind(); + } + + /** + * Bind all raw data + * + * @return object Current object + */ + public function bind() + { + return $this; + } + + /** + * Check Data integrity + * + * @return object Current object + */ + public function check() + { + return $this; + } + + /** + * Store Data + * + * @param integer $dryRun Dry Run mode + * @return object Current object + */ + public function store($dryRun = 1) + { + // Are we running in dry run mode? + if ($dryRun || count($this->record->errors) > 0) + { + return $this; + } + + return $this; + } + + /** + * Output object of string + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * To String object + * + * Removes private properties before returning + * + * @return string + */ + public function toString() + { + // Reflect on class to get private or protected props + $privateProperties = with(new \ReflectionClass($this))->getProperties(\ReflectionProperty::IS_PROTECTED); + + // Remove each private or protected prop + foreach ($privateProperties as $prop) + { + $name = (string) $prop->name; + unset($this->$name); + } + + // Output as json + return json_encode($this); + } +} diff --git a/core/libraries/Hubzero/Content/Import/Model/Run.php b/core/libraries/Hubzero/Content/Import/Model/Run.php new file mode 100644 index 00000000000..9bbf82ea22d --- /dev/null +++ b/core/libraries/Hubzero/Content/Import/Model/Run.php @@ -0,0 +1,103 @@ + 'positive|nonzero' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + **/ + public $initiate = array( + 'ran_at', + 'ran_by' + ); + + /** + * Generates automatic created field value + * + * @return string + * @since 2.0.0 + **/ + public function automaticRanAt() + { + $dt = new Date('now'); + return $dt->toSql(); + } + + /** + * Generates automatic created by field value + * + * @return int + * @since 2.0.0 + **/ + public function automaticRanBy() + { + return User::get('id'); + } + + /** + * Get parent import record + * + * @return object + */ + public function import() + { + return $this->belongsToOne('Hubzero\Content\Import\Model\Import', 'import_id'); + } + + /** + * Add to the processed number on this run + * + * @param integer $number Number to increpemnt by + * @return void + */ + public function processed($number = 1) + { + $this->set('processed', (int)$this->get('processed', 0) + $number); + $this->save(); + } +} diff --git a/core/libraries/Hubzero/Content/Importer.php b/core/libraries/Hubzero/Content/Importer.php new file mode 100644 index 00000000000..260c2ab2c76 --- /dev/null +++ b/core/libraries/Hubzero/Content/Importer.php @@ -0,0 +1,204 @@ +bootAdapters(); + } + + /** + * Get Instance of importer + * + * @param $key Instance Key + * @return void + */ + static function &getInstance($key=null) + { + static $instances; + + if (!isset($instances)) + { + $instances = array(); + } + + if (!isset($instances[$key])) + { + $instances[$key] = new static(); + } + + return $instances[$key]; + } + + /** + * Method to boot import adapters + * + * @return void + */ + private function bootAdapters() + { + // include all adapters + foreach (glob(__DIR__ . DS . 'Import' . DS . 'Adapter' . DS . '*.php') as $adapter) + { + require_once $adapter; + } + + // anonymous function to get adapters + $isAdapterClass = function($class) + { + return (in_array('Hubzero\Content\Import\Adapter', class_implements($class))); + }; + + // set our adapters (any declared class implementing the ResourcesImportInterface) + $this->adapters = array_values(array_filter(get_declared_classes(), $isAdapterClass)); + } + + /** + * Method to set adapater + * + * @param object $adapter + * @return void + */ + public function setAdapter(Adapter $adapter) + { + $this->adapter = $adapter; + } + + /** + * Method to get adapater + * + * @return object + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * Method auto detect adapter based on mime type + * + * @param object $import + * @return void + */ + private function autoDetectAdapter(Import $import) + { + // make sure we dont already have an adapter + if ($this->adapter) + { + return; + } + + // get path to data file + $dataPath = $import->getDataPath(); + + // get the mime type of file + $file = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($file, $dataPath); + if ($mime == 'text/plain') + { + $mime = pathinfo($dataPath, PATHINFO_EXTENSION); + } + + // anonymous function to see if we can use any + $respondsTo = function($class) use ($mime) + { + return $class::accepts($mime); + }; + + // set the adapter if we found one + $responded = array_filter($this->adapters, $respondsTo); + if ($adapter = array_shift($responded)) + { + $this->setAdapter(new $adapter()); + } + + // do we still not have adapter + if (!$this->adapter) + { + throw new \Exception(\Lang::txt('Content Import: No adapter found to count import data.')); + } + } + + /** + * Count import data + * + * @param object $import + * @return integer + */ + public function count(Import $import) + { + // autodetect adapter + $this->autoDetectAdapter($import); + + // call count on adapter + return $this->getAdapter()->count($import); + } + + /** + * Count import data + * + * @param object $import + * @return integer + */ + public function headers(Import $import) + { + // autodetect adapter + $this->autoDetectAdapter($import); + + // call count on adapter + return $this->getAdapter()->headers($import); + } + + /** + * Process import data + * + * @param object $import + * @param array $callbacks Array of Closure Objects + * @param integer $dryRun Dry run? + * @return void + */ + public function process(Import $import, array $callbacks, $dryRun = 1) + { + // autodetect adapter + $this->autoDetectAdapter($import); + + // mark import run + $import->markRun($dryRun); + + // call process on adapter + return $this->getAdapter()->process($import, $callbacks, $dryRun); + } +} diff --git a/core/libraries/Hubzero/Content/Migration.php b/core/libraries/Hubzero/Content/Migration.php new file mode 100644 index 00000000000..c2984fb3f1f --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration.php @@ -0,0 +1,829 @@ +addSearchPath(PATH_CORE) + ->addSearchPath(PATH_APP); + + $nodes = array( + PATH_CORE . DS . 'templates', + PATH_APP . DS . 'templates', + PATH_CORE . DS . 'components', + PATH_APP . DS . 'components', + PATH_CORE . DS . 'modules', + PATH_APP . DS . 'modules' + ); + + foreach ($nodes as $base) + { + $directories = array_diff(scandir($base), ['.', '..']); + + foreach ($directories as $directory) + { + if (!is_dir($base . DS . $directory)) + { + continue; + } + + // Does the directory conform to extension naming conventions? + if (strstr($directory, '.') || strstr($directory, ' ')) + { + continue; + } + + $this->addSearchPath($base . DS . $directory); + } + } + + // Plugins have one extra level of directories + $nodes = array( + PATH_CORE . DS . 'plugins', + PATH_APP . DS . 'plugins' + ); + + foreach ($nodes as $base) + { + $directories = array_diff(scandir($base), ['.', '..']); + + foreach ($directories as $directory) + { + if (!is_dir($base . DS . $directory)) + { + continue; + } + + $subdirectories = array_diff(scandir($base . DS . $directory), ['.', '..']); + + foreach ($subdirectories as $subdirectory) + { + // Does the directory conform to extension naming conventions? + if (strstr($subdirectory, '.') || strstr($subdirectory, ' ')) + { + continue; + } + + $this->addSearchPath($base . DS . $directory . DS . $subdirectory); + } + } + } + } + else + { + $docroot = rtrim($docroot, DS); + $this->addSearchPath($docroot); + } + + // Setup the database connection + if (!$this->db = $this->getDBO()) + { + $this->log('Error: database connection failed.', 'error'); + return false; + } + + // This is the database that migrations will run against + // This is used for super group migrations, that don't run against + // the default database schema + if (isset($runDb)) + { + $this->runDb = $runDb; + } + } + + /** + * Adds a search path to the migration + * + * @param string $path The path to add + * @return $this + **/ + public function addSearchPath($path) + { + $this->searchPaths[] = $path; + + return $this; + } + + /** + * Getter for class private variables + * + * @param string $var the var to retrieve + * @return mixed + **/ + public function get($var) + { + if (property_exists($this, $var)) + { + return $this->$var; + } + else + { + return false; + } + } + + /** + * Setup database connect, test, return object + * + * @return object + **/ + public function getDBO() + { + $db = \App::get('db'); + + // Test the connection + if (!$db->connected()) + { + $this->log('PDO connection failed', 'error'); + return false; + } + + // Check for the existance of the migrations table + $tables = $db->getTableList(); + $prefix = $db->getPrefix(); + $tableset = false; + + if (in_array('migrations', $tables)) + { + $this->setTableName('migrations'); + $tableset = true; + } + + if (in_array($prefix . 'migrations', $tables)) + { + if ($tableset) + { + $this->log('Tables `migrations` and `' . $prefix . 'migrations` both exist', 'error'); + return false; + } + + $this->setTableName('#__migrations'); + $tableset = true; + } + + if (!$tableset) + { + if ($this->createMigrationsTable($db) === false) + { + return false; + } + } + + // Add a callback so that a migration can update $this in real time if necessary + $this->registerCallback('migration', $this); + + return $db; + } + + /** + * Find all migration scripts + * + * @param string $extension Only look for migrations for this extension + * @param string $file The specific file to run + * @return array + **/ + public function find($extension = null, $file = null) + { + // Exclude certain thiings from our search + $exclude = array(".", ".."); + $files = []; + $ext = ''; + + foreach ($this->searchPaths as $path) + { + if (!is_dir($path . DS . 'migrations')) + { + continue; + } + $found = array_diff(scandir($path . DS . 'migrations'), $exclude); + + foreach ($found as $f) + { + $files[$path . DS . 'migrations' . DS . $f] = $f; + } + } + + asort($files); + + if (!is_null($file)) + { + if (in_array($file, $files)) + { + $this->files[] = array_search($file, $files); + return true; + } + else + { + $this->log("Provided file ({$file}) could not be found.", 'error'); + return false; + } + } + + if (!is_null($extension)) + { + $parts = explode('_', $extension); + foreach ($parts as $part) + { + $ext .= ucfirst($part); + } + } + + foreach ($files as $path => $file) + { + // Make sure they have a php extension and proper filename format + if (preg_match('/^Migration[0-9]{14}[[:alnum:]]+\.php$/', $file)) + { + // If an extension was provided...match against it... + if (empty($ext) || (!empty($ext) && preg_match('/Migration[0-9]{14}'.$ext.'\.php/', $file))) + { + $this->files[] = $path; + } + } + } + + return true; + } + + /** + * Migrate up/down on all files gathered via 'find' + * + * @param string $direction Direction to migrate (up or down) + * @param bool $force Run the update, even if the database says it's already been run + * @param bool $dryrun Run the udpate, but only display what would be changed, wihthout actually doing anything + * @param bool $listAll List all files found, not just those needing to be run + * @param bool $logOnly Run the update, and mark as run, but don't actually run sql (usefully to mark changes that had already been made manually) + * @return bool + **/ + public function migrate($direction = 'up', $force = false, $dryrun = false, $listAll = false, $logOnly = false) + { + // Make sure we have files + if (empty($this->files)) + { + $this->log("There were no migrations to run"); + return true; + } + + if (!$this->db) + { + return false; + } + + // Notify if we're making a dry run + if ($dryrun) + { + $this->log("Dry run: no changes will be made!"); + } + + // Notify if we're listing all files + if ($listAll) + { + $this->log("List all: all found files will be listed!"); + } + + // Now, fire hooks + if (!$dryrun && !$logOnly) + { + $this->fireHooks('onBeforeMigrate'); + } + + $hasStatus = $this->db->tableHasField($this->get('tbl_name'), 'status'); + + // Loop through files and run their '$direction' method + foreach ($this->files as $fullpath) //$file) + { + // Get just the file + $file = basename($fullpath); + + // Create a hash of the file (not using this at the moment) + $hash = hash('md5', $file); + + // Get the file name + $info = pathinfo($file); + + // Make sure the file exists + // If it doesn't, there's no point going any further + if (!is_file($fullpath)) + { + $this->log("{$fullpath} is not a valid file", 'warning'); + continue; + } + + // Generate the scope + // This will be the path to the migration, minus the document root + // ex: "core/migrations" or "app/components/com_example/migrations" + $scope = str_replace(PATH_ROOT . DS, '', dirname($fullpath)); + + // Check to see if this file has already been run + try + { + // Look to the database log to see the last run on this file + $query = "SELECT `direction`"; + + if ($this->db->tableHasField($this->get('tbl_name'), 'status')) + { + $query .= ", `status`"; + } + + $query .= " FROM `{$this->get('tbl_name')}` WHERE `file` = " . $this->db->quote($file); + + if ($this->db->tableHasField($this->get('tbl_name'), 'scope')) + { + if ($scope == 'core/migrations') + { + $query .= " AND (`scope`='' OR `scope` IN (" . $this->db->quote($scope) . "," . $this->db->quote('migrations') . "))"; + } + else + { + $query .= " AND `scope` = " . $this->db->quote($scope); + } + } + + $query .= " ORDER BY `date` DESC LIMIT 1"; + + $this->db->setQuery($query); + $row = $this->db->loadObject(); + + // Decide whether or not we want to show the file at all + // If list all, then we just show everything + // If force, we assume we have to show it + if (!$listAll && !$force) + { + // If we have a row (meaning it's been run at least once before), + // and the direction is the same as is being run now, then it's already been run + if ($row && $row->direction == $direction) + { + // The last check is to make sure that the previous run we see was a success + // If we don't have a status line (which is an implicit success), + // or we do have a status and it was a success, then we can reasonably skip this entry + if (!$hasStatus || ($hasStatus && $row->status == 'success')) + { + continue; + } + } + } + + // Now, if we are showing the file, should it actually be run? + if (!$force) + { + // If we have no row at all + if (!$row && $direction == 'down') + { + $this->log("Ignoring {$direction}() - you should run up first ({$scope}/{$file})"); + continue; + } + // If the last run was the same direction as is currently being run, we shouldn't run it again + else if ($row && $row->direction == $direction) + { + // Lastly, check status as well + if (!$hasStatus || ($hasStatus && $row->status == 'success')) + { + if ($dryrun) + { + $this->log("Would ignore {$direction}() {$scope}/{$file}"); + continue; + } + else + { + $this->log("Ignoring {$direction}() {$scope}/{$file}"); + continue; + } + } + } + } + } + catch (\Hubzero\Database\Exception\QueryFailedException $e) + { + // Our query failed altogether...that's not good + $this->log("Error: the check for preexisting migrations failed!", 'error'); + return false; + } + + require_once $fullpath; + + // Set classname + $classname = $info['filename']; + + // Make sure file and classname match + if (!class_exists($classname)) + { + $this->log("{$info['filename']} does not have a class of the same name", 'warning'); + continue; + } + + // We've made it this far, add this file to list of affected files + $this->affectedFiles[] = $info['filename']; + + // Instantiate our class + $class = new $classname($this->db, $this->callbacks, $this->runDb); + + // Check if we're making a dry run, or only logging changes + if ($dryrun) + { + $this->log("Would run {$direction}() {$scope}/{$file}", 'success'); + } + else if ($logOnly) + { + $this->recordMigration($file, $scope, $hash, $direction); + $this->log("Marking as run: {$direction}() in {$scope}/{$file}", 'success'); + } + else + { + // Try running the '$direction' SQL + if (method_exists($class, $direction)) + { + try + { + $result = $class->$direction(); + $errors = $class->getErrors(); + $status = 'success'; + + // Loop through errors if we have them + if ($errors && count($errors) > 0) + { + foreach ($errors as $error) + { + if ($error['type'] == 'fatal') + { + // Completely failed...log and stop immediately + $this->log("Error: running {$direction}() resulted in a fatal error in {$scope}/{$file}: {$error['message']}", 'error'); + $this->recordMigration($file, $scope, $hash, $direction, 'fatal'); + return false; + } + else if ($error['type'] == 'warning') + { + // Just a warning...display message and carry on (my wayward son) + $this->log("Warning: running {$direction}() resulted in a non-fatal error in {$scope}/{$file}: {$error['message']}", 'warning'); + $status = 'warning'; + continue; + } + else if ($error['type'] == 'info') + { + // Informational error (is that a real thing?) + $this->log("Info: running {$direction}() noted this in {$scope}/{$file}: {$error['message']}", 'info'); + } + } + } + + $this->recordMigration($file, $scope, $hash, $direction, $status); + $this->log("Completed {$direction}() in {$scope}/{$file}", 'success'); + } + catch (\Hubzero\Database\Exception\QueryFailedException $e) + { + $this->log("Error: running {$direction}() resulted in\n\n{$e->getMessage()}\n\nin {$scope}/{$file}", 'error'); + return false; + } + catch (\PDOException $e) + { + $this->log("Error: running {$direction}() resulted in\n\n{$e->getMessage()}\n\nin {$scope}/{$file}", 'error'); + return false; + } + } + } + } + + // Now, fire hooks + if (!$dryrun && !$logOnly) + { + $this->fireHooks('onAfterMigrate'); + } + + return true; + } + + /** + * Fire migration pre/post hooks + * + * @param string $timing Which hooks to fire + * @return void + **/ + private function fireHooks($timing) + { + $exclude = array('.', '..'); + $hooks = []; + + foreach ($this->searchPaths as $path) + { + // Make sure we have a hooks directroy + if (is_dir($path . DS . 'migrations' . DS . 'hooks')) + { + $found = []; + foreach (glob($path . DS . 'migrations' . DS . 'hooks' . DS . '*.php') as $hook) + { + // We just want the filename, so strip the path off + $hook = str_replace($path . DS . 'migrations' . DS . 'hooks' . DS, '', $hook); + + $found[] = [ + 'base' => $path . DS . 'migrations' . DS . 'hooks', + 'name' => $hook + ]; + } + + $hooks = array_merge($hooks, $found); + } + } + + if (count($hooks) > 0) + { + foreach ($hooks as $hook) + { + // Get the file name + $fullpath = $hook['base'] . DS . $hook['name']; + + // Include the file + if (is_file($fullpath)) + { + require_once $fullpath; + } + else + { + continue; + } + + // Set classname + $info = pathinfo($hook['name']); + $classname = $info['filename']; + + // Instantiate our class + $class = new $classname($this->db, $this->callbacks); + $hookTiming = $class->getOption('timing'); + + if ($hookTiming != $timing && $hookTiming != 'onAll') + { + continue; + } + + if (method_exists($class, 'fire')) + { + $result = $class->fire(); + + if (is_array($result) && !$result['success']) + { + // Just a warning...display message and carry on (my wayward son) + $message = (isset($result['message']) && !empty($result['message'])) ? $result['message'] : '[no message provided]'; + $this->log("Warning: {$timing} hook '{$hook['name']}' resulted in an error: {$message}", 'warning'); + } + } + } + } + } + + /** + * Record migration in migrations table + * + * @param string $file The path to file being recorded + * @param string $scope The folder of migration + * @param string $hash The hash of file + * @param string $direction Up or down + * @param string $status The status of the run + * @return bool + **/ + public function recordMigration($file, $scope, $hash, $direction, $status = 'success') + { + // Catch instances where we don't have a status field yet + // and mimic prior behavior where these runs were not logged + if (!$this->db->tableHasField($this->get('tbl_name'), 'status') && $status != 'success') + { + return true; + } + + // Try inserting a migration record into the database + try + { + $date = new \Hubzero\Utility\Date(); + + // Craete our object to insert + $obj = (object) array( + 'file' => $file, + 'hash' => $hash, + 'direction' => $direction, + 'date' => $date->toSql(), + 'action_by' => (php_sapi_name() == 'cli') ? exec("whoami") : \User::get('id') + ); + + if ($this->db->tableHasField($this->get('tbl_name'), 'scope')) + { + $obj->scope = $scope; + } + + if ($this->db->tableHasField($this->get('tbl_name'), 'status')) + { + $obj->status = $status; + } + + $this->db->insertObject($this->get('tbl_name'), $obj); + return true; + } + catch (\Hubzero\Database\Exception\QueryFailedException $e) + { + $this->log("Failed inserting migration record: {$e->getMessage()}", 'error'); + return false; + } + } + + /** + * Return migration run history + * + * @return mixed False on error, array on success + **/ + public function history() + { + try + { + $query = "SELECT * FROM " . $this->db->quoteName($this->get('tbl_name')); + $this->db->setQuery($query); + $results = $this->db->loadObjectList(); + + return $results; + } + catch (\Hubzero\Database\Exception\QueryFailedException $e) + { + $this->log("Failed to retrieve history.", 'error'); + return false; + } + } + + /** + * Set ignore callbacks to true + * + * @return void + **/ + public function ignoreCallbacks() + { + $this->ignoreCallbacks = true; + } + + /** + * Set ignore callbacks to false + * + * @return void + **/ + public function honorCallbacks() + { + $this->ignoreCallbacks = false; + } + + /** + * Logging mechanism + * + * @param string $message Message to log + * @param string $type Message type, can be one predefined values from output class (not specified will default to 'normal' text) + * @return void + **/ + public function log($message, $type = null) + { + $this->log[] = array('message' => $message, 'type' => $type); + + if (!$this->ignoreCallbacks && isset($this->callbacks['message']) && is_callable($this->callbacks['message'])) + { + $this->callbacks['message']($message, $type); + } + } + + /** + * Set the table name used for internal logging of migrations + * + * @param string $tbl_name The table name to set + * @return void + **/ + public function setTableName($tbl_name) + { + $this->tbl_name = $tbl_name; + } + + /** + * Register a callback + * + * @param string $name The callback name + * @param closure $callback The function to run + * @return void + **/ + public function registerCallback($name, $callback) + { + $this->callbacks[$name] = $callback; + } + + /** + * Attempt to create needed migrations table + * + * @param object $db The database connection object + * @return bool + **/ + private function createMigrationsTable($db) + { + $this->log('Migrations table did not exist...attempting to create it now'); + + $query = "CREATE TABLE `{$this->get('tbl_name')}` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `file` varchar(255) NOT NULL DEFAULT '', + `scope` varchar(255) NOT NULL, + `hash` char(32) NOT NULL DEFAULT '', + `direction` varchar(10) NOT NULL DEFAULT '', + `date` datetime NOT NULL, + `action_by` varchar(255) NOT NULL DEFAULT '', + `status` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;"; + + // Try creating the migrations table now + try + { + $db->setQuery($query); + $db->query(); + $this->log('Migrations table successfully created'); + return true; + } + catch (\Hubzero\Database\Exception\QueryFailedException $e) + { + $this->log('Unable to create needed migrations table', 'error'); + return false; + } + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Base.php b/core/libraries/Hubzero/Content/Migration/Base.php new file mode 100644 index 00000000000..f028188f270 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Base.php @@ -0,0 +1,519 @@ +baseDb = $db; + $this->db = (isset($altDb)) ? $altDb : $db; + $this->callbacks = $callbacks; + + if (!isset($altDb)) + { + $this->protectedMode = false; + } + + if (empty(self::$macroNamespaces)) + { + $this->registerMacroNamespace(__NAMESPACE__ . '\\Macros'); + } + } + + /** + * Helper function for calling a given callback + * + * @param string $callback Name of callback to use + * @param string $fund Name of callback function to call + * @param array $args Args to pass to callback function + * @return mixed + **/ + public function callback($callback, $func, $args=array()) + { + // Make sure the callback is set (this is protecting us when running in non-interactive mode and callbacks aren't set) + if (!isset($this->callbacks[$callback])) + { + return false; + } + + // Call function + return call_user_func_array(array($this->callbacks[$callback], $func), $args); + } + + /** + * Helper function for logging messages + * + * @param string $message + * @param string $type (info, warning, error, success) + * @return void + **/ + public function log($message, $type='info') + { + $this->callback('migration', 'log', [ + 'message' => $message, + 'type' => $type + ]); + } + + /** + * Get option - these are specified/overwritten by the individual migrations/hooks + * + * @param string $key + * @return mixed + **/ + public function getOption($key) + { + return (isset($this->options[$key])) ? $this->options[$key] : false; + } + + /** + * Return a middleware database object + * + * @return object + */ + public function getMWDBO() + { + static $instance; + + if (!is_object($instance)) + { + $config = $this->getParams('com_tools'); + + $options['driver'] = 'pdo'; + $options['host'] = $config->get('mwDBHost'); + $options['port'] = $config->get('mwDBPort'); + $options['user'] = $config->get('mwDBUsername'); + $options['password'] = $config->get('mwDBPassword'); + $options['database'] = $config->get('mwDBDatabase'); + $options['prefix'] = $config->get('mwDBPrefix'); + + if ((!isset($options['password']) || $options['password'] == '') + && (!isset($options['user']) || $options['user'] == '') + && (!isset($options['database']) || $options['database'] == '')) + { + $instance = $this->db; + } + else + { + try + { + $instance = Driver::getInstance($options); + } + catch (\PDOException $e) + { + $instance = null; + return false; + } + } + + // Test the connection + if (!$instance->connected()) + { + $instance = null; + return false; + } + } + + return $instance; + } + + /** + * Try to get the root credentials from a variety of locations + * + * @return mixed Array of creds or false on failure + **/ + private function getRootCredentials() + { + $secrets = DIRECTORY_SEPARATOR . 'etc' . DIRECTORY_SEPARATOR . 'hubzero.secrets'; + $conf_file = DIRECTORY_SEPARATOR . 'root' . DIRECTORY_SEPARATOR . '.my.cnf'; + $hub_maint = DIRECTORY_SEPARATOR . 'etc' . DIRECTORY_SEPARATOR . 'mysql' . DIRECTORY_SEPARATOR . 'hubmaint.cnf'; + $deb_maint = DIRECTORY_SEPARATOR . 'etc' . DIRECTORY_SEPARATOR . 'mysql' . DIRECTORY_SEPARATOR . 'debian.cnf'; + + if (is_file($secrets) && is_readable($secrets)) + { + $conf = Processor::instance('ini')->parse($secrets); + $user = (isset($conf['DEFAULT']['MYSQL-ROOT-USER'])) ? $conf['DEFAULT']['MYSQL-ROOT-USER'] : 'root'; + $pw = (isset($conf['DEFAULT']['MYSQL-ROOT'])) ? $conf['DEFAULT']['MYSQL-ROOT'] : false; + + if ($user && $pw) + { + return array('user' => $user, 'password' => $pw); + } + } + + if (is_file($conf_file) && is_readable($conf_file)) + { + $conf = Processor::instance('ini')->parse($conf_file, true); + $user = (isset($conf['client']['user'])) ? $conf['client']['user'] : false; + $pw = (isset($conf['client']['password'])) ? $conf['client']['password'] : false; + + if ($user && $pw) + { + return array('user' => $user, 'password' => $pw); + } + } + + if (is_file($hub_maint) && is_readable($hub_maint)) + { + $conf = Processor::instance('ini')->parse($hub_maint, true); + $user = (isset($conf['client']['user'])) ? $conf['client']['user'] : false; + $pw = (isset($conf['client']['password'])) ? $conf['client']['password'] : false; + + if ($user && $pw) + { + return array('user' => $user, 'password' => $pw); + } + } + + if (is_file($deb_maint) && is_readable($deb_maint)) + { + $conf = Processor::instance('ini')->parse($deb_maint, true); + $user = (isset($conf['client']['user'])) ? $conf['client']['user'] : false; + $pw = (isset($conf['client']['password'])) ? $conf['client']['password'] : false; + + if ($user && $pw) + { + return array('user' => $user, 'password' => $pw); + } + } + + return false; + } + + /** + * Try to run commands as MySql root user + * + * @return bool If successfully upgraded to root access + **/ + public function runAsRoot() + { + if ($this->protectedMode) + { + return false; + } + + if ($creds = $this->getRootCredentials()) + { + $db = Driver::getInstance( + array( + 'driver' => (\Config::get('dbtype') == 'mysql') ? 'pdo' : \Config::get('dbtype'), + 'host' => \Config::get('host'), + 'user' => $creds['user'], + 'password' => $creds['password'], + 'database' => \Config::get('db'), + 'prefix' => \Config::get('dbprefix') + ) + ); + + // Test the connection + if (!$db->connected()) + { + return false; + } + else + { + $this->db = $db; + return true; + } + } + + return false; + } + + /** + * Set an error + * + * @param string $message + * @param string $type + * @return void + **/ + public function setError($message, $type='fatal') + { + $this->errors[] = array('type' => $type, 'message' => $message); + } + + /** + * Get errors + * + * @return array Errors + **/ + public function getErrors() + { + return $this->errors; + } + + /** + * Register a custom macro + * + * @param string $name + * @param callable $macro + * @return void + */ + public static function macro($name, $macro) + { + static::$macros[$name] = $macro; + } + + /** + * Checks if macro is registered + * + * @param string $name + * @return boolean + */ + public static function hasMacro($name) + { + return isset(static::$macros[$name]); + } + + /** + * Registers a location to look for commands + * + * @param string $namespace The namespace location to use + * @return $this + **/ + public static function registerMacroNamespace($namespace, $paths = array()) + { + self::$macroNamespaces[$namespace] = (array)$paths; + } + + /** + * Dynamically handle calls to the class. + * + * @param string $method + * @param array $parameters + * @return mixed + * @throws \BadMethodCallException + */ + public function __call($method, $parameters) + { + if (static::hasMacro($method)) + { + $callback = static::$macros[$method]->setMigration($this); + + return call_user_func_array($callback, $parameters); + } + + + foreach (self::$macroNamespaces as $namespace => $paths) + { + $invokable = $namespace . '\\' . ucfirst($method); + + if (!class_exists($invokable)) + { + foreach ($paths as $path) + { + include_once $path . DIRECTORY_SEPARATOR . $method . '.php'; + } + } + + if (class_exists($invokable)) + { + $callback = new $invokable(); + + if ($callback instanceof Macro) + { + $callback->setMigration($this)->setDatabase($this->db); + + $this->macro($method, $callback); + + return call_user_func_array($callback, $parameters); + } + } + } + + throw new \BadMethodCallException("Method {$method} does not exist."); + } + + /** + * Generates ALTER TABLE SQL query to add columns absent from given table + * + * @param string $table Given table + * @param array $columns Columns to add to given table + * @return string + **/ + protected function _generateSafeAddColumns($table, $columns) + { + $query = $this->_generateSafeAlterTableColumnOperation( + $table, $columns, '_safeAddColumn' + ); + + return $query; + } + + /** + * Generates ADD COLUMN SQL statement if column absent from given table + * + * @param string $table Given table + * @param array $columnData Data for column to be added + * @return string + **/ + protected function _safeAddColumn($table, $columnData) + { + $columnName = $columnData['name']; + $addColumnStatement = ''; + + if (!$this->db->tableHasField($table, $columnName)) + { + $addColumnStatement = (new QueryAddColumnStatement($columnData)) + ->toString(); + } + + return $addColumnStatement; + } + + /** + * Generates ALTER TABLE SQL query to drop columns present on given table + * + * @param string $table Given table + * @param array $columns Columns to drop from given table + * @return string + **/ + protected function _generateSafeDropColumns($table, $columns) + { + $query = $this->_generateSafeAlterTableColumnOperation( + $table, $columns, '_safeDropColumn' + ); + + return $query; + } + + /** + * Generates DROP COLUMN SQL statement if column present on given table + * + * @param string $table Given table + * @param array $columnData Data for column to be dropped + * @return string + **/ + protected function _safeDropColumn($table, $columnData) + { + $columnName = $columnData['name']; + $dropColumnStatement = ''; + + if ($this->db->tableHasField($table, $columnName)) + { + $dropColumnStatement = with(new QueryDropColumnStatement($columnData)) + ->toString(); + } + + return $dropColumnStatement; + } + + /** + * Generates SQL statements to alter table for each column + * + * @param string $table Given table + * @param array $columns Columns to be affected by query + * @param string $functionName Function to generate per column statements + * @return string + **/ + protected function _generateSafeAlterTableColumnOperation($table, $columns, $functionName) + { + $query = "ALTER TABLE $table "; + + foreach ($columns as $columnData) + { + $query .= $this->$functionName($table, $columnData) . ','; + } + + $query = rtrim($query, ',') . ';'; + + return $query; + } + + /** + * Executes given query if given table exists + * + * @param string $table Given table + * @param string $query Query to execute + * @return void + **/ + protected function _queryIfTableExists($table, $query) + { + if ($this->db->tableExists($tableName)) + { + $this->db->setQuery($query); + $this->db->query(); + } + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macro.php b/core/libraries/Hubzero/Content/Migration/Macro.php new file mode 100644 index 00000000000..f8168bd5e95 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macro.php @@ -0,0 +1,78 @@ +db = $database; + + return $this; + } + + /** + * Set the migration object + * + * @param object $migration + * @return object + */ + public function setMigration(Base $migration) + { + $this->migration = $migration; + + return $this; + } + + /** + * Get the migration object + * + * @return object + */ + public function getMigration() + { + return $this->migration; + } + + /** + * Log a message + * + * @param string $message + * @param string $type + * @return object + */ + public function log($message, $type='info') + { + return $this->getMigration()->log($message, $type); + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/AddComponentEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/AddComponentEntry.php new file mode 100644 index 00000000000..4aa3bc3e63f --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/AddComponentEntry.php @@ -0,0 +1,275 @@ +db->tableExists('#__extensions')) + { + $this->log(sprintf('Required table not found for adding component "%s"', $name), 'warning'); + + return false; + } + + if (is_null($option)) + { + $option = 'com_' . strtolower($name); + } + $name = $option; + + // First, make sure it isn't already there + $query = $this->db->getQuery() + ->select('extension_id') + ->from('#__extensions') + ->whereEquals('name', $option) + ->toString(); + + $this->db->setQuery($query); + if ($this->db->loadResult()) + { + $component_id = $this->db->loadResult(); + + $this->log(sprintf('Extension entry already exists for component "%s"', $name)); + } + else + { + $ordering = 0; + + if (!empty($params) && is_array($params)) + { + $params = json_encode($params); + } + + $query = $this->db->getQuery() + ->insert('#__extensions') + ->values(array( + 'name' => $name, + 'type' => 'component', + 'element' => $option, + 'folder' => '', + 'client_id' => 1, + 'enabled' => $enabled, + 'access' => 1, + 'protected' => 0, + 'manifest_cache' => '', + 'params' => $params, + 'custom_data' => '', + 'system_data' => '', + 'checked_out' => 0, + 'ordering' => $ordering, + 'state' => 0 + )) + ->toString(); + + $this->db->setQuery($query); + $this->db->query(); + + $component_id = $this->db->insertId(); + + $this->log(sprintf('Added extension entry for component "%s"', $name)); + } + + if ($this->db->tableExists('#__assets')) + { + // Secondly, add asset entry if not yet created + $query = $this->db->getQuery() + ->select('id') + ->from('#__assets') + ->whereEquals('name', $option) + ->toString(); + $this->db->setQuery($query); + if (!$this->db->loadResult()) + { + // Build default ruleset + $defaulRules = array( + "core.admin" => array( + "7" => 1 + ), + "core.manage" => array( + "6" => 1 + ), + "core.create" => array(), + "core.delete" => array(), + "core.edit" => array(), + "core.edit.state" => array() + ); + + // Register the component container just under root in the assets table + $asset = Asset::blank(); + $asset->set('name', $option); + $asset->set('parent_id', 1); + $asset->set('rules', json_encode($defaulRules)); + $asset->set('title', $option); + $asset->saveAsChildOf(1); + + $this->log(sprintf('Added asset entry for component "%s"', $name)); + } + } + + if ($createMenuItem && $this->db->tableExists('#__menu')) + { + // Check for an admin menu entry...if it's not there, create it + $query = $this->db->getQuery() + ->select('id') + ->from('#__menu') + ->whereEquals('menutype', 'main') + ->whereEquals('title', $option) + ->toString(); + $this->db->setQuery($query); + if ($this->db->loadResult()) + { + return true; + } + + $alias = substr($option, 4); + + $query = $this->db->getQuery() + ->insert('#__menu') + ->values(array( + 'menutype' => 'main', + 'title' => $option, + 'alias' => $alias, + 'note' => '', + 'path' => $alias, + 'link' => 'index.php?option=' . $option, + 'type' => 'component', + 'published' => $enabled, + 'parent_id' => 1, + 'level' => 1, + 'component_id' => $component_id, + 'ordering' => 0, + 'checked_out' => 0, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '', + 'lft' => 0, + 'rgt' => 0, + 'home' => 0, + 'language' => '*', + 'client_id' => 1 + )) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Added menu entry for component "%s"', $name)); + + // Rebuild lft/rgt + $this->rebuildMenu(); + } + + return true; + } + + /** + * Method to recursively rebuild the whole nested set tree. + * + * @param integer $parentId The root of the tree to rebuild. + * @param integer $leftId The left id to start with in building the tree. + * @param integer $level The level to assign to the current nodes. + * @param string $path The path to the current nodes. + * @return integer 1 + value of root rgt on success, false on failure + */ + private function rebuildMenu($parentId = null, $leftId = 0, $level = 0, $path = '') + { + // If no parent is provided, try to find it. + if ($parentId === null) + { + // Get the root item. + $query = $this->db->getQuery() + ->select('id') + ->from('#__menu') + ->whereEquals('parent_id', 0) + ->toString(); + + $this->db->setQuery($query); + $parentId = $this->db->loadResult(); + + if ($parentId === false) + { + return false; + } + } + + // Build the structure of the recursive query. + $rebuild = $this->db->getQuery() + ->select('id') + ->select('alias') + ->from('#__menu') + ->whereEquals('parent_id', (int) $parentId) + ->order('parent_id', 'asc') + ->order('ordering', 'asc') + ->order('lft', 'asc') + ->toString(); + + // Assemble the query to find all children of this node. + $this->db->setQuery($rebuild); + $children = $this->db->loadObjectList(); + + // The right value of this node is the left value + 1 + $rightId = $leftId + 1; + + // execute this function recursively over all children + foreach ($children as $node) + { + // $rightId is the current right value, which is incremented on recursion return. + // Increment the level for the children. + // Add this item's alias to the path (but avoid a leading /) + $rightId = $this->rebuildMenu($node->id, $rightId, $level + 1, $path . (empty($path) ? '' : '/') . $node->alias); + + // If there is an update failure, return false to break out of the recursion. + if ($rightId === false) + { + return false; + } + } + + // We've got the left value, and now that we've processed + // the children of this node we also know the right value. + $query = $this->db->getQuery() + ->update('#__menu') + ->set(array( + 'lft' => (int) $leftId, + 'rgt' => (int) $rightId, + 'level' => (int) $level, + 'path' => $path + )) + ->whereEquals('id', (int) $parentId) + ->toString(); + $this->db->setQuery($query); + + // If there is an update failure, return false to break out of the recursion. + if (!$this->db->execute()) + { + return false; + } + + // Return the right value of this node + 1. + return $rightId + 1; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/AddModuleEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/AddModuleEntry.php new file mode 100644 index 00000000000..7e06d2dd7de --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/AddModuleEntry.php @@ -0,0 +1,84 @@ +db->tableExists('#__extensions')) + { + $name = $element; + + // First, make sure it isn't already there + $query = $this->db->getQuery() + ->select('extension_id') + ->from('#__extensions') + ->whereEquals('name', $name) + ->toString(); + $this->db->setQuery($query); + if ($this->db->loadResult()) + { + $this->log(sprintf('Extension entry already exists for module "%s"', $element)); + return true; + } + + $ordering = 0; + + if (!empty($params) && is_array($params)) + { + $params = json_encode($params); + } + + $query = $this->db->getQuery() + ->insert('#__extensions') + ->values(array( + 'name' => $name, + 'type' => 'module', + 'element' => $element, + 'folder' => '', + 'client_id' => $client, + 'enabled' => $enabled, + 'access' => 1, + 'protected' => 0, + 'manifest_cache' => '', + 'params' => $params, + 'custom_data' => '', + 'system_data' => '', + 'checked_out' => 0, + 'ordering' => $ordering, + 'state' => 0 + )) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Added extension entry for module "%s"', $element)); + + return true; + } + + $this->log(sprintf('Required table not found for adding module "%s"', $element), 'warning'); + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/AddPluginEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/AddPluginEntry.php new file mode 100644 index 00000000000..443164aec47 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/AddPluginEntry.php @@ -0,0 +1,99 @@ +db->tableExists('#__extensions')) + { + $this->log(sprintf('Required table not found for adding plugin "plg_%s_%s"', $folder, $element), 'warning'); + return false; + } + + $folder = strtolower($folder); + $element = strtolower($element); + $name = 'plg_' . $folder . '_' . $element; + + // First, make sure it isn't already there + $query = $this->db->getQuery() + ->select('extension_id') + ->from('#__extensions') + ->whereEquals('folder', $folder) + ->whereEquals('element', $element) + ->toString(); + $this->db->setQuery($query); + if ($this->db->loadResult()) + { + $this->log(sprintf('Extension entry already exists for plugin "%s"', $name)); + return true; + } + + // Get ordering + $query = $this->db->getQuery() + ->select('ordering') + ->from('#__extensions') + ->whereEquals('folder', $folder) + ->order('ordering', 'desc') + ->limit(1) + ->toString(); + $this->db->setQuery($query); + $ordering = (is_numeric($this->db->loadResult())) ? $this->db->loadResult()+1 : 1; + + if (!empty($params) && is_array($params)) + { + $params = json_encode($params); + } + + $query = $this->db->getQuery() + ->insert('#__extensions') + ->values(array( + 'name' => $name, + 'type' => 'plugin', + 'element' => $element, + 'folder' => $folder, + 'client_id' => 0, + 'enabled' => $enabled, + 'access' => 1, + 'protected' => 0, + 'manifest_cache' => '', + 'params' => $params, + 'custom_data' => '', + 'system_data' => '', + 'checked_out' => 0, + 'ordering' => $ordering, + 'state' => 0 + )) + ->toString(); + $this->db->setQuery($query); + if (!$this->db->query()) + { + $this->log(sprintf('Failed to add extension entry for plugin "%s"', $name)); + return false; + } + + $this->log(sprintf('Added extension entry for plugin "%s"', $name)); + + return true; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/AddTemplateEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/AddTemplateEntry.php new file mode 100644 index 00000000000..96f2b35b4c0 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/AddTemplateEntry.php @@ -0,0 +1,133 @@ +db->tableExists('#__extensions')) + { + if (!isset($name)) + { + if (substr($element, 0, 4) == 'tpl_') + { + $name = substr($element, 4); + $element = $name; + } + else + { + $name = $element; + } + + $name = ucwords($name); + } + + // First, see if it already exists + $query = $this->db->getQuery() + ->select('extension_id') + ->from('#__extensions') + ->whereEquals('type', 'template') + ->whereEquals('client_id', $client) + ->whereEquals('element', $element, 1) + ->orWhereLike('element', $name, 1) + ->resetDepth() + ->toString(); + $this->db->setQuery($query); + + if (!$this->db->loadResult()) + { + $query = $this->db->getQuery() + ->insert('#__extensions') + ->values(array( + 'name' => $name, + 'type' => 'template', + 'element' => $element, + 'folder' => '', + 'client_id' => $client, + 'enabled' => $enabled, + 'access' => 1, + 'protected' => 0, + 'manifest_cache' => '', + 'params' => '{}', + 'custom_data' => '', + 'system_data' => '', + 'checked_out' => 0, + 'ordering' => 0, + 'state' => 0 + )) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Added extension entry for template "%s"', $element)); + + if ($this->db->tableExists('#__template_styles')) + { + // If we're setting this template to be default, disable others first + if ($home) + { + $query = $this->db->getQuery() + ->update('#__template_styles') + ->set(array( + 'home' => 0 + )) + ->whereEquals('client_id', $client) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Disabling "home" for all other templates (client "%s")', $client)); + } + + $query = $this->db->getQuery() + ->insert('#__extensions') + ->values(array( + 'template' => $element, + 'client_id' => $client, + 'home' => $home, + 'title' => $name, + 'params' => ((isset($styles)) ? $this->db->quote(json_encode($styles)) : '{}') + )) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Added style entry for template "%s"', $element)); + } + } + else + { + $this->log(sprintf('Extension entry already exists for template "%s"', $element)); + } + + return true; + } + + $this->log(sprintf('Required table not found for adding template "%s"', $element), 'warning'); + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/DeleteComponentEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/DeleteComponentEntry.php new file mode 100644 index 00000000000..e39146ccbf7 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/DeleteComponentEntry.php @@ -0,0 +1,165 @@ +db->tableExists('#__components')) + { + $table = '#__components'; + } + + if ($this->db->tableExists($table)) + { + if (substr($name, 0, 4) !== 'com_') + { + $name = 'com_' . strtolower($name); + } + + // Delete component entry + $query = $this->db->getQuery() + ->delete($table) + ->whereEquals('name', $name) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + // Remove the component container in the assets table + $asset = Asset::oneByName($name); + if ($asset && $asset->get('id')) + { + $asset->destroy(); + } + + $this->log(sprintf('Removed extension entry for component "%s"', $name)); + + if ($this->db->tableExists('#__menu')) + { + // Check for an admin menu entry...if it's not there, create it + $query = $this->db->getQuery() + ->delete('#__menu') + ->whereEquals('menutype', 'main') + ->whereEquals('title', $name) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + // Rebuild lft/rgt + $this->rebuildMenu(); + + $this->log(sprintf('Removed menu entry for component "%s"', $name)); + } + + return true; + } + + return false; + } + + /** + * Method to recursively rebuild the whole nested set tree. + * + * @param integer $parentId The root of the tree to rebuild. + * @param integer $leftId The left id to start with in building the tree. + * @param integer $level The level to assign to the current nodes. + * @param string $path The path to the current nodes. + * @return integer 1 + value of root rgt on success, false on failure + */ + private function rebuildMenu($parentId = null, $leftId = 0, $level = 0, $path = '') + { + // If no parent is provided, try to find it. + if ($parentId === null) + { + // Get the root item. + $query = $this->db->getQuery() + ->select('id') + ->from('#__menu') + ->whereEquals('parent_id', 0) + ->toString(); + + $this->db->setQuery($query); + $parentId = $this->db->loadResult(); + + if ($parentId === false) + { + return false; + } + } + + // Build the structure of the recursive query. + $rebuild = $this->db->getQuery() + ->select('id') + ->select('alias') + ->from('#__menu') + ->whereEquals('parent_id', (int) $parentId) + ->order('parent_id', 'asc') + ->order('ordering', 'asc') + ->order('lft', 'asc') + ->toString(); + + // Assemble the query to find all children of this node. + $this->db->setQuery($rebuild); + $children = $this->db->loadObjectList(); + + // The right value of this node is the left value + 1 + $rightId = $leftId + 1; + + // execute this function recursively over all children + foreach ($children as $node) + { + // $rightId is the current right value, which is incremented on recursion return. + // Increment the level for the children. + // Add this item's alias to the path (but avoid a leading /) + $rightId = $this->rebuildMenu($node->id, $rightId, $level + 1, $path . (empty($path) ? '' : '/') . $node->alias); + + // If there is an update failure, return false to break out of the recursion. + if ($rightId === false) + { + return false; + } + } + + // We've got the left value, and now that we've processed + // the children of this node we also know the right value. + $query = $this->db->getQuery() + ->update('#__menu') + ->set(array( + 'lft' => (int) $leftId, + 'rgt' => (int) $rightId, + 'level' => (int) $level, + 'path' => $path + )) + ->whereEquals('id', (int) $parentId) + ->toString(); + $this->db->setQuery($query); + + // If there is an update failure, return false to break out of the recursion. + if (!$this->db->execute()) + { + return false; + } + + // Return the right value of this node + 1. + return $rightId + 1; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/DeleteModuleEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/DeleteModuleEntry.php new file mode 100644 index 00000000000..d72cb7fa83b --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/DeleteModuleEntry.php @@ -0,0 +1,78 @@ +db->tableExists('#__extensions')) + { + // Delete module entry + $query = $this->db->getQuery() + ->delete('#__extensions') + ->whereEquals('element', $element); + if (isset($client)) + { + $query->whereEquals('client_id', $client); + } + + $this->db->setQuery($query->toString()); + $this->db->query(); + + $this->log(sprintf('Removed extension entry for module "%s"', $element)); + } + + // See if entries are present in #__modules table as well + $query = $this->db->getQuery() + ->select('id') + ->from('#__modules') + ->whereEquals('module', $element); + if (isset($client)) + { + $query->whereEquals('client_id', $client); + } + + $this->db->setQuery($query->toString()); + $ids = $this->db->loadColumn(); + + if ($ids && count($ids) > 0) + { + // Delete modules and module menu entries + $query = $this->db->getQuery() + ->delete('#__modules') + ->whereIn('id', $ids) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $query = $this->db->getQuery() + ->delete('#__modules_menu') + ->whereIn('moduleid', $ids) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Removed module/menu entries for module "%s"', $element)); + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/DeletePluginEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/DeletePluginEntry.php new file mode 100644 index 00000000000..8f88a43959e --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/DeletePluginEntry.php @@ -0,0 +1,58 @@ +db->tableExists('#__plugins')) + { + $table = '#__plugins'; + } + + if ($this->db->tableExists($table)) + { + $enabled = 0; + + $query = $this->db->getQuery() + ->delete($table) + ->whereEquals('folder', $folder); + if ($element) + { + $query->whereEquals('element', $element); + } + + $query = $query + ->toString(); + + $this->db->setQuery($query); + + if ($this->db->query()) + { + $this->log(sprintf('Removed plugin entry "plg_%s_%s"', $folder, $element)); + return true; + } + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/DeleteTemplateEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/DeleteTemplateEntry.php new file mode 100644 index 00000000000..68696b1ee4f --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/DeleteTemplateEntry.php @@ -0,0 +1,91 @@ +db->tableExists('#__extensions')) + { + $query = $this->db->getQuery() + ->delete('#__extensions') + ->whereEquals('type', 'template') + ->whereEquals('element', $element) + ->whereEquals('client_id', $client) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Removed extension entry for template "%s"', $element)); + + if ($this->db->tableExists('#__template_styles')) + { + $query = $this->db->getQuery() + ->delete('#__template_styles') + ->whereEquals('template', $element) + ->whereEquals('client_id', $client) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Removed style entry for template "%s"', $element)); + + // Now make sure we have an enabled template (don't really care which one it is) + $query = $this->db->getQuery() + ->select('id') + ->from('#__template_styles') + ->whereEquals('home', 1) + ->whereEquals('client_id', $client) + ->toString(); + $this->db->setQuery($query); + if (!$this->db->loadResult()) + { + $query = $this->db->getQuery() + ->select('id') + ->from('#__template_styles') + ->whereEquals('client_id', $client) + ->order('id', 'desc') + ->limit(1) + ->toString(); + $this->db->setQuery($query); + if ($id = $this->db->loadResult()) + { + $query = $this->db->getQuery() + ->update('#__template_styles') + ->set(array( + 'home' => 1 + )) + ->whereEquals('id', $id) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Setting "home" for template style "%s"', $id)); + } + } + } + + return true; + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/DisableComponent.php b/core/libraries/Hubzero/Content/Migration/Macros/DisableComponent.php new file mode 100644 index 00000000000..a9f7b2c78b8 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/DisableComponent.php @@ -0,0 +1,54 @@ +db->tableExists('#__components')) + { + $table = '#__components'; + } + + if ($this->db->tableExists($table)) + { + $enabled = 0; + + $query = $this->db->getQuery() + ->update($table) + ->set(array( + 'enabled' => $enabled + )) + ->whereEquals('element', $element) + ->toString(); + + $this->db->setQuery($query); + + if ($this->db->query()) + { + $this->log(sprintf('Set component "%s" status to "%s"', $element, $enabled)); + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/DisableModule.php b/core/libraries/Hubzero/Content/Migration/Macros/DisableModule.php new file mode 100644 index 00000000000..49adc77c64a --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/DisableModule.php @@ -0,0 +1,59 @@ +db->tableExists('#__extensions')) + { + $enabled = 0; + + $query = $this->db->getQuery() + ->update('#__extensions') + ->set(array( + 'enabled' => $enabled + )) + ->whereEquals('element', $element) + ->toString(); + $this->db->setQuery($query); + if ($this->db->query()) + { + if ($this->db->tableExists('#__modules')) + { + $query = $this->db->getQuery() + ->update('#__modules') + ->set(array( + 'published' => $enabled + )) + ->whereEquals('module', $element) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + } + + $this->log(sprintf('Set module "%s" status to "%s"', $element, $enabled)); + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/DisablePlugin.php b/core/libraries/Hubzero/Content/Migration/Macros/DisablePlugin.php new file mode 100644 index 00000000000..a412497d5a3 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/DisablePlugin.php @@ -0,0 +1,58 @@ +db->tableExists('#__plugins')) + { + $table = '#__plugins'; + $field = 'published'; + } + + if ($this->db->tableExists($table)) + { + $enabled = 0; + + $query = $this->db->getQuery() + ->update($table) + ->set(array( + $field => $enabled + )) + ->whereEquals('folder', $folder) + ->whereEquals('element', $element) + ->toString(); + + $this->db->setQuery($query); + + if ($this->db->query()) + { + $this->log(sprintf('Set plugin "plg_%s_%s" status to "%s"', $folder, $element, $enabled)); + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/EnableComponent.php b/core/libraries/Hubzero/Content/Migration/Macros/EnableComponent.php new file mode 100644 index 00000000000..3a05c12a0ca --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/EnableComponent.php @@ -0,0 +1,54 @@ +db->tableExists('#__components')) + { + $table = '#__components'; + } + + if ($this->db->tableExists($table)) + { + $enabled = 1; + + $query = $this->db->getQuery() + ->update($table) + ->set(array( + 'enabled' => $enabled + )) + ->whereEquals('element', $element) + ->toString(); + + $this->db->setQuery($query); + + if ($this->db->query()) + { + $this->log(sprintf('Set component "%s" status to "%s"', $element, $enabled)); + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/EnableModule.php b/core/libraries/Hubzero/Content/Migration/Macros/EnableModule.php new file mode 100644 index 00000000000..32e7d181326 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/EnableModule.php @@ -0,0 +1,61 @@ +db->tableExists('#__extensions')) + { + $enabled = 1; + + $query = $this->db->getQuery() + ->update('#__extensions') + ->set(array( + 'enabled' => $enabled + )) + ->whereEquals('element', $element) + ->toString(); + + $this->db->setQuery($query); + + if ($this->db->query()) + { + if ($this->db->tableExists('#__modules')) + { + $query = $this->db->getQuery() + ->update('#__modules') + ->set(array( + 'published' => $enabled + )) + ->whereEquals('module', $element) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + } + + $this->log(sprintf('Set module "%s" status to "%s"', $element, $enabled)); + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/EnablePlugin.php b/core/libraries/Hubzero/Content/Migration/Macros/EnablePlugin.php new file mode 100644 index 00000000000..d0414a8a918 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/EnablePlugin.php @@ -0,0 +1,58 @@ +db->tableExists('#__plugins')) + { + $table = '#__plugins'; + $field = 'published'; + } + + if ($this->db->tableExists($table)) + { + $enabled = 1; + + $query = $this->db->getQuery() + ->update($table) + ->set(array( + $field => $enabled + )) + ->whereEquals('folder', $folder) + ->whereEquals('element', $element) + ->toString(); + + $this->db->setQuery($query); + + if ($this->db->query()) + { + $this->log(sprintf('Set plugin "plg_%s_%s" status to "%s"', $folder, $element, $enabled)); + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/EnableTemplate.php b/core/libraries/Hubzero/Content/Migration/Macros/EnableTemplate.php new file mode 100644 index 00000000000..12eaf405c0e --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/EnableTemplate.php @@ -0,0 +1,46 @@ +db->tableExists('#__extensions')) + { + $enabled = 1; + + $query = $this->db->getQuery() + ->update('#__extensions') + ->set(array( + 'enabled' => $enabled + )) + ->whereEquals('element', $element) + ->toString(); + $this->db->setQuery($query); + if ($this->db->query()) + { + $this->log(sprintf('Set component "%s" status to "%s"', $element, $enabled)); + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/GetParams.php b/core/libraries/Hubzero/Content/Migration/Macros/GetParams.php new file mode 100644 index 00000000000..a08d045eaae --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/GetParams.php @@ -0,0 +1,60 @@ +db->tableExists('#__extensions')) + { + $query = $this->db->getQuery() + ->select('params') + ->from('#__extensions'); + + if (substr($element, 0, 4) == 'plg_') + { + $ext = explode('_', $element); + $element = $ext[2]; + + $query->whereEquals('folder', $ext[1]); + } + + $query->whereEquals('element', $element); + + $this->db->setQuery($query->toString()); + $params = $this->db->loadResult(); + } + else + { + $this->log(sprintf('Required table not found for retrieving "%s" params', $element), 'warning'); + } + + if (!$returnRaw) + { + $params = new Registry($params); + } + + return $params; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/InstallModule.php b/core/libraries/Hubzero/Content/Migration/Macros/InstallModule.php new file mode 100644 index 00000000000..19eb286254e --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/InstallModule.php @@ -0,0 +1,104 @@ +db->quote(ucfirst($module)); + $position = $this->db->quote($position); + $module = $this->db->quote('mod_' . strtolower($module)); + $client = $this->db->quote((int)$client); + $access = ($this->db->tableExists('#__extensions')) ? 1 : 0; + + // Build params string + $params = json_encode($params); + + if (!$always) + { + $query = $this->db->getQuery() + ->select('id') + ->from('#__modules') + ->whereEquals('module', $module) + ->toString(); + $this->db->setQuery($query); + + if ($this->db->loadResult()) + { + return true; + } + } + + $query = $this->db->getQuery() + ->select('ordering') + ->from('#__modules') + ->whereEquals('position', $position) + ->order('ordering', 'desc') + ->limit(1) + ->toString(); + $this->db->setQuery($query); + $ordering = (int)(($this->db->loadResult()) ? $this->db->loadResult() + 1 : 0); + + $query = $this->db->getQuery() + ->insert('#__modules') + ->values(array( + 'title' => $title, + 'content' => '', + 'ordering' => $ordering, + 'position' => $position, + 'published' => 1, + 'module' => $module, + 'access' => $access, + 'showtitle' => 0, + 'params' => $params, + 'client_id' => $client + )) + ->toString(); + + $this->db->setQuery($query); + $this->db->query(); + $id = $this->db->quote($this->db->insertid()); + + $menus = (array)$menus; + foreach ($menus as $menu) + { + $menu = $this->db->quote($menu); + + $query = $this->db->getQuery() + ->insert('#__modules_menu') + ->values(array( + 'moduleid' => $id, + 'menuid' => $menu + )) + ->toString(); + $this->db->setQuery($query); + $this->db->query(); + + $this->log(sprintf('Added module_menu entry for module "%s" to menu "%s"', $module, $menu)); + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/InstallTemplateEntry.php b/core/libraries/Hubzero/Content/Migration/Macros/InstallTemplateEntry.php new file mode 100644 index 00000000000..bc4f2c49c2a --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/InstallTemplateEntry.php @@ -0,0 +1,32 @@ +db->tableExists('#__plugins')) + { + $table = '#__plugins'; + $pk = 'id'; + } + + $folder = strtolower($folder); + $element = strtolower($element); + + $query = $this->db->getQuery() + ->select($pk) + ->from($table) + ->whereEquals('folder', $folder) + ->whereEquals('element', $element) + ->toString(); + + // First, make sure the plugin exists + $this->db->setQuery($query); + + if ($id = $this->db->loadResult()) + { + $query = $this->db->getQuery() + ->update($table) + ->set(array( + 'name' => $name + )) + ->whereEquals($pk, $id) + ->toString(); + + $this->db->setQuery($query); + + if ($this->db->query()) + { + $this->log(sprintf('Renamed plugin plg_%s_%s to "%s"', $folder, $element, $name)); + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/SaveParams.php b/core/libraries/Hubzero/Content/Migration/Macros/SaveParams.php new file mode 100644 index 00000000000..d8e10953b6f --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/SaveParams.php @@ -0,0 +1,87 @@ +db->tableExists('#__extensions')) + { + $element = strtolower($element); + + $query = $this->db->getQuery() + ->select('extension_id') + ->from('#__extensions'); + + // First, make sure it's there + if (substr($element, 0, 4) == 'plg_') + { + $ext = explode('_', $element); + $element = $ext[2]; + + $query->whereEquals('folder', $ext[1]); + } + + $query->whereEquals('element', $element); + + $this->db->setQuery($query->toString()); + if (!$id = $this->db->loadResult()) + { + return false; + } + + // Build params JSON + if (is_array($params)) + { + $params = json_encode($params); + } + elseif ($params instanceof Registry) + { + $params = $params->toString('JSON'); + } + else + { + $this->log(sprintf('Params for extension "%s" not in usable format', $element), 'warning'); + return false; + } + + $query = $this->db->getQuery() + ->update('#__extensions') + ->set(array( + 'params' => $params + )) + ->whereEquals('extension_id', $id) + ->toString(); + $this->db->setQuery($query); + + if ($this->db->query()) + { + $this->log(sprintf('Extension params saved for "%s"', $element)); + return true; + } + } + + $this->log(sprintf('Required table not found for saving "%s" params', $element), 'warning'); + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/Macros/SavePluginParams.php b/core/libraries/Hubzero/Content/Migration/Macros/SavePluginParams.php new file mode 100644 index 00000000000..c6178c7e71c --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/Macros/SavePluginParams.php @@ -0,0 +1,27 @@ +db->tableExists('#__assets')) + { + $asset = Asset::oneByName($element); + + if (!$asset || !$asset->get('id')) + { + return false; + } + + // Loop through and map textual groups to ids (if applicable) + foreach ($rules as $idx => $rule) + { + foreach ($rule as $group => $value) + { + if (!is_numeric($group)) + { + $query = $this->db->getQuery() + ->select('id') + ->from('#__usergroups') + ->whereEquals('title', $group) + ->toString(); + + $this->db->setQuery($query); + + if ($id = $this->db->loadResult()) + { + unset($rules[$idx][$group]); + + $rules[$idx][$id] = $value; + } + } + } + } + + $asset->set('rules', json_encode($rules)); + $asset->save(); + + return true; + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/helpers/queryAddColumnStatement.php b/core/libraries/Hubzero/Content/Migration/helpers/queryAddColumnStatement.php new file mode 100644 index 00000000000..de37dddd7c0 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/helpers/queryAddColumnStatement.php @@ -0,0 +1,105 @@ +_name = $args['name']; + $this->_type = $args['type']; + $this->_restriction = Arr::getValue($args, 'restriction', null); + $this->_default = Arr::getValue($args, 'default', null); + $this->_asString = ''; + } + + /** + * Returns string representation of add column statement + * + * @return string + */ + public function toString() + { + $this->_generateBaseString(); + $this->_addName(); + $this->_addType(); + $this->_addRestriction(); + $this->_addDefault(); + + return $this->_asString; + } + + /** + * Generates base SQL string statement + * + * @return void + */ + protected function _generateBaseString() + { + $this->_asString = 'ADD COLUMN'; + } + + /** + * Adds column name to SQL string statement + * + * @return void + */ + protected function _addName() + { + $this->_asString .= " $this->_name"; + } + + /** + * Adds column type to SQL string statement + * + * @return void + */ + protected function _addType() + { + $this->_asString .= " $this->_type"; + } + + /** + * Adds column restriction to SQL string statement + * + * @return void + */ + protected function _addRestriction() + { + $restriction = rtrim(" $this->_restriction"); + + $this->_asString .= $restriction; + } + + /** + * Adds column default to SQL string statement + * + * @return void + */ + protected function _addDefault() + { + $default = ''; + + if ($this->_default === 0 || $this->_default) + { + $default = " DEFAULT $this->_default"; + } + + $this->_asString .= $default; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/helpers/queryDropColumnStatement.php b/core/libraries/Hubzero/Content/Migration/helpers/queryDropColumnStatement.php new file mode 100644 index 00000000000..84d46b20ccb --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/helpers/queryDropColumnStatement.php @@ -0,0 +1,60 @@ +_name = $args['name']; + $this->_asString = ''; + } + + /** + * Returns string representation of drop column statement + * + * @return string + */ + public function toString() + { + $this->_generateBaseString(); + $this->_addName(); + + return $this->_asString; + } + + /** + * Generates base SQL string statement + * + * @return void + */ + protected function _generateBaseString() + { + $this->_asString = 'DROP COLUMN'; + } + + /** + * Adds column name to SQL string statement + * + * @return void + */ + protected function _addName() + { + $this->_asString .= " $this->_name"; + } +} diff --git a/core/libraries/Hubzero/Content/Migration/tests/queryAddColumnStatementTest.php b/core/libraries/Hubzero/Content/Migration/tests/queryAddColumnStatementTest.php new file mode 100644 index 00000000000..205df39a0a1 --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/tests/queryAddColumnStatementTest.php @@ -0,0 +1,119 @@ + 'test', + 'type' => 'varchar(255)' + ]; + $addColumnStatement = new QueryAddColumnStatement($columnData); + $expectedStatement = 'ADD COLUMN test varchar(255)'; + + $actualStatement = $addColumnStatement->toString(); + + $this->assertEquals($expectedStatement, $actualStatement); + } + + /** + * Test toString returns correct string when restriction provided + * + * @return void + */ + public function testToStringReturnsCorrectStringWhenRestrictionProvided() + { + $columnData = [ + 'name' => 'test', + 'type' => 'varchar(255)', + 'restriction' => 'NOT NULL' + ]; + $addColumnStatement = new QueryAddColumnStatement($columnData); + $expectedStatement = 'ADD COLUMN test varchar(255) NOT NULL'; + + $actualStatement = $addColumnStatement->toString(); + + $this->assertEquals($expectedStatement, $actualStatement); + } + + /** + * Test toString returns correct string when default provided + * + * @return void + */ + public function testToStringReturnsCorrectStringWhenDefaultProvided() + { + $columnData = [ + 'name' => 'test', + 'type' => 'varchar(255)', + 'default' => "'foo'" + ]; + $addColumnStatement = new QueryAddColumnStatement($columnData); + $expectedStatement = "ADD COLUMN test varchar(255) DEFAULT 'foo'"; + + $actualStatement = $addColumnStatement->toString(); + + $this->assertEquals($expectedStatement, $actualStatement); + } + + /** + * Test toString returns correct string when restriction and default provided + * + * @return void + */ + public function testToStringReturnsCorrectStringWhenRestrictionAndDefaultProvided() + { + $columnData = [ + 'name' => 'test', + 'type' => 'varchar(255)', + 'restriction' => 'NOT NULL', + 'default' => "'foo'" + ]; + $addColumnStatement = new QueryAddColumnStatement($columnData); + $expectedStatement = "ADD COLUMN test varchar(255) NOT NULL DEFAULT 'foo'"; + + $actualStatement = $addColumnStatement->toString(); + + $this->assertEquals($expectedStatement, $actualStatement); + } + + /** + * Test toString returns correct string when restriction and default 0 + * + * @return void + */ + public function testToStringReturnsCorrectStringWhenRestrictionAndDefaultZero() + { + $columnData = [ + 'name' => 'test', + 'type' => 'varchar(255)', + 'restriction' => 'NOT NULL', + 'default' => 0 + ]; + $addColumnStatement = new QueryAddColumnStatement($columnData); + $expectedStatement = "ADD COLUMN test varchar(255) NOT NULL DEFAULT 0"; + + $actualStatement = $addColumnStatement->toString(); + + $this->assertEquals($expectedStatement, $actualStatement); + } +} diff --git a/core/libraries/Hubzero/Content/Migration/tests/queryDropColumnStatementTest.php b/core/libraries/Hubzero/Content/Migration/tests/queryDropColumnStatementTest.php new file mode 100644 index 00000000000..5a7477251fa --- /dev/null +++ b/core/libraries/Hubzero/Content/Migration/tests/queryDropColumnStatementTest.php @@ -0,0 +1,34 @@ + 'test']; + $dropColumnStatement = new QueryDropColumnStatement($columnData); + $expectedStatement = 'DROP COLUMN test'; + + $actualStatement = $dropColumnStatement->toString(); + + $this->assertEquals($expectedStatement, $actualStatement); + } +} diff --git a/core/libraries/Hubzero/Content/Mimetypes.php b/core/libraries/Hubzero/Content/Mimetypes.php new file mode 100644 index 00000000000..fea54f5c752 --- /dev/null +++ b/core/libraries/Hubzero/Content/Mimetypes.php @@ -0,0 +1,825 @@ + 'x-world/x-3dmf', + '3dmf' => 'x-world/x-3dmf', + 'a' => 'application/octet-stream', + 'aab' => 'application/x-authorware-bin', + 'aam' => 'application/x-authorware-map', + 'aas' => 'application/x-authorware-seg', + 'abc' => 'text/vnd.abc', + 'acgi' => 'text/html', + 'afl' => 'video/animaflex', + 'ai' => 'application/postscript', + 'aif' => 'audio/aiff', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/aiff', + 'aiff' => 'audio/x-aiff', + 'aim' => 'application/x-aim', + 'aip' => 'text/x-audiosoft-intra', + 'ani' => 'application/x-navi-animation', + 'aos' => 'application/x-nokia-9000-communicator-add-on-software', + 'aps' => 'application/mime', + 'arc' => 'application/octet-stream', + 'arj' => 'application/arj', + 'arj' => 'application/octet-stream', + 'art' => 'image/x-jg', + 'asf' => 'video/x-ms-asf', + 'asm' => 'text/x-asm', + 'asp' => 'text/asp', + 'asx' => 'application/x-mplayer2', + 'asx' => 'video/x-ms-asf', + 'asx' => 'video/x-ms-asf-plugin', + 'au' => 'audio/basic', + 'au' => 'audio/x-au', + 'avi' => 'application/x-troff-msvideo', + 'avi' => 'video/avi', + 'avi' => 'video/msvideo', + 'avi' => 'video/x-msvideo', + 'avs' => 'video/avs-video', + 'bcpio' => 'application/x-bcpio', + 'bin' => 'application/mac-binary', + 'bin' => 'application/macbinary', + 'bin' => 'application/octet-stream', + 'bin' => 'application/x-binary', + 'bin' => 'application/x-macbinary', + 'bm' => 'image/bmp', + 'bmp' => 'image/bmp', + 'bmp' => 'image/x-windows-bmp', + 'boo' => 'application/book', + 'book' => 'application/book', + 'boz' => 'application/x-bzip2', + 'bsh' => 'application/x-bsh', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'c' => 'text/plain', + 'c' => 'text/x-c', + 'c++' => 'text/plain', + 'cat' => 'application/vnd.ms-pki.seccat', + 'cc' => 'text/plain', + 'cc' => 'text/x-c', + 'ccad' => 'application/clariscad', + 'cco' => 'application/x-cocoa', + 'cdf' => 'application/cdf', + 'cdf' => 'application/x-cdf', + 'cdf' => 'application/x-netcdf', + 'cer' => 'application/pkix-cert', + 'cer' => 'application/x-x509-ca-cert', + 'cha' => 'application/x-chat', + 'chat' => 'application/x-chat', + 'class' => 'application/java', + 'class' => 'application/java-byte-code', + 'class' => 'application/x-java-class', + 'com' => 'application/octet-stream', + 'com' => 'text/plain', + 'conf' => 'text/plain', + 'cpio' => 'application/x-cpio', + 'cpp' => 'text/x-c', + 'cpt' => 'application/mac-compactpro', + 'cpt' => 'application/x-compactpro', + 'cpt' => 'application/x-cpt', + 'crl' => 'application/pkcs-crl', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/pkix-cert', + 'crt' => 'application/x-x509-ca-cert', + 'crt' => 'application/x-x509-user-cert', + 'csh' => 'application/x-csh', + 'csh' => 'text/x-script.csh', + 'css' => 'application/x-pointplus', + 'css' => 'text/css', + 'cxx' => 'text/plain', + 'dcr' => 'application/x-director', + 'deepv' => 'application/x-deepv', + 'def' => 'text/plain', + 'der' => 'application/x-x509-ca-cert', + 'dif' => 'video/x-dv', + 'dir' => 'application/x-director', + 'dl' => 'video/dl', + 'dl' => 'video/x-dl', + 'doc' => 'application/msword', + 'dot' => 'application/msword', + 'dp' => 'application/commonground', + 'drw' => 'application/drafting', + 'dump' => 'application/octet-stream', + 'dv' => 'video/x-dv', + 'dvi' => 'application/x-dvi', + 'dwf' => 'model/vnd.dwf', + 'dwg' => 'application/acad', + 'dwg' => 'image/vnd.dwg', + 'dwg' => 'image/x-dwg', + 'dxf' => 'application/dxf', + 'dxf' => 'image/vnd.dwg', + 'dxf' => 'image/x-dwg', + 'dxr' => 'application/x-director', + 'el' => 'text/x-script.elisp', + 'elc' => 'application/x-bytecode.elisp', + 'elc' => 'application/x-elc', + 'env' => 'application/x-envoy', + 'eps' => 'application/postscript', + 'es' => 'application/x-esrehber', + 'etx' => 'text/x-setext', + 'evy' => 'application/envoy', + 'evy' => 'application/x-envoy', + 'exe' => 'application/octet-stream', + 'f' => 'text/plain', + 'f' => 'text/x-fortran', + 'f77' => 'text/x-fortran', + 'f90' => 'text/plain', + 'f90' => 'text/x-fortran', + 'fdf' => 'application/vnd.fdf', + 'fif' => 'application/fractals', + 'fif' => 'image/fif', + 'fli' => 'video/fli', + 'fli' => 'video/x-fli', + 'flo' => 'image/florian', + 'flx' => 'text/vnd.fmi.flexstor', + 'fmf' => 'video/x-atomic3d-feature', + 'for' => 'text/plain', + 'for' => 'text/x-fortran', + 'fpx' => 'image/vnd.fpx', + 'fpx' => 'image/vnd.net-fpx', + 'frl' => 'application/freeloader', + 'funk' => 'audio/make', + 'g' => 'text/plain', + 'g3' => 'image/g3fax', + 'gif' => 'image/gif', + 'gl' => 'video/gl', + 'gl' => 'video/x-gl', + 'gsd' => 'audio/x-gsm', + 'gsm' => 'audio/x-gsm', + 'gsp' => 'application/x-gsp', + 'gss' => 'application/x-gss', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-compressed', + 'gz' => 'application/x-gzip', + 'gzip' => 'application/x-gzip', + 'gzip' => 'multipart/x-gzip', + 'h' => 'text/plain', + 'h' => 'text/x-h', + 'hdf' => 'application/x-hdf', + 'help' => 'application/x-helpfile', + 'hgl' => 'application/vnd.hp-hpgl', + 'hh' => 'text/plain', + 'hh' => 'text/x-h', + 'hlb' => 'text/x-script', + 'hlp' => 'application/hlp', + 'hlp' => 'application/x-helpfile', + 'hlp' => 'application/x-winhelp', + 'hpg' => 'application/vnd.hp-hpgl', + 'hpgl' => 'application/vnd.hp-hpgl', + 'hqx' => 'application/binhex', + 'hqx' => 'application/binhex4', + 'hqx' => 'application/mac-binhex', + 'hqx' => 'application/mac-binhex40', + 'hqx' => 'application/x-binhex40', + 'hqx' => 'application/x-mac-binhex40', + 'hta' => 'application/hta', + 'htc' => 'text/x-component', + 'htm' => 'text/html', + 'html' => 'text/html', + 'htmls' => 'text/html', + 'htt' => 'text/webviewhtml', + 'htx' => 'text/html', + 'ice' => 'x-conference/x-cooltalk', + 'ico' => 'image/x-icon', + 'idc' => 'text/plain', + 'ief' => 'image/ief', + 'iefs' => 'image/ief', + 'iges' => 'application/iges', + 'iges' => 'model/iges', + 'igs' => 'application/iges', + 'igs' => 'model/iges', + 'ima' => 'application/x-ima', + 'imap' => 'application/x-httpd-imap', + 'inf' => 'application/inf', + 'ins' => 'application/x-internett-signup', + 'ip' => 'application/x-ip2', + 'isu' => 'video/x-isvideo', + 'it' => 'audio/it', + 'iv' => 'application/x-inventor', + 'ivr' => 'i-world/i-vrml', + 'ivy' => 'application/x-livescreen', + 'jam' => 'audio/x-jam', + 'jav' => 'text/plain', + 'jav' => 'text/x-java-source', + 'java' => 'text/plain', + 'java' => 'text/x-java-source', + 'jcm' => 'application/x-java-commerce', + 'jfif' => 'image/jpeg', + 'jfif' => 'image/pjpeg', + 'jfif-tbnl' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpe' => 'image/pjpeg', + 'jpeg' => 'image/jpeg', + 'jpeg' => 'image/pjpeg', + 'jpg' => 'image/jpeg', + 'jpg' => 'image/pjpeg', + 'jps' => 'image/x-jps', + 'js' => 'application/x-javascript', + 'jut' => 'image/jutvision', + 'kar' => 'audio/midi', + 'kar' => 'music/x-karaoke', + 'ksh' => 'application/x-ksh', + 'ksh' => 'text/x-script.ksh', + 'la' => 'audio/nspaudio', + 'la' => 'audio/x-nspaudio', + 'lam' => 'audio/x-liveaudio', + 'latex' => 'application/x-latex', + 'lha' => 'application/lha', + 'lha' => 'application/octet-stream', + 'lha' => 'application/x-lha', + 'lhx' => 'application/octet-stream', + 'list' => 'text/plain', + 'lma' => 'audio/nspaudio', + 'lma' => 'audio/x-nspaudio', + 'log' => 'text/plain', + 'lsp' => 'application/x-lisp', + 'lsp' => 'text/x-script.lisp', + 'lst' => 'text/plain', + 'lsx' => 'text/x-la-asf', + 'ltx' => 'application/x-latex', + 'lzh' => 'application/octet-stream', + 'lzh' => 'application/x-lzh', + 'lzx' => 'application/lzx', + 'lzx' => 'application/octet-stream', + 'lzx' => 'application/x-lzx', + 'm' => 'text/plain', + 'm' => 'text/x-m', + 'm1v' => 'video/mpeg', + 'm2a' => 'audio/mpeg', + 'm2v' => 'video/mpeg', + 'm3u' => 'audio/x-mpequrl', + 'man' => 'application/x-troff-man', + 'map' => 'application/x-navimap', + 'mar' => 'text/plain', + 'mbd' => 'application/mbedlet', + 'mc$' => 'application/x-magic-cap-package-1.0', + 'mcd' => 'application/mcad', + 'mcd' => 'application/x-mathcad', + 'mcf' => 'image/vasa', + 'mcf' => 'text/mcf', + 'mcp' => 'application/netmc', + 'me' => 'application/x-troff-me', + 'mht' => 'message/rfc822', + 'mhtml' => 'message/rfc822', + 'mid' => 'application/x-midi', + 'mid' => 'audio/midi', + 'mid' => 'audio/x-mid', + 'mid' => 'audio/x-midi', + 'mid' => 'music/crescendo', + 'mid' => 'x-music/x-midi', + 'midi' => 'application/x-midi', + 'midi' => 'audio/midi', + 'midi' => 'audio/x-mid', + 'midi' => 'audio/x-midi', + 'midi' => 'music/crescendo', + 'midi' => 'x-music/x-midi', + 'mif' => 'application/x-frame', + 'mif' => 'application/x-mif', + 'mime' => 'message/rfc822', + 'mime' => 'www/mime', + 'mjf' => 'audio/x-vnd.audioexplosion.mjuicemediafile', + 'mjpg' => 'video/x-motion-jpeg', + 'mm' => 'application/base64', + 'mm' => 'application/x-meme', + 'mme' => 'application/base64', + 'mod' => 'audio/mod', + 'mod' => 'audio/x-mod', + 'moov' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp2' => 'audio/x-mpeg', + 'mp2' => 'video/mpeg', + 'mp2' => 'video/x-mpeg', + 'mp2' => 'video/x-mpeq2a', + 'mp3' => 'audio/mpeg3', + 'mp3' => 'audio/x-mpeg-3', + 'mp3' => 'video/mpeg', + 'mp3' => 'video/x-mpeg', + 'mp4' => 'video/mp4', + 'm3u' => 'audio/x-mpegurl', + 'm4a' => 'audio/mp4a-latm', + 'm4b' => 'audio/mp4a-latm', + 'm4p' => 'audio/mp4a-latm', + 'm4u' => 'video/vnd.mpegurl', + 'm4v' => 'video/x-m4v', + 'mpa' => 'audio/mpeg', + 'mpa' => 'video/mpeg', + 'mpc' => 'application/x-project', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'audio/mpeg', + 'mpg' => 'video/mpeg', + 'mpga' => 'audio/mpeg', + 'mpp' => 'application/vnd.ms-project', + 'mpt' => 'application/x-project', + 'mpv' => 'application/x-project', + 'mpx' => 'application/x-project', + 'mrc' => 'application/marc', + 'ms' => 'application/x-troff-ms', + 'mv' => 'video/x-sgi-movie', + 'my' => 'audio/make', + 'mzz' => 'application/x-vnd.audioexplosion.mzz', + 'nap' => 'image/naplps', + 'naplps' => 'image/naplps', + 'nc' => 'application/x-netcdf', + 'ncm' => 'application/vnd.nokia.configuration-message', + 'nif' => 'image/x-niff', + 'niff' => 'image/x-niff', + 'nix' => 'application/x-mix-transfer', + 'nsc' => 'application/x-conference', + 'nvd' => 'application/x-navidoc', + 'o' => 'application/octet-stream', + 'oda' => 'application/oda', + 'omc' => 'application/x-omc', + 'omcd' => 'application/x-omcdatamaker', + 'omcr' => 'application/x-omcregerator', + 'p' => 'text/x-pascal', + 'p10' => 'application/pkcs10', + 'p10' => 'application/x-pkcs10', + 'p12' => 'application/pkcs-12', + 'p12' => 'application/x-pkcs12', + 'p7a' => 'application/x-pkcs7-signature', + 'p7c' => 'application/pkcs7-mime', + 'p7c' => 'application/x-pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7m' => 'application/x-pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'part' => 'application/pro_eng', + 'pas' => 'text/pascal', + 'pbm' => 'image/x-portable-bitmap', + 'pcl' => 'application/vnd.hp-pcl', + 'pcl' => 'application/x-pcl', + 'pct' => 'image/x-pict', + 'pcx' => 'image/x-pcx', + 'pdb' => 'chemical/x-pdb', + 'pdf' => 'application/pdf', + 'pfunk' => 'audio/make', + 'pfunk' => 'audio/make.my.funk', + 'pgm' => 'image/x-portable-graymap', + 'pgm' => 'image/x-portable-greymap', + 'pic' => 'image/pict', + 'pict' => 'image/pict', + 'pkg' => 'application/x-newton-compatible-pkg', + 'pko' => 'application/vnd.ms-pki.pko', + 'pl' => 'text/plain', + 'pl' => 'text/x-script.perl', + 'plx' => 'application/x-pixclscript', + 'pm' => 'image/x-xpixmap', + 'pm' => 'text/x-script.perl-module', + 'pm4' => 'application/x-pagemaker', + 'pm5' => 'application/x-pagemaker', + 'png' => 'image/png', + 'pnm' => 'application/x-portable-anymap', + 'pnm' => 'image/x-portable-anymap', + 'pot' => 'application/mspowerpoint', + 'pot' => 'application/vnd.ms-powerpoint', + 'pov' => 'model/x-pov', + 'ppa' => 'application/vnd.ms-powerpoint', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/mspowerpoint', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppt' => 'application/mspowerpoint', + 'ppt' => 'application/powerpoint', + 'ppt' => 'application/vnd.ms-powerpoint', + 'ppt' => 'application/x-mspowerpoint', + 'ppz' => 'application/mspowerpoint', + 'pre' => 'application/x-freelance', + 'prt' => 'application/pro_eng', + 'ps' => 'application/postscript', + 'psd' => 'application/octet-stream', + 'pvu' => 'paleovu/x-pv', + 'pwz' => 'application/vnd.ms-powerpoint', + 'py' => 'text/x-script.phyton', + 'pyc' => 'applicaiton/x-bytecode.python', + 'qcp' => 'audio/vnd.qcelp', + 'qd3' => 'x-world/x-3dmf', + 'qd3d' => 'x-world/x-3dmf', + 'qif' => 'image/x-quicktime', + 'qt' => 'video/quicktime', + 'qtc' => 'video/x-qtc', + 'qti' => 'image/x-quicktime', + 'qtif' => 'image/x-quicktime', + 'ra' => 'audio/x-pn-realaudio', + 'ra' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'ras' => 'application/x-cmu-raster', + 'ras' => 'image/cmu-raster', + 'ras' => 'image/x-cmu-raster', + 'rast' => 'image/cmu-raster', + 'rexx' => 'text/x-script.rexx', + 'rf' => 'image/vnd.rn-realflash', + 'rgb' => 'image/x-rgb', + 'rm' => 'application/vnd.rn-realmedia', + 'rm' => 'audio/x-pn-realaudio', + 'rmi' => 'audio/mid', + 'rmm' => 'audio/x-pn-realaudio', + 'rmp' => 'audio/x-pn-realaudio', + 'rmp' => 'audio/x-pn-realaudio-plugin', + 'rng' => 'application/ringing-tones', + 'rng' => 'application/vnd.nokia.ringing-tone', + 'rnx' => 'application/vnd.rn-realplayer', + 'roff' => 'application/x-troff', + 'rp' => 'image/vnd.rn-realpix', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'rt' => 'text/richtext', + 'rt' => 'text/vnd.rn-realtext', + 'rtf' => 'application/rtf', + 'rtf' => 'application/x-rtf', + 'rtf' => 'text/richtext', + 'rtx' => 'application/rtf', + 'rtx' => 'text/richtext', + 'rv' => 'video/vnd.rn-realvideo', + 's' => 'text/x-asm', + 's3m' => 'audio/s3m', + 'saveme' => 'application/octet-stream', + 'sbk' => 'application/x-tbook', + 'scm' => 'application/x-lotusscreencam', + 'scm' => 'text/x-script.guile', + 'scm' => 'text/x-script.scheme', + 'scm' => 'video/x-scm', + 'sdml' => 'text/plain', + 'sdp' => 'application/sdp', + 'sdp' => 'application/x-sdp', + 'sdr' => 'application/sounder', + 'sea' => 'application/sea', + 'sea' => 'application/x-sea', + 'set' => 'application/set', + 'sgm' => 'text/sgml', + 'sgm' => 'text/x-sgml', + 'sgml' => 'text/sgml', + 'sgml' => 'text/x-sgml', + 'sh' => 'application/x-bsh', + 'sh' => 'application/x-sh', + 'sh' => 'application/x-shar', + 'sh' => 'text/x-script.sh', + 'shar' => 'application/x-bsh', + 'shar' => 'application/x-shar', + 'shtml' => 'text/html', + 'shtml' => 'text/x-server-parsed-html', + 'sid' => 'audio/x-psid', + 'sit' => 'application/x-sit', + 'sit' => 'application/x-stuffit', + 'skd' => 'application/x-koan', + 'skm' => 'application/x-koan', + 'skp' => 'application/x-koan', + 'skt' => 'application/x-koan', + 'sl' => 'application/x-seelogo', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'snd' => 'audio/basic', + 'snd' => 'audio/x-adpcm', + 'sol' => 'application/solids', + 'spc' => 'application/x-pkcs7-certificates', + 'spc' => 'text/x-speech', + 'spl' => 'application/futuresplash', + 'spr' => 'application/x-sprite', + 'sprite' => 'application/x-sprite', + 'src' => 'application/x-wais-source', + 'ssi' => 'text/x-server-parsed-html', + 'ssm' => 'application/streamingmedia', + 'sst' => 'application/vnd.ms-pki.certstore', + 'step' => 'application/step', + 'stl' => 'application/sla', + 'stl' => 'application/vnd.ms-pki.stl', + 'stl' => 'application/x-navistyle', + 'stp' => 'application/step', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svf' => 'image/vnd.dwg', + 'svf' => 'image/x-dwg', + 'svr' => 'application/x-world', + 'svr' => 'x-world/x-svr', + 'swf' => 'application/x-shockwave-flash', + 't' => 'application/x-troff', + 'talk' => 'text/x-speech', + 'tar' => 'application/x-tar', + 'tbk' => 'application/toolbook', + 'tbk' => 'application/x-tbook', + 'tcl' => 'application/x-tcl', + 'tcl' => 'text/x-script.tcl', + 'tcsh' => 'text/x-script.tcsh', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'text' => 'application/plain', + 'text' => 'text/plain', + 'tgz' => 'application/gnutar', + 'tgz' => 'application/x-compressed', + 'tif' => 'image/tiff', + 'tif' => 'image/x-tiff', + 'tiff' => 'image/tiff', + 'tiff' => 'image/x-tiff', + 'tr' => 'application/x-troff', + 'tsi' => 'audio/tsp-audio', + 'tsp' => 'application/dsptype', + 'tsp' => 'audio/tsplayer', + 'tsv' => 'text/tab-separated-values', + 'turbot' => 'image/florian', + 'txt' => 'text/plain', + 'uil' => 'text/x-uil', + 'uni' => 'text/uri-list', + 'unis' => 'text/uri-list', + 'unv' => 'application/i-deas', + 'uri' => 'text/uri-list', + 'uris' => 'text/uri-list', + 'ustar' => 'application/x-ustar', + 'ustar' => 'multipart/x-ustar', + 'uu' => 'application/octet-stream', + 'uu' => 'text/x-uuencode', + 'uue' => 'text/x-uuencode', + 'vcd' => 'application/x-cdlink', + 'vcs' => 'text/x-vcalendar', + 'vda' => 'application/vda', + 'vdo' => 'video/vdo', + 'vew' => 'application/groupwise', + 'viv' => 'video/vivo', + 'viv' => 'video/vnd.vivo', + 'vivo' => 'video/vivo', + 'vivo' => 'video/vnd.vivo', + 'vmd' => 'application/vocaltec-media-desc', + 'vmf' => 'application/vocaltec-media-file', + 'voc' => 'audio/voc', + 'voc' => 'audio/x-voc', + 'vos' => 'video/vosaic', + 'vox' => 'audio/voxware', + 'vqe' => 'audio/x-twinvq-plugin', + 'vqf' => 'audio/x-twinvq', + 'vql' => 'audio/x-twinvq-plugin', + 'vrml' => 'application/x-vrml', + 'vrml' => 'model/vrml', + 'vrml' => 'x-world/x-vrml', + 'vrt' => 'x-world/x-vrt', + 'vsd' => 'application/x-visio', + 'vst' => 'application/x-visio', + 'vsw' => 'application/x-visio', + 'w60' => 'application/wordperfect6.0', + 'w61' => 'application/wordperfect6.1', + 'w6w' => 'application/msword', + 'wav' => 'audio/wav', + 'wav' => 'audio/x-wav', + 'wb1' => 'application/x-qpro', + 'wbmp' => 'image/vnd.wap.wbmp', + 'web' => 'application/vnd.xara', + 'wiz' => 'application/msword', + 'wk1' => 'application/x-123', + 'wmf' => 'windows/metafile', + 'wml' => 'text/vnd.wap.wml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'wmls' => 'text/vnd.wap.wmlscript', + 'wmlsc' => 'application/vnd.wap.wmlscriptc', + 'word' => 'application/msword', + 'wp' => 'application/wordperfect', + 'wp5' => 'application/wordperfect', + 'wp5' => 'application/wordperfect6.0', + 'wp6' => 'application/wordperfect', + 'wpd' => 'application/wordperfect', + 'wpd' => 'application/x-wpwin', + 'wq1' => 'application/x-lotus', + 'wri' => 'application/mswrite', + 'wri' => 'application/x-wri', + 'wrl' => 'application/x-world', + 'wrl' => 'model/vrml', + 'wrl' => 'x-world/x-vrml', + 'wrz' => 'model/vrml', + 'wrz' => 'x-world/x-vrml', + 'wsc' => 'text/scriplet', + 'wsrc' => 'application/x-wais-source', + 'wtk' => 'application/x-wintalk', + 'xbm' => 'image/x-xbitmap', + 'xbm' => 'image/x-xbm', + 'xbm' => 'image/xbm', + 'xdr' => 'video/x-amt-demorun', + 'xgz' => 'xgl/drawing', + 'xif' => 'image/vnd.xiff', + 'xl' => 'application/excel', + 'xla' => 'application/excel', + 'xla' => 'application/x-excel', + 'xla' => 'application/x-msexcel', + 'xlb' => 'application/excel', + 'xlb' => 'application/vnd.ms-excel', + 'xlb' => 'application/x-excel', + 'xlc' => 'application/excel', + 'xlc' => 'application/vnd.ms-excel', + 'xlc' => 'application/x-excel', + 'xld' => 'application/excel', + 'xld' => 'application/x-excel', + 'xlk' => 'application/excel', + 'xlk' => 'application/x-excel', + 'xll' => 'application/excel', + 'xll' => 'application/vnd.ms-excel', + 'xll' => 'application/x-excel', + 'xlm' => 'application/excel', + 'xlm' => 'application/vnd.ms-excel', + 'xlm' => 'application/x-excel', + 'xls' => 'application/excel', + 'xls' => 'application/vnd.ms-excel', + 'xls' => 'application/x-excel', + 'xls' => 'application/x-msexcel', + 'xlt' => 'application/excel', + 'xlt' => 'application/x-excel', + 'xlv' => 'application/excel', + 'xlv' => 'application/x-excel', + 'xlw' => 'application/excel', + 'xlw' => 'application/vnd.ms-excel', + 'xlw' => 'application/x-excel', + 'xlw' => 'application/x-msexcel', + 'xm' => 'audio/xm', + 'xml' => 'application/xml', + 'xml' => 'text/xml', + 'xmz' => 'xgl/movie', + 'xpix' => 'application/x-vnd.ls-xpix', + 'xpm' => 'image/x-xpixmap', + 'xpm' => 'image/xpm', + 'x-png' => 'image/png', + 'xsr' => 'video/x-amt-showrun', + 'xwd' => 'image/x-xwd', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-pdb', + 'z' => 'application/x-compress', + 'z' => 'application/x-compressed', + 'zip' => 'application/x-compressed', + 'zip' => 'application/x-zip-compressed', + 'zip' => 'application/zip', + 'zip' => 'multipart/x-zip', + 'zoo' => 'application/octet-stream', + 'zsh' => 'text/x-script.zsh', + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-phps', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + 'asc' => 'text/plain', + 'atom' => 'application/atom+xml', + 'bcpio' => 'application/x-bcpio', + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'au' => 'audio/basic', + 'avi' => 'video/x-msvideo', + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'doc' => 'application/msword', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'swf' => 'application/x-shockwave-flash', + 'swf' => 'application/x-shockwave-flash2-preview', + 'swf' => 'application/futuresplash', + 'swf' => 'image/vnd.rn-realflash', + 'wmv' => 'video/x-ms-wmv', + /* MS Office 2007+ */ + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'dmg' => 'application/x-apple-diskimage' + ); + + /** + * Retrieve the corresponding MIME type, if one exists + * + * @param string $file File Name (relative location such as "image_test.jpg" or full "http://site.com/path/to/image_test.jpg") + * @return string The type of the file passed in the argument + */ + public function getMimeType($file = null) + { + if (is_file($file)) + { + $extension = $this->_getExtension($file); + + // Attempts to retrieve file info from FINFO + // If FINFO functions are not available then try to retrieve MIME type from pre-defined MIMEs + // If MIME type doesn't exist, then try (as a last resort) to use the (deprecated) mime_content_type function + // If all else fails, just return application/octet-stream + if (!function_exists('finfo_open') || in_array($extension, array('docx', 'pptx', 'xlsx'))) + { + if (array_key_exists($extension, $this->mimeTypes)) + { + return $this->mimeTypes[$extension]; + } + else + { + if (function_exists('mime_content_type')) + { + $type = mime_content_type($file); + return !empty($type) ? $type : 'application/octet-stream'; + } + else + { + return 'application/octet-stream'; + } + } + } + else + { + $finfo = finfo_open(FILEINFO_MIME); + $MIMEType = finfo_file($finfo, $file); + finfo_close($finfo); + return $MIMEType; + } + } + else + { + $extension = $this->_getExtension($file); + if (array_key_exists($extension, $this->mimeTypes)) + { + return $this->mimeTypes[$extension]; + } + } + return '##INVALID_FILE##'; + } + + /** + * Retrieve the corresponding file extension for a given MIME type, if one exists + * + * @param string $mimetype + * @return string A file extension associated with the mimetype + */ + public function getExtension($mimetype) + { + $key = array_search($mimetype, $this->mimeTypes); + + if (!$key) + { + $key = ''; + } + + return $key; + } + + /** + * Gets the file extension from a string + * + * @param string $file The full file name + * @return string The file extension + */ + private function _getExtension($file = null) + { + if (!is_null($file)) + { + $dot = strrpos($file, '.') + 1; + + return strtolower(substr($file, $dot)); + } + + return '##INVALID_FILE##'; + } +} diff --git a/core/libraries/Hubzero/Content/Moderator.php b/core/libraries/Hubzero/Content/Moderator.php new file mode 100644 index 00000000000..1fd5be61f79 --- /dev/null +++ b/core/libraries/Hubzero/Content/Moderator.php @@ -0,0 +1,154 @@ +path = $identifier; + } + else + { + $this->decompose($identifier); + } + + // We assume that if session_id and secret aren't included, that we're + // in an environement where we can easily grab them. + if (!$session_id && App::has('session')) + { + $session_id = App::get('session')->getId(); + } + $this->session_id = $session_id; + + if (!$secret && App::has('config')) + { + $secret = App::get('config')->get('secret'); + } + $this->secret = $secret; + } + + /** + * Builds the url identifier to the content + * + * @return string + **/ + public function getUrl() + { + return App::get('request')->root(true) . 'files/' . $this->getIdentifier(); + } + + /** + * Gets the file path + * + * @return string + **/ + public function getPath() + { + return $this->path; + } + + /** + * Validates the given token against the session date + * + * @return bool + **/ + public function validateToken() + { + if ($this->token !== $this->getToken()) + { + // Using 'public' as the session ID allows for shareable URLs + // not tied to a specific user session. Usage would be for + // files that do not need access control. + $this->session_id = 'public'; + } + return ($this->token === $this->getToken()); + } + + /** + * Generates the url string identifier + * + * @return string + **/ + private function getIdentifier() + { + return base64_encode($this->getToken() . ':' . $this->path); + } + + /** + * Generates the request token + * + * @return string + **/ + private function getToken() + { + return hash('sha256', $this->session_id . ':' . $this->secret); + } + + /** + * Unpacks the token into meaninful bits + * + * @param string $identifier The identifier to process + * @return void + **/ + private function decompose($identifier) + { + $identifier = base64_decode($identifier); + + if (strstr($identifier, ':')) + { + list($token, $path) = explode(':', $identifier, 2); + + $this->token = $token; + $this->path = $path; + } + } +} diff --git a/core/libraries/Hubzero/Content/Server.php b/core/libraries/Hubzero/Content/Server.php new file mode 100644 index 00000000000..279288e454d --- /dev/null +++ b/core/libraries/Hubzero/Content/Server.php @@ -0,0 +1,507 @@ +disposition('inline'); + } + + /** + * Set the name to save file as + * + * @param string $saveas Name to save file as + * @return mixed String if field is set, NULL if not + */ + public function saveas($saveas = null) + { + if (!is_null($saveas)) + { + $this->_saveas = basename($saveas); + } + + return $this->_saveas; + } + + /** + * Set the filename value + * + * @param string $filename File to serve up + * @return mixed String if field is set, NULL if not + */ + public function filename($filename = null) + { + if (!is_null($filename)) + { + $this->_filename = $filename; + } + + return $this->_filename; + } + + /** + * Allow apache to serve files + * + * @return void + */ + public function allowXsendFile() + { + if (function_exists('apache_get_modules')) + { + // is mod_xsendfile loaded & we have allowed xsendfile in config + if (in_array('mod_xsendfile', apache_get_modules()) + && \Config::get('allow_xsendfile', 0) == 1) + { + self::$_allowxsendfle = true; + } + } + } + + /** + * Set the filename value + * + * @param string $filename File to serve up + * @return boolean True if path is allowable, False if not + */ + public static function valid($filename = null) + { + if (!$filename) + { + return false; + } + + if (preg_match("/^\s*http[s]{0,1}:/i", $filename)) + { + return false; + } + if (preg_match("/^\s*[\/]{0,1}index.php\?/i", $filename)) + { + return false; + } + // Disallow windows drive letter + if (preg_match("/^\s*[.]:/", $filename)) + { + return false; + } + // Disallow \ + if (strpos($filename, '\\')) + { + return false; + } + // Disallow .. + if (strpos($filename, '..')) + { + return false; + } + + return true; + } + + /** + * Set the acceptranges value + * + * @param boolean $acceptranges Value to set + * @return mixed Boolean if field is set, NULL if not + */ + public function acceptranges($acceptranges = null) + { + if (!is_null($acceptranges)) + { + $this->_acceptranges = ($acceptranges) ? true : false; + } + + return $this->_acceptranges; + } + + /** + * Set the disposition value + * + * @param string $disposition Value to set + * @return mixed String if field is set, NULL if not + */ + public function disposition($disposition = null) + { + if (!is_null($disposition)) + { + if (strcasecmp($disposition, 'inline') == 0) + { + $disposition = 'inline'; + } + else if (strcasecmp($disposition, 'attachment') == 0) + { + $disposition = 'attachment'; + } + else + { + $disposition = 'inline'; + } + + $this->_disposition = $disposition; + } + + return $this->_disposition; + } + + /** + * Set Content Type + * + * @param string $contentType + */ + public function setContentType($contentType = null) + { + if ($contentType !== null) + { + self::$_contentType = $contentType; + } + } + + /** + * Read the contents of a file and display it + * + * @return boolean + */ + public function serve() + { + return self::serve_file($this->_filename, $this->_saveas, $this->_disposition, $this->_acceptranges); + } + + /** + * Read the contents of a file and display display as attachment + * (browser should default to saving file rather than displaying) + * + * @param string $filename File to serve up + * @param string $saveas Name to save file as + * @param boolean $acceptranges Generate Accept-Ranges header? + * @return boolean True on success, False if error + */ + public function serve_attachment($filename, $saveas = null, $acceptranges = true) + { + return self::serve_file($filename, $saveas, 'attachment', $acceptranges); + } + + /** + * Read the contents of a file and display it inline + * (display in browser window) + * + * @param string $filename File to serve up + * @param boolean $acceptranges Generate Accept-Ranges header? + * @return boolean True on success, False if error + */ + public function serve_inline($filename, $acceptranges = true) + { + return self::serve_file($filename, null, 'inline', $acceptranges); + } + + /** + * Read the contents of a file and display it + * + * @param string $filename File to serve up + * @param string $saveas Name to save file as (used for attachment disposition) + * @param string $disposition inline or attachment + * @param boolean $acceptranges Generate Accept-Ranges header? + * @return boolean True on success, False if error + */ + public static function serve_file($filename, $saveas=null, $disposition='inline', $acceptranges=true) + { + if (!self::valid($filename)) + { + return false; + } + + $fp = fopen($filename, 'rb'); + + if ($fp == false) + { + return false; + } + + $fileinfo = pathinfo($filename); + + if (empty($saveas)) + { + $saveas = $fileinfo['basename']; + } + else + { + $saveas = basename($saveas); + } + + $saveas = addslashes($saveas); + $filesize = filesize($filename); + + // Get the file's mimetype + if (!self::$_contentType) + { + $mime = new Mimetypes(); + self::$_contentType = $mime->getMimeType($filename); + } + + // Mimetype couldn't be determined? + if (self::$_contentType == '##INVALID_FILE##') + { + self::$_contentType = 'application/octet-stream'; + } + + // send xsend file now (before any headers are sent) + if (self::$_allowxsendfle === true) + { + header('Content-Type: ' . self::$_contentType); + header('Content-Disposition: ' . $disposition . '; filename=' . $saveas); + header('X-Sendfile: ' . $filename); + exit(0); + } + + if ($acceptranges + && $_SERVER['REQUEST_METHOD'] == 'GET' + && isset($_SERVER['HTTP_RANGE']) + && $range = stristr(trim($_SERVER['HTTP_RANGE']), 'bytes=')) + { + $range = substr($range, 6); + $boundary = 'g45d64df96bmdf4sdgh45hf5'; //set a random boundary + $ranges = explode(',', $range); + $partial = true; + } + else + { + $ranges = array('0-' . ($filesize - 1)); + $partial = false; + } + + $multipart = (count($ranges) > 1); + $content_length = 0; + + foreach ($ranges as $range) + { + preg_match("/^\s*(\d*)\s*-\s*(\d*)\s*$/", $range, $match); + + $first = isset($match[1]) ? $match[1] : ''; + $last = isset($match[2]) ? $match[2] : ''; + + if ($first!='') // byte-range-set + { + if (($last >= $filesize) || ($last == '')) + { + $last = $filesize - 1; + } + } + else if ($last != '') // suffix-byte-range-set + { + $first = $filesize - $last; + $last = $filesize - 1; + + if ($first < 0) + { + $first = 0; + } + } + + if (($first > $last) || ($last == '')) // unsatisfiable range + { + header("Status: 416 Requested range not satisfiable"); + header("Content-Range: */$filesize"); + exit; + } + + $result[$range]['first'] = $first; + $result[$range]['last'] = $last; + + if ($multipart) + { + $content_length += strlen("\r\n--$boundary\r\n"); + $content_length += strlen("Content-type: " . self::$_contentType . "\r\n"); + $content_length += strlen("Content-range: bytes $first-$last/$filesize\r\n\r\n"); + } + + $content_length += $last - $first + 1; + } + + if ($multipart) + { + $content_length += strlen("\r\n--$boundary--\r\n"); + } + + //output headers + if ($partial) + { + header('HTTP/1.1 206 Partial content'); + + if (!$multipart) + { + header("Content-range: bytes $first-$last/$filesize"); + } + } + + if (isset($_SERVER['HTTP_USER_AGENT'])) + { + $msie = preg_match("/MSIE (\d+)\.(\d+)([^;]*);/i", $_SERVER['HTTP_USER_AGENT'], $matches); // MSIE + } + else + { + $msie = false; + } + + if (!$partial && ($disposition == 'attachment')) + { + if ($msie && ($matches[1] < 6)) // untested IE 5.5 workaround + { + header('Content-Disposition: filename=' . $saveas); + } + else + { + header('Content-Disposition: attachment; filename="' . $saveas . '"'); + } + } + elseif (!$partial && ($disposition == 'inline')) + { + header('Content-Disposition: inline; filename="' . $saveas . '"'); + } + + if ($multipart) + { + header("Content-Type: multipart/x-byteranges; boundary=$boundary"); + } + else + { + header("Content-Type: " . self::$_contentType); + } + + // IE6 "save as" chokes on pragma no-cache or no-cache being + // first on the Cache-Control header + if (!$msie) + { + header('Pragma: no-cache'); + } + + header('Expires: 0'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + + if ($acceptranges) + { + header('Accept-Ranges: bytes'); + } + + header("Content-Length: $content_length"); + + $depth = ob_get_level(); + + for ($i=0; $i < $depth; $i++) + { + ob_end_clean(); + } + + + foreach ($ranges as $range) + { + $first = $result[$range]['first']; + $last = $result[$range]['last']; + + if ($multipart) + { + echo "\r\n--$boundary\r\n"; + echo "Content-type: " . self::$_contentType . "\r\n"; + echo "Content-range: bytes $first-$last/$filesize\r\n\r\n"; + } + + $buffer_size = 8096; + + fseek($fp, $first); + + if (($last + 1) == $filesize) + { + fpassthru($fp); + } + else + { + $bytes_left = $last - $first + 1; + + while ($bytes_left > 0 && !feof($fp)) + { + if ($bytes_left > $buffer_size) + { + $bytes_to_read = $buffer_size; + } + else + { + $bytes_to_read = $bytes_left; + } + + $bytes_left -= $bytes_to_read; + + echo fread($fp, $bytes_to_read); + + flush(); + } + } + } + + if ($multipart) + { + echo "\r\n--$boundary--\r\n"; + } + + fclose($fp); + + ob_start(); // restart buffering so we can throw away any extraneous output after this point + + return true; + } +} diff --git a/core/libraries/Hubzero/Database/Asset.php b/core/libraries/Hubzero/Database/Asset.php new file mode 100644 index 00000000000..eca8b5d7d78 --- /dev/null +++ b/core/libraries/Hubzero/Database/Asset.php @@ -0,0 +1,210 @@ +model = $model; + } + + /** + * Resolves the asset id based on the default parameters and expectations + * + * @param object $model The database model to which the asset refers + * @return int + * @since 2.0.0 + **/ + public static function resolve($model) + { + return with(new self($model))->getId(); + } + + /** + * Deletes the asset entry for the provided model + * + * @param object $model The model being deleted + * @return bool + * @since 2.0.0 + **/ + public static function destroy($model) + { + return with(new self($model))->delete(); + } + + /** + * Gets the asset id for the object instance + * + * @return int + * @since 2.0.0 + **/ + public function getId() + { + // Check for current asset id and compute other vars + $current = $this->model->get('asset_id', null); + $parentId = $this->getAssetParentId(); + $name = $this->getAssetName(); + $title = $this->getAssetTitle(); + $title = $title ?: $name; + + // Get model for assets + $asset = Model::oneByName($name); + + // Re-inject the asset id into the model + $this->model->set('asset_id', $asset->get('id')); + + // Prepare the asset to be stored + $asset->set('parent_id', $parentId); + $asset->set('name', $name); + $asset->set('title', $title); + + if ($this->model->assetRules instanceof \JAccessRules || $this->model->assetRules instanceof \Hubzero\Access\Rules) + { + $asset->set('rules', (string)$this->model->assetRules); + } + + // Specify how a new or moved node asset is inserted into the tree + if (!$this->model->get('asset_id', null) || $asset->parent_id != $parentId) + { + $parent = Model::one($parentId); + + if (!$asset->saveAsLastChildOf($parent)) + { + return false; + } + } + elseif (!$asset->save()) + { + return false; + } + + // Register an event to update the asset name once we know the model id + if ($this->model->isNew()) + { + $me = $this; + \Event::listen( + function($event) use ($asset, $me) + { + $asset->set('name', $me->getAssetName()); + $asset->save(); + }, + $this->model->getTableName() . '_new' + ); + } + + // Return the id + return (int)$asset->get('id'); + } + + /** + * Deletes the current asset entry + * + * @return bool + * @since 2.0.0 + **/ + public function delete() + { + $asset = Model::oneByName($this->getAssetName()); + + if ($asset->get('id')) + { + if (!$asset->destroy()) + { + return false; + } + } + + return true; + } + + /** + * Computes the (distinct) name of the asset + * + * @return string + * @since 2.0.0 + */ + private function getAssetName() + { + // @FIXME: this scheme won't always work... + // * namespace isn't always defined, at which point the model name is the namespace + // * namespace might be something like time_hub, which should become time.hub + // * non-integer ids will fail + return strtolower("com_{$this->model->getNamespace()}.{$this->model->getModelName()}.") . (int)$this->model->getPkValue(); + } + + /** + * Gets the title to use for the asset table + * + * @return string + * @since 2.0.0 + */ + private function getAssetTitle() + { + // @FIXME: need a way to override this + return $this->model->name; + } + + /** + * Gets the parent asset id for the record + * + * @return int + * @since 2.0.0 + */ + private function getAssetParentId() + { + $assetId = null; + + // Build the query to get the asset id for the parent category + $asset = Model::oneByName('com_' . $this->model->getNamespace()); + + if ($asset->get('id')) + { + $assetId = (int)$asset->get('id'); + } + + return ($assetId) ? $assetId : $this->getRootId(); + } + + /** + * Gets the root asset id from the #__assets table, defaulting to 1 + * + * @return int + * @since 2.0.0 + */ + private function getRootId() + { + $rootId = Model::getRootId(); + + if (empty($rootId)) + { + $rootId = 1; + } + + return $rootId; + } +} diff --git a/core/libraries/Hubzero/Database/DatabaseServiceProvider.php b/core/libraries/Hubzero/Database/DatabaseServiceProvider.php new file mode 100644 index 00000000000..8e17b514be9 --- /dev/null +++ b/core/libraries/Hubzero/Database/DatabaseServiceProvider.php @@ -0,0 +1,41 @@ +app['db'] = function($app) + { + // @FIXME: this isn't pretty, but it will shim the removal of the old mysql_* calls from php + $driver = (Config::get('dbtype') == 'mysql') ? 'pdo' : Config::get('dbtype'); + + $options = [ + 'driver' => $driver, + 'host' => Config::get('host'), + 'user' => Config::get('user'), + 'password' => Config::get('password'), + 'database' => Config::get('db'), + 'prefix' => Config::get('dbprefix') + ]; + + return Driver::getInstance($options); + }; + } +} diff --git a/core/libraries/Hubzero/Database/Driver.php b/core/libraries/Hubzero/Database/Driver.php new file mode 100644 index 00000000000..f0249576f83 --- /dev/null +++ b/core/libraries/Hubzero/Database/Driver.php @@ -0,0 +1,1518 @@ +tablePrefix = (isset($options['prefix'])) ? $options['prefix'] : $this->tablePrefix; + $this->database = (isset($options['database'])) ? $options['database'] : $this->database; + $this->setUTF(); + } + + /** + * Provides alias support for quote() and quoteName() + * + * @param string $method The called method name + * @param array $args The array of arguments passed to the method + * @return string + * @since 2.0.0 + * @deprecated 2.0.0 + * @throws \Hubzero\Error\Exception\BadMethodCallException + */ + public function __call($method, $args) + { + // We have to have args + if (empty($args)) + { + return; + } + + switch ($method) + { + case 'q': + return $this->quote($args[0], isset($args[1]) ? $args[1] : true); + break; + + case 'nq': + case 'qn': + return $this->quoteName($args[0], isset($args[1]) ? $args[1] : null); + break; + } + + // This method doesn't exist + throw new BadMethodCallException("'{$method}' method does not exist.", 500); + } + + /** + * Returns a driver instance based on the given options + * + * There are three global options and then the rest are specific to the database driver: + * * The 'driver' option defines which driver class is used for the connection -- the default is 'pdo'. + * * The 'database' option determines which database is to be used for the connection. + * * The 'select' option determines whether the connector should automatically select the chosen database. + * + * @param array $options Parameters to construct the database driver requested + * @return static + * @since 2.0.0 + * @throws \Hubzero\Error\Exception\RuntimeException + */ + public static function getInstance($options = []) + { + // Sanitize the database connector options + $options['driver'] = (isset($options['driver'])) ? preg_replace('/[^A-Z0-9_\.-]/i', '', $options['driver']) : 'mysql'; + $options['database'] = (isset($options['database'])) ? $options['database'] : null; + $options['select'] = (isset($options['select'])) ? $options['select'] : true; + + // @TODO: Eventually remove this? + if ($options['driver'] == 'pdo') + { + $options['driver'] = 'mysql'; + } + + // Get the options signature for the database connector + $signature = md5(serialize($options)); + + // If we already have a database connector instance for these options, then just use that + if (!isset(self::$instances[$signature])) + { + // Derive the class name from the driver + $class = __NAMESPACE__ . '\Driver\\' . ucfirst(strtolower($options['driver'])); + + // If the class doesn't exist we have a problem + if (!class_exists($class)) + { + throw new RuntimeException('Database driver not available.', 500); + } + + // Set the new connector to the global instances based on signature + self::$instances[$signature] = new $class($options); + } + + return self::$instances[$signature]; + } + + /** + * Sets the connection + * + * This method is public because it can be helpful when testing. + * You can ignore the constructor and just set the connection of + * your choice. We assume the person setting the connection + * has done their checks to make sure it is valid. + * + * @param object $connection the connection to set + * @return $this + * @since 2.0.0 + **/ + public function setConnection($connection) + { + $this->connection = $connection; + $this->setSyntax($this->detectSyntax()); + + return $this; + } + + /** + * Destroys the connection + * + * @return void + * @since 2.1.11 + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * Destroys the connection + * + * @return void + * @since 2.0.0 + */ + public function __destruct() + { + $this->disconnect(); + } + + /** + * Sets the SQL statement string for later execution + * + * @param string $query The SQL statement to set + * @return $this + * @since 2.0.0 + */ + public function setQuery($query) + { + $this->prepare((string)$query); + + return $this; + } + + /** + * Quotes and optionally escape a string to database requirements for insertion into the database + * + * @param string $text The string to quote + * @param bool $escape True to escape the string, false to leave it unchanged + * @return string + * @since 2.0.0 + */ + public function quote($text, $escape = true) + { + return '\'' . ($escape ? $this->escape($text) : $text) . '\''; + } + + /** + * Wraps an SQL statement identifier name such as column, table or database names + * in quotes to prevent injection risks and reserved word conflicts + * + * @param string $name The identifier name to wrap in quotes, supporting dot-notation names + * @param string $as The AS query part associated to $name + * @return string + * @since 2.0.0 + */ + public function quoteName($name, $as = null) + { + $parts = (strpos($name, '.') !== false) ? explode('.', $name) : (array)$name; + $bits = array(); + + foreach ($parts as $part) + { + $bits[] = sprintf($this->wrapper, $part); + } + + // Put back together and add 'AS' clause + $string = implode('.', $bits); + $string .= (isset($as)) ? ' AS ' . sprintf($this->wrapper, $as) : ''; + + return $string; + } + + /** + * Quotes table names and columns in the appropriate characters + * + * The only difference between this and quoteName above is that this + * allows you to directly pass in a string already including the AS + * statement, and still correctly quote it. + * + * @param string $value The table definition + * @return string + */ + public function wrap($value) + { + // Look for an 'AS' identifier first, and make sure not to choke on that + if (strpos(strtolower($value), ' as ') !== false) + { + $parts = explode(' ', $value); + + return $this->wrap($parts[0]) . ' AS ' . $this->wrap($parts[2]); + } + + $quoted = []; + $parts = explode('.', $value); + + foreach ($parts as $part) + { + // Make sure it's not an *, which shouldn't be quoted + $quoted[] = $part !== '*' ? sprintf($this->wrapper, $part) : $part; + } + + // Put it back together and return + return implode('.', $quoted); + } + + /** + * Inserts a row into a table based on an object's properties + * + * @param string $table The name of the database table to insert into + * @param object &$object A reference to an object whose public properties match the table fields + * @param string $key The name of the primary key. If provided the object property is updated + * @return bool + * @since 2.0.0 + */ + public function insertObject($table, &$object, $key = null) + { + // Initialise some variables + $fields = []; + $values = []; + $binds = []; + + // Create the base insert statement + $statement = 'INSERT INTO ' . $this->quoteName($table) . ' (%s) VALUES (%s)'; + + // Iterate over the object variables to build the query fields and values + foreach (get_object_vars($object) as $k => $v) + { + // Only process non-null scalars + if (is_array($v) or is_object($v) or $v === null) + { + continue; + } + + // Ignore any internal fields + if ($k[0] == '_') + { + continue; + } + + // Prepare and sanitize the fields and values for the database query + $fields[] = $this->quoteName($k); + $values[] = '?'; + $binds[] = $v; + } + + // Set the query and execute the insert + $this->prepare(sprintf($statement, implode(',', $fields), implode(',', $values))) + ->bind($binds); + + if (!$this->execute()) + { + return false; + } + + // Update the primary key if it exists + $id = $this->insertid(); + if ($key && $id) + { + $object->$key = $id; + } + + return true; + } + + /** + * Updates a row in a table based on an object's properties + * + * @param string $table The name of the database table to update + * @param object &$object A reference to an object whose public properties match the table fields + * @param string $key The name of the primary key + * @param bool $nulls True to update null fields or false to ignore them + * @return bool + * @since 2.0.0 + */ + public function updateObject($table, &$object, $key, $nulls = false) + { + // Initialise variables + $fields = []; + $where = ''; + + // Create the base update statement + $statement = 'UPDATE ' . $this->quoteName($table) . ' SET %s WHERE %s'; + + // Iterate over the object variables to build the query fields/value pairs + foreach (get_object_vars($object) as $k => $v) + { + // Only process scalars that are not internal fields. + if (is_array($v) or is_object($v) or $k[0] == '_') + { + continue; + } + + // Set the primary key to the WHERE clause instead of a field to update + if ($k == $key) + { + $where = $this->quoteName($k) . '=' . $this->quote($v); + continue; + } + + // Prepare and sanitize the fields and values for the database query + if ($v === null) + { + // If the value is null and we want to update nulls then set it + if ($nulls) + { + $val = 'NULL'; + } + // If the value is null and we do not want to update nulls then ignore this field + else + { + continue; + } + } + // The field is not null so we prep it for update + else + { + $val = $this->quote($v); + } + + // Add the field to be updated + $fields[] = $this->quoteName($k) . '=' . $val; + } + + // We don't have any fields to update + if (empty($fields)) + { + return true; + } + + // Set the query and execute the update. + $this->setQuery(sprintf($statement, implode(",", $fields), $where)); + + return $this->execute(); + } + + /** + * Gets the first row of the result set from the database query + * as an associative array of type: ['field_name' => 'row_value'] + * + * @return array|null + * @since 2.0.0 + */ + public function loadAssoc() + { + // Initialise variables + $return = null; + + // Execute the query + if (!$this->execute()) + { + return null; + } + + // Get the first row from the result set as an associative array + if ($row = $this->fetchAssoc()) + { + $return = $row; + } + + // Free up system resources and return + $this->freeResult(); + + return $return; + } + + /** + * Gets an array of the result set rows from the database query where each row is an associative array + * of ['field_name' => 'row_value']. The array of rows can optionally be keyed by a field name, + * but defaults to a sequential numeric array. + * + * NOTE: Chosing to key the result array by a non-unique field name can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of a field on which to key the result array + * @param string $column Instead of the whole row, only this column value will be in the result array + * @return array|null + * @since 2.0.0 + */ + public function loadAssocList($key = null, $column = null) + { + // Initialise variables + $array = []; + + // Execute the query + if (!$this->execute()) + { + return null; + } + + // Get all of the rows from the result set + while ($row = $this->fetchAssoc()) + { + $value = ($column) ? (isset($row[$column]) ? $row[$column] : $row) : $row; + if ($key) + { + $array[$row[$key]] = $value; + } + else + { + $array[] = $value; + } + } + + // Free up system resources and return + $this->freeResult(); + + return $array; + } + + /** + * Gets an array of values from the offset field in each row of the result set from the database query + * + * @param int $offset The row offset to use to build the result array + * @return array|null + * @since 2.0.0 + */ + public function loadColumn($offset = 0) + { + // Initialise variables + $column = []; + + // Execute the query + if (!$this->execute()) + { + return null; + } + + // Get all of the rows from the result set as arrays + while ($row = $this->fetchArray()) + { + $column[] = $row[$offset]; + } + + // Free up system resources and return + $this->freeResult(); + + return $column; + } + + /** + * Gets the next row in the result set from the database query as an object + * + * You must call query() or execute() before calling this method, otherwise + * you'll have nothing to load. + * + * @param string $class The class name to use for the returned row object + * @return object|bool + * @since 2.0.0 + */ + public function loadNextObject($class = 'stdClass') + { + // Get the next row from the result set as an object of type $class + if ($row = $this->fetchObject($class)) + { + return $row; + } + + // Free up system resources and return + $this->freeResult(); + + return false; + } + + /** + * Gets the next row in the result set from the database query as an array + * + * You must call query() or execute() before calling this method, otherwise + * you'll have nothing to load. + * + * @return array|bool + * @since 2.0.0 + */ + public function loadNextRow() + { + // Get the next row from the result set as an array + if ($row = $this->fetchArray()) + { + return $row; + } + + // Free up system resources and return + $this->freeResult(); + + return false; + } + + /** + * Gets the first row of the result set from the database query as an object + * + * @param string $class The class name to use for the returned row object + * @return object|null + * @since 2.0.0 + */ + public function loadObject($class = 'stdClass') + { + // Initialise variables + $return = null; + + // Execute the query + if (!$this->execute()) + { + return null; + } + + // Get the first row from the result set as an object of type $class + if ($row = $this->fetchObject($class)) + { + $return = $row; + } + + // Free up system resources and return + $this->freeResult(); + + return $return; + } + + /** + * Gets the first field of the first row of the result set from the database query + * + * @return string|null + * @since 2.0.0 + */ + public function loadResult() + { + // Initialise variables + $return = null; + + // Execute the query + if (!$this->execute()) + { + return null; + } + + // Get the first row from the result set as an array + if ($row = $this->fetchArray()) + { + $return = $row[0]; + } + + // Free up system resources and return + $this->freeResult(); + + return $return; + } + + /** + * Gets the first row of the result set from the database query as an array + * + * @return array|null + * @since 2.0.0 + */ + public function loadRow() + { + // Initialise variables + $return = null; + + // Execute the query + if (!$this->execute()) + { + return null; + } + + // Get the first row from the result set as an array + if ($row = $this->fetchArray()) + { + $return = $row; + } + + // Free up system resources and return + $this->freeResult(); + + return $return; + } + + /** + * Gets an array of the result set rows from the database query where each row is an array. The array + * of objects can optionally be keyed by a field offset, but defaults to a sequential numeric array. + * + * NOTE: Choosing to key the result array by a non-unique field can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of a field on which to key the result array + * @return array|null + * @since 2.0.0 + */ + public function loadRowList($key = null) + { + // Initialise variables + $rows = []; + + // Execute the query + if (!$this->execute()) + { + return null; + } + + // Get all of the rows from the result set as arrays + while ($row = $this->fetchArray()) + { + if ($key !== null) + { + $rows[$row[$key]] = $row; + } + else + { + $rows[] = $row; + } + } + + // Free up system resources and return + $this->freeResult(); + + return $rows; + } + + /** + * Gets an array of the result set rows from the database query where each row is an object. + * The array of objects can optionally be keyed by a field name, but defaults to a sequential numeric array. + * + * NOTE: Choosing to key the result array by a non-unique field name can result in unwanted + * behavior and should be avoided. + * + * @param string $key The name of the field on which to key the result array + * @param string $class The class name to use for the returned row objects + * @return array + * @since 2.0.0 + */ + public function loadObjectList($key = '', $class = 'stdClass') + { + $rows = []; + + // Execute the query + if (!$this->execute()) + { + return null; + } + + // Get all of the rows from the result set as objects of type $class + while ($row = $this->fetchObject($class)) + { + if ($key) + { + $rows[$row->$key] = $row; + } + else + { + $rows[] = $row; + } + } + + // Free up system resources and return the rows + $this->freeResult(); + + return $rows; + } + + /** + * Get a list of available database connectors. The list will only be populated with connectors that both + * the class exists and the static test method returns true. This gives us the ability to have a multitude + * of connector classes that are self-aware as to whether or not they are able to be used on a given system. + * + * @return array + * @since 2.0.0 + */ + public static function getConnectors() + { + // Instantiate variables + $connectors = []; + + // Get a list of types, only including php files + $types = glob(__DIR__ . DIRECTORY_SEPARATOR . 'Driver' . DIRECTORY_SEPARATOR . '*.php'); + + // Loop through the types and find the ones that are available + foreach ($types as $type) + { + // Get just the file name + $type = basename($type); + + // Derive the class name from the type + $class = __NAMESPACE__ . '\\Driver\\' . str_ireplace('.php', '', ucfirst(trim($type))); + + // If the class doesn't exist...these are not the droids you're looking for... + if (!class_exists($class)) + { + continue; + } + + // Our class exists, so now we just need to know if it passes it's test method + if (call_user_func_array(array($class, 'test'), array())) + { + // Connector names should not have file extensions + $connectors[] = str_ireplace('.php', '', $type); + } + } + + return $connectors; + } + + /** + * Replaces a string placeholder with the string held in the class variable + * + * @param string $sql The SQL statement to prepare + * @param string $prefix The common table prefix + * @return string + * @since 2.0.0 + */ + public function replacePrefix($sql, $prefix = '#__') + { + // As we replace strings of different lengths, subsequent prefix positions will become invalid. + // Thus, we track that differential here to account for the shifting locations + $differential = strlen($this->tablePrefix) - strlen($prefix); + $count = 0; + + foreach (Str::findLiteral($prefix, $sql) as $prefixPosition) + { + $sql = substr_replace($sql, $this->tablePrefix, $prefixPosition + ($differential*$count), strlen($prefix)); + $count++; + } + + return $sql; + } + + /** + * Splits a string of multiple queries into an array of individual queries + * + * @param string $sql Input SQL string from which to split into individual queries + * @return array + * @since 2.0.0 + */ + public static function splitSql($sql) + { + $start = 0; + $length = strlen($sql); + $queries = []; + + foreach (Str::findLiteral(';', $sql) as $queryEndPosition) + { + $queries[] = trim(substr($sql, $start, $queryEndPosition - $start + 1)); + $start = $queryEndPosition + 1; + } + + // Grab the last query in case it doesn't end with a ';' + if ($end = trim(substr($sql, $start))) + { + $queries[] = $end; + } + + return $queries; + } + + /** + * Logs the current sql statement + * + * @param int $time The time elapsed during query execution + * @return $this + * @since 2.0.0 + **/ + protected function log($time) + { + // Build the actual query + $query = $this->toString(); + + Event::trigger('database_query', [ + 'query' => $query, + 'time' => $time + ]); + + // Add it to the internal logs + $this->log[] = $query; + $this->count++; + $this->timer += $time; + } + + /** + * Gets the string version of the query + * + * @return string + * @since 2.0.0 + **/ + public function toString() + { + // Build the actual query + if (is_object($this->statement)) + { + $query = $this->interpolate($this->statement->queryString, $this->bindings); + } + else + { + $query = $this->statement; + } + + return $query; + } + + /** + * Builds a string version of the prepared statement + * + * @param string $query The query string to use as the base + * @param array $bindings The bindings to interpolate in + * @return string + * @since 2.0.0 + **/ + private function interpolate($query, $bindings) + { + $offset = 0; + $index = 0; + + foreach (Str::findLiteral('?', $query) as $placeholder) + { + $sub = (is_null($bindings[$index])) ? 'NULL' : $this->quote($bindings[$index]); + $query = substr_replace($query, $sub, $placeholder + $offset, 1); + $offset += (strlen($sub) - 1); + $index++; + } + + return $query; + } + + /** + * Executes the SQL statement (basically an alias for execute()) + * + * @return static + * @since 2.0.0 + */ + public function query() + { + return $this->execute(); + } + + /** + * Sets the database debugging state for the driver + * + * @param bool $level True to enable debugging + * @return $this + * @since 2.0.0 + */ + public function setDebug($level) + { + $this->debug = (bool) $level; + + return $this; + } + + /** + * Enables debugging + * + * @return $this + * @since 2.0.0 + */ + public function enableDebugging() + { + return $this->setDebug(true); + } + + /** + * Disables debugging + * + * @return $this + * @since 2.0.0 + */ + public function disableDebugging() + { + return $this->setDebug(false); + } + + /** + * Truncates a table + * + * @param string $table The table to truncate + * @return void + * @since 2.0.0 + */ + public function truncateTable($table) + { + $this->setQuery('TRUNCATE TABLE ' . $this->quoteName($table)); + $this->execute(); + } + + /** + * Grabs the underlying database connection + * + * Useful for when you need to call a proprietary method such as postgresql's lo_* methods. + * + * @return mixed + * @since 2.0.0 + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Grabs the syntax + * + * @return string + * @since 2.0.0 + */ + public function getSyntax() + { + return $this->syntax; + } + + /** + * Sets the syntax + * + * @param string $syntax The syntax being used based on the connection + * @return $this + * @since 2.0.0 + */ + public function setSyntax($syntax) + { + $this->syntax = $syntax; + + return $this; + } + + /** + * Gets the total number of SQL statements executed by the database driver + * + * @return int + * @since 2.0.0 + */ + public function getCount() + { + return $this->count; + } + + /** + * Gets the name of the database in use by this connection + * + * @return string + * @since 2.0.0 + */ + protected function getDatabase() + { + return $this->database; + } + + /** + * Returns a PHP date() function compliant date format for the database driver + * + * @return string + * @since 2.0.0 + */ + public function getDateFormat() + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the database driver SQL statement log + * + * @return array + * @since 2.0.0 + */ + public function getLog() + { + return $this->log; + } + + /** + * Gets the database timer + * + * @return int + * @since 2.0.0 + */ + public function getTimer() + { + return $this->timer; + } + + /** + * Gets the null or zero representation of a timestamp for the database driver + * + * @return string + * @since 2.0.0 + */ + public function getNullDate() + { + return $this->nullDate; + } + + /** + * Gets the common table prefix for the database driver + * + * @return string + * @since 2.0.0 + */ + public function getPrefix() + { + return $this->tablePrefix; + } + + /** + * Sets the common table prefix for the database driver + * + * @param string $prefix The prefix to use + * @return $this + * @since 2.0.0 + */ + public function setPrefix($prefix) + { + $this->tablePrefix = $prefix; + + return $this; + } + + /** + * Gets the current sql statement + * + * @return string + * @since 2.0.0 + */ + public function getStatement() + { + return (is_object($this->statement)) + ? $this->interpolate($this->statement->queryString, $this->bindings) + : $this->statement; + } + + /** + * Fetchs a row from the result set as an object + * + * @param string $class The class name to use for the returned row object + * @return mixed + * @since 2.0.0 + */ + abstract protected function fetchObject($class = 'stdClass'); + + /** + * Fetches a row from the result set as an array + * + * @return mixed + * @since 2.0.0 + */ + abstract protected function fetchArray(); + + /** + * Fetches a row from the result set as an associative array + * + * @return mixed + * @since 2.0.0 + */ + abstract protected function fetchAssoc(); + + /** + * Detects the driver syntax + * + * @return string + * @since 2.0.0 + **/ + abstract protected function detectSyntax(); + + /** + * Frees up the memory used for the result set + * + * @return $this + * @since 2.0.0 + */ + abstract protected function freeResult(); + + /** + * Sets the error reporting mode to throw exceptions + * + * @return $this + * @since 2.0.0 + **/ + abstract public function throwExceptions(); + + /** + * Sets the error reporting mode to return errors + * + * @return $this + * @since 2.0.0 + **/ + abstract public function returnErrors(); + + /** + * Checks for a database connection, throwing an exception if not + * + * @return $this + * @since 2.0.0 + **/ + abstract public function hasConnectionOrFail(); + + /** + * Prepares a query for binding + * + * @param string $statement The query statement to prepare + * @return $this + * @since 2.0.0 + **/ + abstract public function prepare($statement); + + /** + * Binds the given bindings to the prepared statement + * + * @param array $bindings The param bindings + * @param string $type The param type + * @return $this + * @since 2.0.0 + **/ + abstract public function bind($bindings, $type = null); + + /** + * Gets the auto-incremented value from the last INSERT statement + * + * @return int + * @since 2.0.0 + */ + abstract public function insertid(); + + /** + * Sets the connection to use UTF-8 character encoding + * + * @return bool + * @since 2.0.0 + */ + abstract public function setUTF(); + + /** + * Initializes a transaction + * + * @return void + * @since 2.0.0 + */ + abstract public function transactionStart(); + + /** + * Rolls back a transaction + * + * @return void + * @since 2.0.0 + */ + abstract public function transactionRollback(); + + /** + * Commits a transaction + * + * @return void + * @since 2.0.0 + */ + abstract public function transactionCommit(); + + /** + * Unlocks all tables in the database + * + * @return $this + * @since 2.0.0 + */ + abstract public function unlockTables(); + + /** + * Locks a table in the database + * + * @param string $tableName The name of the table to lock + * @return $this + * @since 2.0.0 + */ + abstract public function lockTable($tableName); + + /** + * Executes the set SQL statement + * + * @return $this|bool + * @since 2.0.0 + */ + abstract public function execute(); + + /** + * Renames a table in the database + * + * @param string $oldTable The name of the table to be renamed + * @param string $newTable The new name for the table + * @param string $backup Table prefix + * @param string $prefix For the table - used to rename constraints in non-mysql databases + * @return $this + * @since 2.0.0 + */ + abstract public function renameTable($oldTable, $newTable, $backup = null, $prefix = null); + + /** + * Selects a database for use + * + * @param string $database The name of the database to select for use + * @return bool + * @since 2.0.0 + */ + abstract public function select($database); + + /** + * Gets a new query for the current driver + * + * @return Query + * @since 2.0.0 + */ + abstract public function getQuery(); + + /** + * Retrieves field information about the given tables + * + * @param string $table The name of the database table + * @param bool $typeOnly True (default) to only return field types + * @return array + * @since 2.0.0 + */ + abstract public function getTableColumns($table, $typeOnly = true); + + /** + * Shows the table CREATE statement that creates the given tables + * + * @param string|array $tables A table name or a list of table names + * @return array + * @since 2.0.0 + */ + abstract public function getTableCreate($tables); + + /** + * Retrieves key information about the given tables + * + * @param string|array $tables A table name or a list of table names + * @return array + * @since 2.0.0 + */ + abstract public function getTableKeys($tables); + + /** + * Gets an array of all tables in the database + * + * @return array + * @since 2.0.0 + */ + abstract public function getTableList(); + + /** + * Gets the version of the database connector + * + * @return string + * @since 2.0.0 + */ + abstract public function getVersion(); + + /** + * Determines if the connection to the server is active + * + * @return bool + * @since 2.0.0 + */ + abstract public function connected(); + + /** + * Drops a table from the database + * + * @param string $table The name of the database table to drop + * @param bool $ifExists Optionally specify that the table must exist before it is dropped + * @return $this + * @since 2.0.0 + */ + abstract public function dropTable($table, $ifExists = true); + + /** + * Escapes a string for usage in an SQL statement + * + * @param string $text The string to be escaped + * @param boolean $extra Optional parameter to provide extra escaping + * @return string + * @since 2.0.0 + */ + abstract public function escape($text, $extra = false); + + /** + * Gets the number of affected rows for the previous executed SQL statement + * + * @return int + * @since 2.0.0 + */ + abstract public function getAffectedRows(); + + /** + * Gets the database collation in use by sampling a text field of a table in the database + * + * @return string|bool + * @since 2.0.0 + */ + abstract public function getCollation(); + + /** + * Grabs the number of returned rows for the previous executed SQL statement + * + * @return int + * @since 2.0.0 + */ + abstract public function getNumRows(); + + /** + * Checks for the existance of a table + * + * @param string $table The table we're looking for + * @return bool + * @since 2.0.0 + */ + abstract public function tableExists($table); + + /** + * Returns whether or not the given table has a given field + * + * @param string $table A table name + * @param string $field A field name + * @return bool + * @since 2.0.0 + */ + abstract public function tableHasField($table, $field); + + /** + * Returns whether or not the given table has a given key + * + * @param string $table A table name + * @param string $key A key name + * @return bool + * @since 2.0.0 + */ + abstract public function tableHaskey($table, $key); + + /** + * Gets the primary key of a table + * + * @return string + * @since 2.0.0 + **/ + abstract public function getPrimaryKey($table); + + /** + * Gets the database engine of the given table + * + * @param string $table The table for which to retrieve the engine type + * @return string|bool + * @since 2.0.0 + **/ + abstract public function getEngine($table); + + /** + * Set the database engine of the given table + * + * @param string $table The table for which to retrieve the engine type + * @param string $engine The engine type to set + * @return bool + * @since 2.2.15 + **/ + abstract public function setEngine($table, $engine); + + /** + * Gets the database character set of the given table + * + * @param string $table The table for which to retrieve the character set + * @param string $field The field to check (optional) + * @return string|bool + * @since 2.0.0 + **/ + abstract public function getCharacterSet($table, $field = null); + + /** + * Gets the auto-increment value for the given table + * + * @param string $table The table for which to retrieve the character set + * @return int|bool + * @since 2.0.0 + **/ + abstract public function getAutoIncrement($table); +} diff --git a/core/libraries/Hubzero/Database/Driver/Mariadb.php b/core/libraries/Hubzero/Database/Driver/Mariadb.php new file mode 100644 index 00000000000..bf188ede708 --- /dev/null +++ b/core/libraries/Hubzero/Database/Driver/Mariadb.php @@ -0,0 +1,18 @@ +setConnection(new \PDO( + (string)$options['dsn'], + (string)$options['user'], + (string)$options['password'], + (array)$options['extras'] + )); + } + catch (\PDOException $e) + { + throw new ConnectionFailedException($e->getMessage(), 500); + } + + // Set error reporting to throw exceptions + $this->throwExceptions(); + + // Call parent construct + parent::__construct($options); + + // @FIXME: Set sql_mode to non_strict mode? + } + + /** + * Sets the error reporting mode to throw exceptions + * + * @return $this + * @since 2.0.0 + **/ + public function throwExceptions() + { + $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + return $this; + } + + /** + * Sets the error reporting mode to return errors + * + * @return $this + * @since 2.0.0 + **/ + public function returnErrors() + { + // Even though this says "SILENT", that doesn't mean that it isn't registering errors + $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); + + return $this; + } + + /** + * Checks for a database connection + * + * @return $this + * @since 2.0.0 + **/ + public function hasConnectionOrFail() + { + if (!is_object($this->connection)) + { + throw new ConnectionFailedException('No database connection.', 500); + } + + return $this; + } + + /** + * Prepares a query for binding + * + * @param string $statement The statement to prepare + * @return $this + * @since 2.0.0 + **/ + public function prepare($statement) + { + $this->statement = $this->connection->prepare($this->replacePrefix($statement)); + + return $this; + } + + /** + * Binds the given bindings to the prepared statement + * + * If you're going to pass in types, they must be keyed + * the same as the bindings. + * + * @param array $bindings The param bindings + * @param array $type The param types + * @return $this + * @since 2.0.0 + **/ + public function bind($bindings, $type = []) + { + $idx = 1; + + $this->bindings = $bindings; + + foreach ($bindings as $binding) + { + // We use bindValue here because that allows us to pass in plain old strings + $this->statement->bindValue( + $idx, + $binding, + isset($type[$idx]) ? $this->translateType($type[$idx]) : $this->inferType($binding) + ); + + $idx++; + } + + return $this; + } + + /** + * Explicitly translate generic type to driver specific types + * + * @param string $type The variable type (bool, null, int, str) + * @return int + * @since 2.0.0 + **/ + private function translateType($type) + { + return constant('\PDO::PARAM_' . strtoupper($type)); + } + + /** + * Infers the variable type from the variable itself + * + * Some sql syntax is more particular about type than others. + * + * @param mixed $binding The binding to infer from + * @return int + * @since 2.0.0 + **/ + private function inferType($binding) + { + if (is_bool($binding)) + { + $type = \PDO::PARAM_BOOL; + } + elseif (is_null($binding)) + { + $type = \PDO::PARAM_NULL; + } + elseif (is_int($binding)) + { + $type = \PDO::PARAM_INT; + } + else + { + $type = \PDO::PARAM_STR; + } + + return $type; + } + + /** + * Executes the SQL statement + * + * @return $this + * @since 2.0.0 + * @throws QueryFailedException + */ + public function execute() + { + // Check connection + $this->hasConnectionOrFail(); + + // Capture the start time + $start = microtime(true); + + // Execute the query + try + { + $this->statement->execute(); + } + catch (\PDOException $e) + { + // @FIXME: this should honor error reporting settings + throw new QueryFailedException($e->getMessage(), 500, $e); + } + + if ($this->debug) + { + // Log it + $this->log(microtime(true) - $start); + } + + return $this; + } + + /** + * Fetches a row from the result set cursor as an object + * + * @param string $class The class name to use for the returned row object + * @return object|null + * @since 2.0.0 + */ + protected function fetchObject($class = 'stdClass') + { + return $this->statement->fetchObject($class); + } + + /** + * Fetches a row from the result set as an array + * + * @return mixed + * @since 2.0.0 + */ + protected function fetchArray() + { + return $this->statement->fetch(\PDO::FETCH_NUM); + } + + /** + * Fetches a row from the result set as an associative array + * + * @return mixed + * @since 2.0.0 + */ + protected function fetchAssoc() + { + return $this->statement->fetch(\PDO::FETCH_ASSOC); + } + + /** + * Gets the auto-incremented value from the last INSERT statement + * + * @return int + * @since 2.0.0 + */ + public function insertid() + { + return $this->connection->lastInsertId(); + } + + /** + * Frees up the memory used for the result set + * + * @return $this + * @since 2.0.0 + */ + protected function freeResult() + { + $this->statement->closeCursor(); + + return $this; + } + + /** + * Drops a table from the database + * + * @param string $tableName The name of the database table to drop + * @param boolean $ifExists Optionally specify that the table must exist before it is dropped + * @return $this + * @since 2.0.0 + */ + public function dropTable($tableName, $ifExists = true) + { + $this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $this->quoteName($tableName)) + ->execute(); + + return $this; + } + + /** + * Gets the database collation in use + * + * @return string|bool + * @since 2.0.0 + */ + public function getCollation() + { + // Attempt to get the database collation by accessing the server system variable. + $this->setQuery('SHOW VARIABLES LIKE "collation_database"'); + $result = $this->loadObject(); + + if (property_exists($result, 'Value')) + { + return $result->Value; + } + + return false; + } + + /** + * Shows the table CREATE statement that creates the given tables + * + * @param string|array $tables A table name or a list of table names + * @return array + * @since 2.0.0 + */ + public function getTableCreate($tables) + { + // Initialise variables + $result = []; + + foreach ((array)$tables as $table) + { + // Set the query to get the table CREATE statement. + $this->setQuery('SHOW CREATE table ' . $this->quoteName($this->escape($table))); + $row = $this->loadRow(); + + // Populate the result array based on the create statements + $result[$table] = $row[1]; + } + + return $result; + } + + /** + * Retrieves field information about the given table + * + * @param string $table The name of the database table + * @param bool $typeOnly True (default) to only return field types + * @return array + * @since 2.0.0 + */ + public function getTableColumns($table, $typeOnly = true) + { + $result = []; + + // Set the query to get the table fields statement + $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($this->escape($table))); + $fields = $this->loadObjectList(); + + // If we only want the type as the value add just that to the list + if ($typeOnly) + { + foreach ($fields as $field) + { + $result[$field->Field] = preg_replace("/[(0-9)]/", '', $field->Type); + } + } + // If we want the whole field data object add that to the list + else + { + foreach ($fields as $field) + { + $result[$field->Field] = $field; + } + } + + return $result; + } + + /** + * Retrieves key information about the given tables + * + * @param string|array $tables A table name or a list of table names + * @return array + * @since 2.0.0 + */ + public function getTableKeys($table) + { + // Get the details columns information + $this->setQuery('SHOW KEYS FROM ' . $this->quoteName($table)); + $keys = $this->loadObjectList('Key_name'); + + return $keys; + } + + /** + * Gets an array of all tables in the database + * + * @return array + * @since 2.0.0 + */ + public function getTableList() + { + // Set the query to get the tables statement + $this->setQuery('SHOW TABLES'); + $tables = $this->loadColumn(); + + return $tables; + } + + /** + * Locks a table in the database + * + * @param string $tableName The name of the table to lock + * @return $this + * @since 2.0.0 + */ + public function lockTable($table) + { + $this->setQuery('LOCK TABLES ' . $this->quoteName($table) . ' WRITE')->execute(); + + return $this; + } + + /** + * Renames a table in the database + * + * @param string $oldTable The name of the table to be renamed + * @param string $newTable The new name for the table + * @param string $backup Table prefix + * @param string $prefix For the table - used to rename constraints in non-mysql databases + * @return $this + * @since 2.0.0 + */ + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + $this->setQuery('RENAME TABLE ' . $oldTable . ' TO ' . $newTable)->execute(); + + return $this; + } + + /** + * Commits a transaction + * + * @return void + * @since 2.0.0 + */ + public function transactionCommit() + { + $this->setQuery('COMMIT')->execute(); + } + + /** + * Rolls back a transaction + * + * @return void + * @since 2.0.0 + */ + public function transactionRollback() + { + $this->setQuery('ROLLBACK')->execute(); + } + + /** + * Initializes a transaction + * + * @return void + * @since 2.0.0 + */ + public function transactionStart() + { + $this->setQuery('START TRANSACTION')->execute(); + } + + /** + * Unlocks all tables in the database + * + * @return $this + * @since 2.0.0 + */ + public function unlockTables() + { + $this->setQuery('UNLOCK TABLES')->execute(); + + return $this; + } + + /** + * Checks for the existance of a table + * + * @param string $table The table we're looking for + * @return bool + * @since 2.0.0 + */ + public function tableExists($table) + { + $query = 'SHOW TABLES LIKE ' . str_replace('#__', $this->tablePrefix, $this->quote($table, false)); + $this->setQuery($query)->execute(); + + return ($this->getAffectedRows() > 0) ? true : false; + } + + /** + * Returns whether or not the given table has a given field + * + * @param string $table A table name + * @param string $field A field name + * @return bool + * @since 2.0.0 + */ + public function tableHasField($table, $field) + { + $this->setQuery('SHOW FIELDS FROM ' . $table); + $fields = $this->loadObjectList('Field'); + + if (!is_array($fields)) + { + return false; + } + + return (in_array($field, array_keys($fields))) ? true : false; + } + + /** + * Returns whether or not the given table has a given key + * + * @param string $table A table name + * @param string $key A key name + * @return bool + * @since 2.0.0 + */ + public function tableHaskey($table, $key) + { + $keys = $this->getTableKeys($table); + + if (!is_array($keys)) + { + return false; + } + + return isset($keys[$key]); + } + + /** + * Gets the primary key of a table + * + * @return string + * @since 2.0.0 + **/ + public function getPrimaryKey($table) + { + $keys = $this->getTableKeys($table); + $key = false; + + if ($keys && count($keys) > 0) + { + foreach ($keys as $k) + { + if ($k->Key_name == 'PRIMARY') + { + $key = $k->Column_name; + } + } + } + + return $key; + } + + /** + * Gets the database engine of the given table + * + * @param string $table The table for which to retrieve the engine type + * @return string|bool + * @since 2.0.0 + **/ + public function getEngine($table) + { + $this->setQuery('SHOW TABLE STATUS WHERE Name = ' . str_replace('#__', $this->tablePrefix, $this->quote($table, false))); + + return ($info = $this->loadObjectList()) ? $info[0]->Engine : false; + } + + /** + * Set the database engine of the given table + * + * @param string $table The table for which to retrieve the engine type + * @param string $engine The engine type to set + * @return bool + * @since 2.2.15 + **/ + public function setEngine($table, $engine) + { + $supported = ['innodb', 'myisam', 'archive', 'merge', 'memory', 'csv', 'federated']; + + if (!in_array(strtolower($engine), $supported)) + { + throw new UnsupportedEngineException(sprintf( + 'Unsupported engine type of "%s" specified. Engine type must be one of %s', + $engine, + implode(', ', $supported) + )); + } + + $this->setQuery('ALTER TABLE ' . str_replace('#__', $this->tablePrefix, $this->quote($table, false)) . " ENGINE = " . $this->quote($engine)); + + return $this->db->query(); + } + + /** + * Gets the database character set of the given table + * + * @param string $table The table for which to retrieve the character set + * @param string $field The field to check (optional) + * @return string|bool + * @since 2.0.0 + **/ + public function getCharacterSet($table, $field = null) + { + $create = $this->getTableCreate($table); + + if (isset($field)) + { + preg_match('/' . $this->quoteName($field) . ' [[:alnum:]\(\)]* CHARACTER SET ([[:alnum:]]*)/', $create[$table], $matches); + } + else + { + preg_match('/CHARSET=([[:alnum:]]*)/', $create[$table], $matches); + } + + return (isset($matches[1])) ? $matches[1] : false; + } + + /** + * Gets the auto-increment value for the given table + * + * @param string $table The table for which to retrieve the character set + * @return int|bool + * @since 2.0.0 + **/ + public function getAutoIncrement($table) + { + $create = $this->getTableCreate($table); + + preg_match('/AUTO_INCREMENT=([0-9]*)/', $create[$table], $matches); + + return (isset($matches[1])) ? $matches[1] : false; + } + + /** + * Escapes a string for usage in an SQL statement + * + * In PDO, the quote method does both escaping and quoting, thus calls + * coming from the quote method need to have the leading and trailing + * quotes removed...otherwise it will be double-quoted. + * + * @FIXME: if escape is called directly, we shouldn't remove first and last char + * + * @param string $text The string to be escaped + * @param bool $extra Optional parameter to provide extra escaping + * @return string + * @since 2.0.0 + */ + public function escape($text, $extra = false) + { + $result = substr($this->connection->quote($text), 1, -1); + + if ($extra) + { + $result = addcslashes($result, '%_'); + } + + return $result; + } + + /** + * Test to see if the PDO connector is available. + * + * @return bool + * @since 2.0.0 + */ + public static function test() + { + return (class_exists('\PDO')); + } + + /** + * Determines if the connection to the server is active + * + * @return bool + * @since 2.0.0 + */ + public function connected() + { + if (is_object($this->connection)) + { + return $this->connection->query("SELECT 1")->fetchAll()[0][1]; + } + + return false; + } + + /** + * Gets the number of affected rows for the previous executed SQL statement + * + * @return int + * @since 2.0.0 + */ + public function getAffectedRows() + { + return $this->statement->rowCount(); + } + + /** + * Gets a new query for the current driver + * + * @param bool $legacy Whether or not to return new query builder or legacy builder + * @return Query + * @since 2.0.0 + */ + public function getQuery($legacy = false) + { + if ($legacy) + { + return new \JDatabaseQueryPDOMySQL($this); + } + + return new Query($this); + } + + /** + * Gets the version of the database connector + * + * @return string + * @since 2.0.0 + */ + public function getVersion() + { + return $this->connection->query("SHOW VARIABLES LIKE '%version%'")->fetchAll()[3]['Value']; + } + + /** + * Selects a database for use + * + * @param string $database The name of the database to select for use + * @return bool + * @since 2.0.0 + */ + public function select($database) + { + if (empty($database)) + { + return false; + } + + $this->connection->exec('USE ' . $this->quoteName($database)); + + $this->database = $database; + + return true; + } + + /** + * Sets the connection to use UTF-8 character encoding + * + * This is already happening in the initial database connection for PDO. + * + * @return bool + * @since 2.0.0 + */ + public function setUTF() + { + return false; + } + + /** + * Grabs the number of returned rows for the previous executed SQL statement + * + * @return int + * @since 2.0.0 + */ + public function getNumRows() + { + // @FIXME: this isn't guaranteed to work on select statements in mysql + return $this->statement->rowCount(); + } + + /** + * Detects the driver syntax + * + * @return string + * @since 2.0.0 + **/ + protected function detectSyntax() + { + return $this->connection->getAttribute(\PDO::ATTR_DRIVER_NAME); + } +} diff --git a/core/libraries/Hubzero/Database/Driver/Percona.php b/core/libraries/Hubzero/Database/Driver/Percona.php new file mode 100644 index 00000000000..2354dfe51b6 --- /dev/null +++ b/core/libraries/Hubzero/Database/Driver/Percona.php @@ -0,0 +1,55 @@ +setQuery('ALTER TABLE ' . str_replace('#__', $this->tablePrefix, $this->quote($table, false)) . " ENGINE = " . $this->quote($engine)); + + return $this->db->query(); + } +} diff --git a/core/libraries/Hubzero/Database/Driver/Pgsql.php b/core/libraries/Hubzero/Database/Driver/Pgsql.php new file mode 100644 index 00000000000..d5ef2a4a7d4 --- /dev/null +++ b/core/libraries/Hubzero/Database/Driver/Pgsql.php @@ -0,0 +1,405 @@ +setQuery('SHOW LC_COLLATE'); + $array = $this->loadAssocList(); + + return $array[0]['lc_collate']; + } + + /** + * Shows the table CREATE statement that creates the given tables + * + * This is unsuported by Postgres + * + * @param string|array $tables A table name or a list of table names + * @return array + * @since 2.2.15 + */ + public function getTableCreate($tables) + { + // Initialise variables + $result = []; + + return $result; + } + + /** + * Retrieves field information about the given table + * + * @param string $table The name of the database table. + * @param boolean $typeOnly True to only return field types. + * @return array An array of fields for the database table. + * @since 2.2.15 + */ + public function getTableColumns($table, $typeOnly = true) + { + $result = array(); + + $tableSub = $this->replacePrefix($table); + + $this->setQuery(' + SELECT a.attname AS "column_name", + pg_catalog.format_type(a.atttypid, a.atttypmod) as "type", + CASE WHEN a.attnotnull IS TRUE + THEN \'NO\' + ELSE \'YES\' + END AS "null", + CASE WHEN pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) IS NOT NULL + THEN pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) + END as "Default", + CASE WHEN pg_catalog.col_description(a.attrelid, a.attnum) IS NULL + THEN \'\' + ELSE pg_catalog.col_description(a.attrelid, a.attnum) + END AS "comments" + FROM pg_catalog.pg_attribute a + LEFT JOIN pg_catalog.pg_attrdef adef ON a.attrelid=adef.adrelid AND a.attnum=adef.adnum + LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid + WHERE a.attrelid = + ( + SELECT oid FROM pg_catalog.pg_class WHERE relname=' . $this->quote($tableSub) . ' + AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = \'public\') + ) + AND a.attnum > 0 AND NOT a.attisdropped + ORDER BY a.attnum' + ); + + $fields = $this->loadObjectList(); + + if ($typeOnly) + { + foreach ($fields as $field) + { + $result[$field->column_name] = preg_replace('/[(0-9)]/', '', $field->type); + } + } + else + { + foreach ($fields as $field) + { + if (stristr(strtolower($field->type), 'character varying')) + { + $field->Default = ''; + } + + if (stristr(strtolower($field->type), 'text')) + { + $field->Default = ''; + } + + // Normalize output + $result[$field->column_name] = (object) array( + 'Default' => $field->Default, + 'Comment' => '', + 'Field' => $field->column_name, + 'Type' => $field->type, + 'Null' => $field->null + ); + } + } + + // Change Postgres' NULL::* type with PHP's null one + foreach ($fields as $field) + { + if (preg_match('/^NULL::*/', $field->Default)) + { + $field->Default = null; + } + } + + return $result; + } + + /** + * Get the details list of keys for a table. + * + * @param string $table The name of the table. + * @return array An array of the column specification for the table. + * @since 2.2.15 + */ + public function getTableKeys($table) + { + $this->connect(); + + // To check if table exists and prevent SQL injection + $tableList = $this->getTableList(); + + if (in_array($table, $tableList, true)) + { + // Get the details columns information. + $this->setQuery(' + SELECT indexname AS "idxName", indisprimary AS "isPrimary", indisunique AS "isUnique", + CASE WHEN indisprimary = true + THEN ( + SELECT \'ALTER TABLE \' || tablename || \' ADD \' || pg_catalog.pg_get_constraintdef(const.oid, true) + FROM pg_constraint AS const WHERE const.conname= pgClassFirst.relname + ) + ELSE pg_catalog.pg_get_indexdef(indexrelid, 0, true) + END AS "Query" + FROM pg_indexes + LEFT JOIN pg_class AS pgClassFirst ON indexname=pgClassFirst.relname + LEFT JOIN pg_index AS pgIndex ON pgClassFirst.oid=pgIndex.indexrelid + WHERE tablename=' . $this->quote($table) . ' ORDER BY indkey' + ); + + return $this->loadObjectList('idxName'); + } + + return false; + } + + /** + * Gets an array of all tables in the database + * + * @return array + * @since 2.2.15 + */ + public function getTableList() + { + // Set the query to get the tables statement + $query = $this->getQuery() + ->select('table_name') + ->from('information_schema.tables') + ->whereEquals('table_type', 'BASE TABLE') + ->whereNotIn('table_schema', ['pg_catalog', 'information_schema']) + ->order('table_name', 'asc') + ->toString(); + + $this->setQuery($query); + $tables = $this->loadColumn(); + + return $tables; + } + + /** + * Locks a table in the database + * + * @param string $tableName The name of the table to lock + * @return $this + * @since 2.2.15 + */ + public function lockTable($tableName) + { + $this->setQuery('LOCK TABLE ' . $this->quoteName($tableName) . ' IN ACCESS EXCLUSIVE MODE')->execute(); + + return $this; + } + + /** + * Renames a table in the database + * + * @param string $oldTable The name of the table to be renamed + * @param string $newTable The new name for the table + * @param string $backup Table prefix + * @param string $prefix For the table - used to rename constraints in non-mysql databases + * @return $this + * @since 2.2.15 + */ + public function renameTable($oldTable, $newTable, $backup = null, $prefix = null) + { + // To check if table exists and prevent SQL injection + $tableList = $this->getTableList(); + + // Origin Table does not exist + if (!in_array($oldTable, $tableList, true)) + { + throw new \RuntimeException('Table not found in Postgres database.'); + } + + // Rename indexes + $subQuery = $this->getQuery() + ->select('indexrelid') + ->from('pg_index') + ->join('pg_class', 'pg_class.oid', 'pg_index.indrelid') + ->whereEquals('pg_class.relname', $oldTable) + ->toString(); + + $this->setQuery( + $this->getQuery() + ->select('relname') + ->from('pg_class') + ->whereRaw('oid IN (' . (string) $subQuery . ')') + ->toString() + ); + + $oldIndexes = $this->loadColumn(); + + foreach ($oldIndexes as $oldIndex) + { + $changedIdxName = str_replace($oldTable, $newTable, $oldIndex); + $this->setQuery('ALTER INDEX ' . $this->escape($oldIndex) . ' RENAME TO ' . $this->escape($changedIdxName))->execute(); + } + + // Rename sequences + $subQuery = $this->getQuery() + ->select('oid') + ->from('pg_namespace') + ->whereNotLike('nspname', 'pg_%') + ->where('nspname', '!=', 'information_schema') + ->toString(); + + $this->setQuery( + $this->getQuery() + ->select('relname') + ->from('pg_class') + ->whereEquals('relkind', 'S') + ->whereRaw('relnamespace IN (' . (string) $subQuery . ')') + ->whereLike('relname', "%$oldTable%") + ->toString() + ); + + $oldSequences = $this->loadColumn(); + + foreach ($oldSequences as $oldSequence) + { + $changedSequenceName = str_replace($oldTable, $newTable, $oldSequence); + $this->setQuery('ALTER SEQUENCE ' . $this->escape($oldSequence) . ' RENAME TO ' . $this->escape($changedSequenceName))->execute(); + } + + // Rename table + $this->setQuery('ALTER TABLE ' . $this->escape($oldTable) . ' RENAME TO ' . $this->escape($newTable))->execute(); + + return true; + } + + /** + * Selects a database for use + * + * This is unsuported by Postgres + * + * @param string $database The name of the database to select for use + * @return bool + * @since 2.2.15 + */ + public function select($database) + { + return false; + } + + /** + * Gets the database engine of the given table + * + * @param string $table The table for which to retrieve the engine type + * @return string|bool + * @since 2.2.15 + **/ + public function getEngine($table) + { + return 'MVCC'; + } + + /** + * Set the database engine of the given table + * + * This is unsuported by Postgres + * + * @param string $table The table for which to retrieve the engine type + * @param string $engine The engine type to set + * @return bool + * @since 2.2.15 + **/ + public function setEngine($table, $engine) + { + return false; + } + + /** + * Gets the database character set of the given table + * + * @param string $table The table for which to retrieve the character set + * @param string $field The field to check (optional) + * @return string|bool + * @since 2.2.15 + **/ + public function getCharacterSet($table, $field = null) + { + // Postgres only supports encodings for an entire database + // Not encodings on a per-table or per-column level. + $this->setQuery("SELECT * FROM information_schema.character_sets;"); + + $result = $this->loadResult(); + + return $result ? $result : false; + } + + /** + * Gets the version of the database connector + * + * @return string + * @since 2.2.15 + */ + public function getVersion() + { + return $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION); + } +} diff --git a/core/libraries/Hubzero/Database/Driver/Sqlite.php b/core/libraries/Hubzero/Database/Driver/Sqlite.php new file mode 100644 index 00000000000..1666dff2591 --- /dev/null +++ b/core/libraries/Hubzero/Database/Driver/Sqlite.php @@ -0,0 +1,246 @@ +connection->getAttribute(\PDO::ATTR_CASE); + + $this->connection->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_UPPER); + + $table = strtoupper($table); + + $this->setQuery('pragma table_info(' . $table . ')'); + $fields = $this->loadObjectList(); + + if ($typeOnly) + { + foreach ($fields as $field) + { + $columns[$field->NAME] = $field->TYPE; + } + } + else + { + foreach ($fields as $field) + { + // Normalize output + $columns[$field->NAME] = (object) array( + 'Field' => $field->NAME, + 'Type' => $field->TYPE, + 'Null' => ($field->NOTNULL == '1' ? 'NO' : 'YES'), + 'Default' => $field->DFLT_VALUE, + 'Key' => ($field->PK != '0' ? 'PRI' : '') + ); + } + } + + $this->connection->setAttribute(\PDO::ATTR_CASE, $fieldCasing); + + return $columns; + } + + /** + * Retrieves key information about the given tables + * + * @param string|array $tables A table name or a list of table names + * @return array + * @since 2.2.15 + */ + public function getTableKeys($table) + { + $keys = array(); + + $fieldCasing = $this->connection->getAttribute(\PDO::ATTR_CASE); + + $this->connection->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_UPPER); + + $table = strtoupper($table); + + $this->setQuery('pragma table_info( ' . $table . ')'); + $rows = $this->loadObjectList(); + + foreach ($rows as $column) + { + if ($column->PK == 1) + { + $keys[$column->NAME] = $column; + } + } + + $this->connection->setAttribute(\PDO::ATTR_CASE, $fieldCasing); + + return $keys; + } + + /** + * Gets an array of all tables in the database + * + * @return array + * @since 2.2.15 + */ + public function getTableList() + { + $query = $this->getQuery() + ->select('name') + ->from('sqlite_master') + ->whereEquals('type', 'table') + ->order('name', 'asc') + ->toString(); + + $this->setQuery($query); + + return $this->loadColumn(); + } + + /** + * Locks a table in the database + * + * This is unsuported by SQLite + * + * @param string $tableName The name of the table to lock + * @return $this + * @since 2.2.15 + */ + public function lockTable($tableName) + { + return $this; + } + + /** + * Unlocks all tables in the database + * + * This is unsuported by SQLite + * + * @return $this + * @since 2.0.0 + */ + public function unlockTables() + { + return $this; + } + + /** + * Selects a database for use + * + * This is unsuported by SQLite + * + * @param string $database The name of the database to select for use + * @return bool + * @since 2.2.15 + */ + public function select($database) + { + return false; + } + + /** + * Gets the database engine of the given table + * + * @param string $table The table for which to retrieve the engine type + * @return string|bool + * @since 2.2.15 + **/ + public function getEngine($table) + { + return 'VDBE'; + } + + /** + * Set the database engine of the given table + * + * This is unsuported by SQLite + * + * @param string $table The table for which to retrieve the engine type + * @param string $engine The engine type to set + * @return bool + * @since 2.2.15 + **/ + public function setEngine($table, $engine) + { + return false; + } + + /** + * Gets the database character set of the given table + * + * @param string $table The table for which to retrieve the character set + * @param string $field The field to check (optional) + * @return string|bool + * @since 2.2.15 + **/ + public function getCharacterSet($table, $field = null) + { + // SQLite only supports encodings for an entire database + // Not encodings on a per-table or per-column level. + $this->setQuery('pragma encoding;'); + + return $this->loadResult(); + } + + /** + * Gets the version of the database connector + * + * @return string + * @since 2.0.0 + */ + public function getVersion() + { + $this->setQuery('SELECT sqlite_version()'); + + return $this->loadResult(); + } +} diff --git a/core/libraries/Hubzero/Database/Exception/ConnectionFailedException.php b/core/libraries/Hubzero/Database/Exception/ConnectionFailedException.php new file mode 100644 index 00000000000..bc2b2eb95f5 --- /dev/null +++ b/core/libraries/Hubzero/Database/Exception/ConnectionFailedException.php @@ -0,0 +1,12 @@ +useFiles($path . DS . self::FILENAME, 'info', "%datetime% %message%\n", "Y-m-d\TH:i:s.uP", 0640); + $logger->info($statement); + } + + /** + * Parses the debug backtrace for the applicable file and line + * + * @return array + * @since 2.0.0 + **/ + public static function parseBacktrace() + { + $file = '-'; + $line = 0; + + // Loop through the backtrace items + foreach (self::getBacktrace() as $item) + { + // Looking for the last instance of one of the following classes... + // this will be our indicator of the command that originated this query + if (isset($item['class']) + && ($item['class'] == 'Hubzero\Database\Relational' + || $item['class'] == 'Hubzero\Database\Relationship\Relationship' + || $item['class'] == 'Hubzero\Database\Rows')) + { + + $file = (isset($item['file'])) ? str_replace(PATH_CORE, '', $item['file']) : $file; + $line = (isset($item['line'])) ? $item['line'] : $line; + } + } + + return array($file, $line); + } + + /** + * Gets the debug backtrace from php + * + * @return array + * @since 2.0.0 + **/ + public static function getBacktrace() + { + return debug_backtrace(); + } +} diff --git a/core/libraries/Hubzero/Database/Nested.php b/core/libraries/Hubzero/Database/Nested.php new file mode 100644 index 00000000000..bd1be277339 --- /dev/null +++ b/core/libraries/Hubzero/Database/Nested.php @@ -0,0 +1,296 @@ +getQuery(); + + $query->update($this->getTableName()); + $query->set([ + $pos => new Value\Raw($pos . ($add ? '+' : '-') . '2'), + ]); + $query->where($pos, '>=', $base) + ->where('id', '!=', $this->id) + ->execute(); + + return $this; + } + + /** + * Resolves the trailing left and right values for the new model + * + * @param int $base The base level after which values should be changed + * @return $this + * @since 2.1.0 + **/ + private function resolveTrailing($base, $add = true) + { + return $this->updateTrailing('lft', $base, $add) + ->updateTrailing('rgt', $base, $add); + } + + /** + * Establishes the model as a proper object as needed + * + * @param object|int $model The model to resolve + * @return $this + * @since 2.1.0 + **/ + private function establishIsModel(&$model) + { + // Turn model into an object if need be + if (!is_object($model)) + { + $model = static::oneOrFail((int) $model); + } + + return $this; + } + + /** + * Sets the default scopes on the model + * + * @param object|int $parent The parent of the child being created + * @return $this + * @since 2.1.0 + **/ + private function establishBaseParametersFromParent($parent) + { + $this->set('parent_id', $parent->id); + $this->set('level', $parent->level + 1); + + return $this->applyScopes($parent); + } + + /** + * Applies the scopes of the given model to the current + * + * @param object|int $parent The parent from which to inherit + * @param string $method The way in which scopes are applied + * @return $this + * @since 2.1.0 + **/ + private function applyScopes($parent, $method = 'set') + { + // Inherit scopes from parent + foreach ($this->scopes as $scope) + { + $this->$method($scope, $parent->$scope); + } + + return $this; + } + + /** + * Applies the scopes of the given model to the current pending query + * + * @param object|int $parent The parent from which to inherit + * @return $this + * @since 2.1.0 + **/ + private function applyScopesWhere($parent) + { + return $this->applyScopes($parent, 'whereEquals'); + } + + /** + * Saves the current model to the database as the nth child of the given parent + * + * @param object|int $parent The parent of the child being created + * @return bool + * @since 2.1.0 + **/ + public function saveAsChildOf($parent) + { + $this->establishIsModel($parent) + ->establishBaseParametersFromParent($parent); + + // Compute the location where the item should reside + $this->set('lft', $parent->rgt); + $this->set('rgt', $parent->rgt + 1); + + // Save + if (!$this->save()) + { + return false; + } + + // Reposition new values of displaced items + $this->resolveTrailing($parent->rgt); + + return true; + } + + /** + * Saves the current model to the database as the first child of the given parent + * + * @param object|int $parent The parent of the child being created + * @return bool + * @since 2.1.0 + **/ + public function saveAsFirstChildOf($parent) + { + $this->establishIsModel($parent) + ->establishBaseParametersFromParent($parent); + + // Compute the location where the item should reside + $this->set('lft', $parent->lft + 1); + $this->set('rgt', $parent->lft + 2); + + // Save + if (!$this->save()) + { + return false; + } + + // Reposition new values of displaced items + $this->resolveTrailing($parent->lft + 1); + + return true; + } + + /** + * Saves the current model to the database as the last child of the given parent + * + * @param object|int $parent The parent of the child being created + * @return bool + * @since 2.1.0 + **/ + public function saveAsLastChildOf($parent) + { + return $this->saveAsChildOf($parent); + } + + /** + * Saves a new root node element + * + * @return bool + * @since 2.1.0 + **/ + public function saveAsRoot() + { + // Compute the location where the item should reside + $this->set('parent_id', 0); + $this->set('level', 0); + $this->set('lft', 0); + $this->set('rgt', 1); + + // Save + return $this->save(); + } + + /** + * Deletes a model, rearranging subordinate nodes as appropriate + * + * @return bool + * @since 2.1.0 + **/ + public function destroy() + { + if (!parent::destroy()) + { + return false; + } + + foreach ($this->getDescendants() as $descendant) + { + $descendant->destroy(); + + // We have to decrement our internal reference to right here + // so that we ultimately resolve trailing below based on the + // properly updated value, otherwise anything upstream of + // what we're destroying won't be properly updated + $this->rgt -= 2; + } + + // Reposition new values of displaced items + $this->resolveTrailing($this->rgt, false); + + return true; + } + + /** + * Establishes the query for the immediate children of the current model + * + * @return array + * @since 2.1.0 + **/ + public function children() + { + return $this->descendants(1); + } + + /** + * Grabs the immediate children of the current model + * + * @return array + * @since 2.1.0 + **/ + public function getChildren() + { + return $this->children()->rows(); + } + + /** + * Establishes the query for all of the descendants of the current model + * + * @param int $level The level to limit to + * @return array + * @since 2.1.0 + **/ + public function descendants($level = null) + { + $instance = self::blank(); + $instance->where('level', '>', $this->level) + ->order('lft', 'asc'); + + if (isset($level)) + { + $instance->where('level', '<=', $this->level + $level); + } + + return $instance->where('lft', '>', $this->lft) + ->where('rgt', '<', $this->rgt) + ->applyScopesWhere($this); + } + + /** + * Grabs all of the descendants of the current model + * + * @param int $level The level to limit to + * @return array + * @since 2.1.0 + **/ + public function getDescendants($level = null) + { + return $this->descendants($level)->rows(); + } +} diff --git a/core/libraries/Hubzero/Database/Pagination.php b/core/libraries/Hubzero/Database/Pagination.php new file mode 100644 index 00000000000..f5b89754812 --- /dev/null +++ b/core/libraries/Hubzero/Database/Pagination.php @@ -0,0 +1,126 @@ +getPaginator()))) + { + $result = call_user_func_array(array($this->getPaginator(), $name), $arguments); + + // If we got back something other than the class itself, return it + if (!($result instanceof \Hubzero\Pagination\Paginator)) + { + return $result; + } + } + + return $this; + } + + /** + * Initializes pagination object + * + * @param string $namespace The session state variable namespace + * @param int $total Total number of records + * @param string $start The variable name representing the pagination start number + * @param string $limit The variable name representing the pagination limit number + * @return object + * @since 2.0.0 + **/ + public static function init($namespace, $total, $start = 'start', $limit = 'limit') + { + $instance = new self; + + $instance->total = $total; + $instance->start = \Request::getInt( + $start, + \User::getState($namespace . '.start', 0) + ); + $instance->limit = \Request::getInt( + $limit, + \User::getState($namespace . '.limit', \Config::get('list_limit')) + ); + + $instance->start = ($instance->limit != 0 ? (floor($instance->start / $instance->limit) * $instance->limit) : 0); + + \User::setState($namespace . '.start', $instance->start); + \User::setState($namespace . '.limit', $instance->limit); + + return $instance; + } + + /** + * Returns the html pagination output + * + * @return string + * @since 2.0.0 + **/ + public function __toString() + { + return $this->getPaginator()->render(); + } + + /** + * Gets the HUBzero paginator, or creates a new one + * + * @return \Hubzero\Pagination\Paginator + * @since 2.0.0 + **/ + protected function getPaginator() + { + if (!isset($this->paginator)) + { + $this->paginator = new \Hubzero\Pagination\Paginator($this->total, $this->start, $this->limit); + } + + return $this->paginator; + } +} diff --git a/core/libraries/Hubzero/Database/Query.php b/core/libraries/Hubzero/Database/Query.php new file mode 100644 index 00000000000..cf2e34cb34c --- /dev/null +++ b/core/libraries/Hubzero/Database/Query.php @@ -0,0 +1,908 @@ +connection = $connection ?: App::get('db'); + $this->reset(); + } + + /** + * Clones the query object, including its individual syntax elements + * + * We want to duplicate our syntax elements, as well as the overall query object, + * hence the need for this. Otherwise, PHP would only provide references to the + * syntax elements, which is counter productive in this instance. + * + * @return void + * @since 2.0.0 + **/ + public function __clone() + { + $this->syntax = clone $this->syntax; + } + + /** + * Purges the query cache + * + * @return void + * @since 2.0.0 + **/ + public static function purgeCache() + { + self::$cache = array(); + } + + /** + * Empties a query clause of current values + * + * @param string $clause [select, update, insert, delete, from, join, set, values, where, group, having, order] + * @return $this + * @since 2.2.15 + **/ + public function clear($clause = '') + { + if (!$clause) + { + $this->reset(); + } + else + { + $clause = 'reset' . ucfirst(strtolower($clause)); + + $this->syntax->$clause(); + } + + return $this; + } + + /** + * Empties a query of current select values + * + * @return $this + * @since 2.2.2 + **/ + public function deselect() + { + $this->syntax->resetSelect(); + return $this; + } + + /** + * Applies a select field to the pending query + * + * @param string $column The column to select + * @param string $as What to call the return val + * @param bool $count Whether or not to count column + * @return $this + * @since 2.0.0 + **/ + public function select($column, $as = null, $count = false) + { + $this->syntax->setSelect($column, $as, $count); + $this->type = 'select'; + return $this; + } + + /** + * Applies an insert statement to the pending query + * + * @param string $table The table into which we will be inserting + * @param bool $ignore Whether or not to ignore errors produced related to things like duplicate keys + * @return $this + * @since 2.0.0 + **/ + public function insert($table, $ignore = false) + { + $this->syntax->setInsert($table, $ignore); + $this->type = 'insert'; + return $this; + } + + /** + * Applies an update statement to the pending query + * + * @param string $table The table whose fields will be updated + * @return $this + * @since 2.0.0 + **/ + public function update($table) + { + $this->syntax->setUpdate($table); + $this->type = 'update'; + return $this; + } + + /** + * Applies a delete statement to the pending query + * + * @param string $table The table whose row will be deleted + * @return $this + * @since 2.0.0 + **/ + public function delete($table) + { + $this->syntax->setDelete($table); + $this->type = 'delete'; + return $this; + } + + /** + * Defines the table from which data should be retrieved + * + * @param string $table The table of interest + * @param string $as What to call the table + * @return $this + **/ + public function from($table, $as = null) + { + $this->syntax->setFrom($table, $as); + return $this; + } + + /** + * Defines a table join to be performed for the query + * + * @param string $table The table join + * @param string $leftKey The left side of the join condition + * @param string $rightKey The right side of the join condition + * @param string $type The join type to perform + * @return $this + **/ + public function join($table, $leftKey, $rightKey, $type = 'inner') + { + $this->syntax->setJoin($table, $leftKey, $rightKey, $type); + return $this; + } + + /** + * Defines a table join to be performed for the query using a raw expression + * + * @param string $table The table join + * @param string $raw The join clause (anything after the ON keyword) + * @param string $type The join type to perform + * @return $this + **/ + public function joinRaw($table, $raw, $type = 'inner') + { + $this->syntax->setRawJoin($table, $raw, $type); + return $this; + } + + /** + * Defines a table INNER join to be performed for the query + * + * @param string $table The table join + * @param string $leftKey The left side of the join condition + * @param string $rightKey The right side of the join condition + * @return $this + **/ + public function innerJoin($table, $leftKey, $rightKey) + { + $this->syntax->setJoin($table, $leftKey, $rightKey, 'inner'); + return $this; + } + + /** + * Defines a table FULL OUTER join to be performed for the query + * + * @param string $table The table join + * @param string $leftKey The left side of the join condition + * @param string $rightKey The right side of the join condition + * @return $this + **/ + public function fullJoin($table, $leftKey, $rightKey) + { + $this->syntax->setJoin($table, $leftKey, $rightKey, 'full'); + return $this; + } + + /** + * Defines a table LEFT join to be performed for the query + * + * @param string $table The table join + * @param string $leftKey The left side of the join condition + * @param string $rightKey The right side of the join condition + * @return $this + **/ + public function leftJoin($table, $leftKey, $rightKey) + { + $this->syntax->setJoin($table, $leftKey, $rightKey, 'left'); + return $this; + } + + /** + * Defines a table RIGHT join to be performed for the query + * + * @param string $table The table join + * @param string $leftKey The left side of the join condition + * @param string $rightKey The right side of the join condition + * @return $this + **/ + public function rightJoin($table, $leftKey, $rightKey) + { + $this->syntax->setJoin($table, $leftKey, $rightKey, 'right'); + return $this; + } + + /** + * Applies a where clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param string $operator The operation that will compare column to value + * @param string $value The value to which the column will be evaluated + * @param string $logical The operator between multiple clauses + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function where($column, $operator, $value, $logical = 'and', $depth = 0) + { + $this->syntax->setWhere($column, $operator, $value, $logical, $depth); + return $this; + } + + /** + * Applies a where clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param string $operator The operation that will compare column to value + * @param string $value The value to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function orWhere($column, $operator, $value, $depth = 0) + { + $this->where($column, $operator, $value, 'or', $depth); + return $this; + } + + /** + * Applies a simple where equals clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param string $value The value to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function whereEquals($column, $value, $depth = 0) + { + $this->where($column, '=', $value, 'and', $depth); + return $this; + } + + /** + * Applies a simple where equals clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param string $value The value to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function orWhereEquals($column, $value, $depth = 0) + { + $this->where($column, '=', $value, 'or', $depth); + return $this; + } + + /** + * Applies a simple where in clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param array $value The values to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function whereIn($column, $values, $depth = 0) + { + $this->where($column, 'IN', $values, 'and', $depth); + return $this; + } + + /** + * Applies a simple where in clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param array $value The values to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function orWhereIn($column, $values, $depth = 0) + { + $this->where($column, 'IN', $values, 'or', $depth); + return $this; + } + + /** + * Applies a simple where not in clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param array $value The values to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function whereNotIn($column, $values, $depth = 0) + { + $this->where($column, 'NOT IN', $values, 'and', $depth); + return $this; + } + + /** + * Applies a simple where not in clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param array $value The values to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function orWhereNotIn($column, $values, $depth = 0) + { + $this->where($column, 'NOT IN', $values, 'or', $depth); + return $this; + } + + /** + * Applies a simple where like clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param string $value The value to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.1.0 + **/ + public function whereLike($column, $value, $depth = 0) + { + $this->where($column, 'LIKE', "%{$value}%", 'and', $depth); + return $this; + } + + /** + * Applies a simple where like clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param string $value The value to which the column will be evaluated + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.1.0 + **/ + public function orWhereLike($column, $value, $depth = 0) + { + $this->where($column, 'LIKE', "%{$value}%", 'or', $depth); + return $this; + } + + /** + * Applies an AND where is null clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.2.15 + **/ + public function whereIsNull($column, $depth = 0) + { + $this->where($column, 'IS', null, 'and', $depth); + return $this; + } + + /** + * Applies a OR where is null clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.2.15 + **/ + public function orWhereIsNull($column, $depth = 0) + { + $this->where($column, 'IS', null, 'or', $depth); + return $this; + } + + /** + * Applies an AND where is not null clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.2.15 + **/ + public function whereIsNotNull($column, $depth = 0) + { + $this->where($column, 'IS NOT', null, 'and', $depth); + return $this; + } + + /** + * Applies a OR where is not null clause to the pending query + * + * @param string $column The column to which the clause will apply + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.2.15 + **/ + public function orWhereIsNotNull($column, $depth = 0) + { + $this->where($column, 'IS NOT', null, 'or', $depth); + return $this; + } + + /** + * Applies a raw where clause to the pending query + * + * @param string $string The raw where clause + * @param array $bindings Any bindings to apply to the where clause + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function whereRaw($string, $bindings = [], $depth = 0) + { + $this->syntax->setRawWhere($string, $bindings, 'and', $depth); + return $this; + } + + /** + * Applies a raw where clause to the pending query + * + * @param string $string The raw where clause + * @param array $bindings Any bindings to apply to the where clause + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function orWhereRaw($string, $bindings = [], $depth = 0) + { + $this->syntax->setRawWhere($string, $bindings, 'or', $depth); + return $this; + } + + /** + * Resets the depth of a nested statement back down to a given level + * + * @param int $depth The depth to set to + * @return $this + * @since 2.1.0 + **/ + public function resetDepth($depth = 0) + { + $this->syntax->resetDepth($depth); + return $this; + } + + /** + * Applies 'order by' clause + * + * @param string $column The column to which the order by will apply + * @param string $dir The direction in which the results will be ordered + * @return $this + * @since 2.0.0 + **/ + public function order($column, $dir) + { + $this->syntax->setOrder($column, $dir); + return $this; + } + + /** + * Removes 'order by' clause + * + * @return $this + * @since 2.0.0 + **/ + public function unorder() + { + $this->syntax->resetOrder(); + return $this; + } + + /** + * Sets query offset to start at a certain position + * + * @param int $start Position to start from + * @return $this + * @since 2.0.0 + **/ + public function start($start) + { + $this->syntax->setStart((int)$start); + return $this; + } + + /** + * Limits query results returned to a certain number + * + * @param int $limit Number of results to return on next query + * @return $this + * @since 2.0.0 + **/ + public function limit($limit) + { + $this->syntax->setLimit((int)$limit); + return $this; + } + + /** + * Sets the values to be inserted into the database + * + * @param array $data The data to be inserted + * @return $this + * @since 2.0.0 + **/ + public function values($data) + { + $this->syntax->setValues($data); + return $this; + } + + /** + * Sets the values to be modified in the database + * + * @param array $data The data to be modified + * @return $this + * @since 2.0.0 + **/ + public function set($data) + { + $this->syntax->setSet($data); + return $this; + } + + /** + * Sets the group by element on the query + * + * @param string $column The column on which to apply the group by + * @return $this + * @since 2.0.0 + **/ + public function group($column) + { + $this->syntax->setGroup($column); + return $this; + } + + /** + * Sets the having element on the query + * + * @param string $column The column to which the clause will apply + * @param string $operator The operation that will compare column to value + * @param string $value The value to which the column will be evaluated + * @return $this + * @since 2.0.0 + **/ + public function having($column, $operator, $value) + { + $this->syntax->setHaving($column, $operator, $value); + return $this; + } + + /** + * Retrieves all applicable data + * + * @FIXME: this could result in slightly odd behavior if you call the same query + * twice, but for some reason want differing structures of the returned data. + * + * @param string $structure The structure of the item(s) returned (if applicable) + * @param bool $noCache Whether or not to check cache for results + * @return $this + * @since 2.0.0 + **/ + public function fetch($structure = 'rows', $noCache = false) + { + // Build and hash query + $query = $this->buildQuery(); + $key = hash('md5', $structure . $query . serialize($this->syntax->getBindings())); + + // Check cache for results first + if ($noCache || !isset(self::$cache[$key])) + { + self::$cache[$key] = $this->query($query, $structure); + } + + // Clear elements + $this->reset(); + + return self::$cache[$key]; + } + + /** + * Inserts a new row using data provided into given table + * + * @param string $table The table name into which the data should be inserted + * @param array $data An associative array of data to insert + * @param bool $ignore Whether or not to perform an insert ignore + * @return bool|int + * @since 2.0.0 + **/ + public function push($table, $data, $ignore = false) + { + // Add insert statement + $this->insert($table, $ignore) + ->values($data); + + $result = $this->execute(); + + // Return the inserted data + return !$result ?: $this->connection->insertid(); + } + + /** + * Updates an existing item in the database using the provided data + * + * @param string $table The table to update + * @param string $pkField The table field serving as primary key + * @param mixed $pkValue The primary key value + * @param array $data The data to update in the database + * @return bool + * @since 2.0.0 + **/ + public function alter($table, $pkField, $pkValue, $data) + { + // Add insert statement + $this->update($table) + ->set($data); + + // Where primary key is... + $this->whereEquals($pkField, $pkValue); + + // Return the result of the query + return $this->execute(); + } + + /** + * Removes a record by its primary key + * + * @param string $table The table to update + * @param string $pkField The table field serving as primary key + * @param mixed $pkValue The primary key value + * @return bool + * @since 2.0.0 + **/ + public function remove($table, $pkField, $pkValue) + { + // Make sure we have an id (i.e. don't delete everything in the table!) + if (is_null($pkValue) || empty($pkValue)) + { + return false; + } + + // Add delete statement + $this->delete($table) + ->whereEquals($pkField, $pkValue); + + // Return result of the query + return $this->execute(); + } + + /** + * Builds and executes the current query based on the elements present + * + * This is a fairly 'dumb' function, in that it just looks for whichever type was + * most recently set by one of the primary functions (select, insert, update, delete). + * Fetch should still be used for select queries as it offers result caching. + * + * @FIXME: maybe this should be combined with fetch? + * + * @return mixed + * @since 2.0.0 + **/ + public function execute() + { + $result = $this->query($this->buildQuery($this->type)); + + // Clear elements + $this->reset(); + + // Return result of the query + return $result; + } + + /** + * Performs the actual query and returns the results + * + * @param string $query The query to perform + * @param string $structure The structure of the item(s) returned (if applicable) + * @return mixed + * @since 2.0.0 + **/ + public function query($query, $structure = null) + { + // Check the type of query to decide what to return + list($type) = explode(' ', $query, 2); + $type = strtolower($type); + + // Default structure if needed + if ($type == 'select' && is_null($structure)) + { + $structure = 'rows'; + } + + $this->connection->prepare($query)->bind($this->syntax->getBindings()); + + $result = (isset($structure)) + ? $this->connection->{constant('self::' . strtoupper($structure))}() + : $this->connection->query(); + + return $result; + } + + /** + * Retrieves the current query as a string (without executing it) + * + * @return string + * @since 2.0.0 + **/ + public function toString() + { + return $this->connection + ->prepare($this->buildQuery($this->type)) + ->bind($this->syntax->getBindings()) + ->toString(); + } + + /** + * Retrieves the current query as a string (without executing it) + * + * @return string + * @since 2.1.9 + **/ + public function __toString() + { + return $this->toString(); + } + + /** + * Builds query based on the current query elements established + * + * @param string $type The type of query to build + * @return string + * @since 2.0.0 + **/ + private function buildQuery($type = 'select') + { + $pieces = array(); + + // Loop through query elements + foreach ($this->$type as $piece) + { + // If we have one of these elements, get its string value + if ($element = $this->syntax->build($piece)) + { + $pieces[] = $element; + } + } + + return implode("\n", $pieces); + } + + /** + * Resets the query elements + * + * @return void + * @since 2.0.0 + **/ + private function reset() + { + // Reset the syntax element + $syntax = '\\Hubzero\\Database\\Syntax\\' . ucfirst($this->connection->getSyntax()); + $this->syntax = new $syntax($this->connection); + } +} diff --git a/core/libraries/Hubzero/Database/Relational.php b/core/libraries/Hubzero/Database/Relational.php new file mode 100644 index 00000000000..47e6f7b1a96 --- /dev/null +++ b/core/libraries/Hubzero/Database/Relational.php @@ -0,0 +1,2467 @@ +modelName = $r->getShortName(); + $this->modelNamespace = $r->getNamespaceName(); + + // If table name isn't explicitly set, build it + $namespace = (!$this->namespace ? '' : $this->namespace . '_'); + $plural = \Hubzero\Utility\Inflector::pluralize(strtolower($this->getModelName())); + $this->table = $this->table ?: '#__' . $namespace . $plural; + + // Set up connection and query object + $this->newQuery(); + + // Store methods for later + // + // Here we store the methods per class name. This allows for quicker + // lookup and less memory usage when dealing with multiple classes + // of the same type (i.e., a listing of records). + $key = $r->getName(); + if (!isset(self::$classMethods[$key])) + { + self::$classMethods[$key] = get_class_methods($this); + + $this->methods = self::$classMethods[$key]; + } + $this->methods = self::$classMethods[$key]; + + // Run extra setup. This is so subclasses don't have to overwrite + // the constructor and then call parent::__construct(). + // They can instead just add a setup() method. + $this->setup(); + } + + /** + * Processes calls to inaccessible or undefined instance methods + * + * @param string $name The method name being called + * @param array $arguments The method arguments provided + * @return mixed + * @throws \Hubzero\Error\Exception\BadMethodCallException If called method does not exist in + * this class or the query class, or + * as a helper* method on the current class. + * @since 2.0.0 + **/ + public function __call($name, $arguments) + { + // See if method is available as a helper method on current class + if ($this->hasHelper($name)) + { + return $this->callHelper($name, $arguments); + } + + // See if method is available as a transformer on current class + if ($this->hasTransformer($name)) + { + return $this->callTransformer($name, $arguments); + } + + // Check if it is a parsable field (i.e. wiki/html) + if ($this->isParsable($name)) + { + return $this->parse($name, (isset($arguments[0])) ? $arguments[0] : 'parsed'); + } + + // See if we need to call a query method + if (in_array($name, get_class_methods($this->query))) + { + // @FIXME: hack to fully qualify field names in one location...is there a better way/location? + if ((substr($name, 0, 5) == 'where' || substr($name, 0, 7) == 'orWhere') && $name != 'whereRaw' && $name != 'orWhereRaw') + { + $arguments[0] = (strpos($arguments[0], '.') === false) + ? $this->getQualifiedFieldName($arguments[0]) + : $arguments[0]; + } + + // Call method and get type of response + $result = call_user_func_array(array($this->query, $name), $arguments); + $class = __NAMESPACE__ . '\\Query'; + // We never want to return an instance of the query class, because + // we want to be able to chain methods together that are on the model + // itself. Plus we auto-forward calls to query functions, so they'll + // get there eventually anyway. + return ($result instanceof $class) ? $this : $result; + } + + // Finally, check for a dynamic relationship definition + if (array_key_exists($name, self::$acquaintances)) + { + return call_user_func_array(self::$acquaintances[$name], [$this]); + } + + // This method doesn't exist + throw new BadMethodCallException("'{$name}' method does not exist.", 500); + } + + /** + * Processes calls to inaccessible or undefined static methods + * + * This is here primarily so we can statically call query class + * methods directly on a newly created object + * For example: Model::whereEquals('field', 'yes'); + * + * @param string $name The method name being called + * @param array $arguments The method arguments provided + * @return mixed + * @since 2.0.0 + **/ + public static function __callStatic($name, $arguments) + { + return call_user_func_array(array(new static, $name), $arguments); + } + + /** + * Gets attributes set on model dynmically + * + * @param string $name The name of the var to retrieve + * @return mixed + * @since 2.0.0 + **/ + public function __get($name) + { + // First, see if a transformer is available on the model + if ($this->hasTransformer($name)) + { + return $this->callTransformer($name); + } + + // Check if it is a parsable field (i.e. wiki/html) + if ($this->isParsable($name)) + { + return $this->parse($name); + } + + // Next check for an attribute on the model + if (isset($this->attributes[$name])) + { + return $this->attributes[$name]; + } + + // Check forwarding + if (!empty($this->forwards)) + { + foreach ($this->forwards as $forward) + { + // We take the first one we find, so in theory, if multiple forwards exist with + // the same name, you'd have to prioritize them somehow. + if ($var = $this->makeRelationship($forward)->getRelationship($forward)->$name) + { + return $var; + } + } + } + + // Now, we'll assume we're looking for a relationship + if (in_array($name, $this->methods)) + { + return $this->makeRelationship($name)->getRelationship($name); + } + + // Finally, check for a dynamic relationship definition + if (array_key_exists($name, self::$acquaintances)) + { + return $this->makeAcquaintance($name)->getRelationship($name); + } + } + + /** + * Sets attributes (i.e. fields) on the model + * + * @param array|string $key The key to set, or array of key/value pairs + * @param mixed $value The value to set if key is string + * @return $this + */ + public function __set($key, $value) + { + return $this->set($key, $value); + } + + /** + * Intercepts calls to copy the object so we can make a true clone of the attached query + * + * PHP, when cloning, does a shallow copy, hence the need for this intercept. + * + * @return void + * @since 2.0.0 + **/ + public function __clone() + { + $this->query = clone $this->query; + } + + /** + * Serializes the model data for storage + * + * @return string + * @since 2.1.0 + **/ + public function serialize() + { + return serialize($this->getAttributes()); + } + + /** + * Unserializes the data into a new model + * + * @param string $data The data to build from + * @return void + * @since 2.1.0 + **/ + public function unserialize($data) + { + $this->__construct(); + $this->set(unserialize($data)); + } + + /** + * Runs extra setup code when creating a new model + * + * @return void + * @since 2.0.0 + **/ + public function setup() + { + // Overload in subclass to do something here...nothing by default + } + + /** + * Sets the database connection to be used by the query builder + * + * @param object $connection The connection to set + * @return void + * @since 2.0.0 + **/ + public static function setDefaultConnection($connection) + { + self::$connection = $connection; + } + + /** + * Disables query caching + * + * @return $this + * @since 2.0.0 + **/ + public function disableCaching() + { + $this->noCache = true; + + return $this; + } + + /** + * Enables query caching + * + * @return $this + * @since 2.0.0 + **/ + public function enableCaching() + { + $this->noCache = false; + + return $this; + } + + /** + * Purges the query cache + * + * @return $this + * @since 2.0.0 + **/ + public function purgeCache() + { + $query = $this->query; + $query::purgeCache(); + + return $this; + } + + /** + * Gets an attribute by key + * + * This will not retrieve properties directly attached to the model, + * even if they are public - those should be accessed directly! + * + * Also, make sure to access properties in transformers using the get method. + * Otherwise you'll just get stuck in a loop! + * + * @param string $key The attribute key to get + * @param mixed $default The value to provide, should the key be non-existent + * @return mixed + * @since 2.0.0 + **/ + public function get($key, $default = null) + { + return $this->hasAttribute($key) ? $this->attributes[$key] : $default; + } + + /** + * Sets attributes (i.e. fields) on the model + * + * This must be used when setting data to be saved. Otherwise, the properties + * will be attached directly to the model itself and not included in the save. + * + * @param array|string $key The key to set, or array of key/value pairs + * @param mixed $value The value to set if key is string + * @return $this + * @since 2.0.0 + **/ + public function set($key, $value = null) + { + if (is_array($key) || is_object($key)) + { + foreach ($key as $k => $v) + { + $this->attributes[$k] = $v; + } + } + else + { + $this->attributes[$key] = $value; + } + + return $this; + } + + /** + * Returns a new empty model + * + * @return static + * @since 2.0.0 + **/ + public static function blank() + { + return new static; + } + + /** + * Construct a new object instance, setting the passed in results on the object + * + * @param object $results The results to set on the new model + * @return static + * @since 2.0.0 + **/ + public static function newFromResults($results) + { + $instance = self::blank(); + $instance->set($results); + + return $instance; + } + + /** + * Copies the current model (likely used to maintain query parameters between multiple queries) + * + * @return $this + * @since 2.0.0 + **/ + public function copy() + { + return clone $this; + } + + /** + * Outputs attributes in JSON encoded format + * + * @return string + * @since 2.0.0 + **/ + public function toJson() + { + return json_encode($this->attributes); + } + + /** + * Outputs attributes as array + * + * @return array + * @since 2.0.0 + **/ + public function toArray() + { + return $this->attributes; + } + + /** + * Outputs attributes as object + * + * @return object + * @since 2.0.0 + **/ + public function toObject() + { + return (object)$this->attributes; + } + + /** + * Checks to see if the current model has a helper by the given name + * + * @param string $name The helper name to check for + * @return bool + * @since 2.0.0 + **/ + public function hasHelper($name) + { + return in_array('helper' . ucfirst($name), $this->methods); + } + + /** + * Calls the requested helper, passing the given arguments + * + * @param string $name The helper name to call + * @param array $arguments Arguments to pass with the method call + * @return mixed + * @since 2.0.0 + **/ + public function callHelper($name, $arguments) + { + return call_user_func_array(array($this, 'helper' . ucfirst($name)), $arguments); + } + + /** + * Checks to see if the current model has a transformer by the given name + * + * @param string $name The transformer name to check for + * @return bool + * @since 2.0.0 + **/ + public function hasTransformer($name) + { + return in_array('transform' . ucfirst($this->snakeToCamel($name)), $this->methods); + } + + /** + * Calls the requested transformer, passing the given arguments + * + * @param string $name The transformer name to call + * @param array $arguments Arguments to pass with the method call + * @return mixed + * @since 2.0.0 + **/ + public function callTransformer($name, $arguments = []) + { + return call_user_func_array(array($this, 'transform' . ucfirst($this->snakeToCamel($name))), $arguments); + } + + /** + * Checks to see if the given field is one to be parsed + * + * @param string $field The field to check + * @return bool + * @since 2.0.0 + **/ + public function isParsable($field) + { + return in_array($field, $this->parsed); + } + + /** + * Parses content string as directed + * + * @param string $field The field to parse + * @param string $as The format to return state in + * @return string + * @since 2.0.0 + **/ + public function parse($field, $as = 'parsed') + { + switch (strtolower($as)) + { + case 'parsed': + $property = "_{$field}Parsed"; + + if (!isset($this->$property)) + { + $this->$property = \Hubzero\Html\Builder\Content::prepare($this->get($field, '')); + } + + return $this->$property; + break; + + case 'raw': + default: + $content = stripslashes($this->get($field, '')); + return preg_replace('/^()/i', '', $content); + break; + } + } + + /** + * Takes a snake-cased string and camel cases it + * + * @param string $text The string to camel case + * @return string + * @since 2.0.0 + **/ + public function snakeToCamel($text) + { + if (strpos($text, '_') !== false) + { + $bits = explode('_', $text); + $bits = array_map('ucfirst', $bits); + $text = lcfirst(implode('', $bits)); + } + + return $text; + } + + /** + * Resets the current model, likely for another query to be performed on it + * + * @return $this + * @since 2.0.0 + **/ + private function reset() + { + $this->clearAttributes(); + $this->newQuery(); + return $this; + } + + /** + * Gets a fresh query object + * + * @return \Hubzero\Database\Query + * @since 2.0.0 + **/ + public function getQuery() + { + return new Query(self::$connection); + } + + /** + * Gets a fresh structure object + * + * @return \Hubzero\Database\Structure + * @since 2.0.0 + **/ + public function getStructure() + { + return new Structure(self::$connection); + } + + /** + * Sets a fresh query object on the model, seeding it with helpful defaults + * + * @return $this + * @since 2.0.0 + **/ + public function newQuery() + { + $select = ($this->getTableAlias() ? $this->getTableAlias() . '.' : '') . '*'; + + $this->query = $this->getQuery()->select($select)->from($this->getTableName(), $this->getTableAlias()); + return $this; + } + + /** + * Checks to see if the requested attribute is set on the model + * + * @return bool + * @since 2.0.0 + **/ + public function hasAttribute($key) + { + return isset($this->attributes[$key]); + } + + /** + * Grabs all of the model attributes + * + * @return array + * @since 2.0.0 + **/ + public function getAttributes() + { + return $this->attributes; + } + + /** + * Removes an attribute + * + * @param string $key The attribute to remove + * @return $this + * @since 2.0.0 + **/ + public function removeAttribute($key) + { + $this->offsetUnset($key); + + return $this; + } + + /** + * Clears data attributes set on the current model + * + * @return void + * @since 2.0.0 + **/ + private function clearAttributes() + { + $this->attributes = array(); + } + + /** + * Determines if the current model is new by looking for the presence of a primary key attribute + * + * @return bool + * @since 2.0.0 + **/ + public function isNew() + { + return (!$this->hasAttribute($this->getPrimaryKey()) || !$this->{$this->getPrimaryKey()}); + } + + /** + * Sets an interator parent on the model + * + * @param \Hubzero\Database\Rows $rows The iterator to set + * @return $this + * @since 2.1.0 + **/ + public function setIterator($rows) + { + $this->collection = $rows; + + return $this; + } + + /** + * Checks to see if the current item is the first in the list + * + * @return bool + * @since 2.1.0 + **/ + public function isFirst() + { + if ($this->collection) + { + return $this->collection->isFirst($this->getPkValue()); + } + + return false; + } + + /** + * Checks to see if the current item is the last in the list + * + * @return bool + * @since 2.1.0 + **/ + public function isLast() + { + if ($this->collection) + { + return $this->collection->isLast($this->getPkValue()); + } + + return false; + } + + /** + * Retrieves the current model's table name + * + * @return string + * @since 2.0.0 + **/ + public function getTableName() + { + return $this->table; + } + + /** + * Retrieves the current model's table alias + * + * @return string + **/ + public function getTableAlias() + { + return $this->tableAlias; + } + + /** + * Sets the current model's table alias + * + * @param string $alias + * @return object + **/ + public function setTableAlias($alias) + { + $this->tableAlias = (string) $alias; + + return $this; + } + + /** + * Retrieves the current model's primary key name + * + * @return string + * @since 2.0.0 + **/ + public function getPrimaryKey() + { + return $this->pk; + } + + /** + * Gets the value of the primary key + * + * @return mixed + * @since 2.0.0 + **/ + public function getPkValue() + { + return isset($this->attributes[$this->getPrimaryKey()]) ? $this->attributes[$this->getPrimaryKey()] : null; + } + + /** + * Creates the fully qualified field name by prepending the table name + * + * @return string + * @since 2.0.0 + **/ + public function getQualifiedFieldName($field) + { + $tbl = ($this->getTableAlias() ? $this->getTableAlias() : $this->getTableName()); + return $tbl . '.' . $field; + } + + /** + * Retrieves the model's name + * + * @return string + * @since 2.0.0 + **/ + public function getModelName() + { + return $this->modelName; + } + + /** + * Retrieves the model's name + * + * @return string + * @since 2.1.0 + **/ + public function getModelNamespace() + { + return $this->modelNamespace; + } + + /** + * Retrieves the model's namespace + * + * @return string + * @since 2.0.0 + **/ + public function getNamespace() + { + return $this->namespace; + } + + /** + * Retrieves the model rules + * + * @return array + * @since 2.0.0 + **/ + public function getRules() + { + return $this->rules; + } + + /** + * Adds a new rule to the validation set + * + * @param string $key The field to which the rule applies + * @param mixed $rule The rule to add + * @return $this + * @since 2.0.0 + **/ + public function addRule($key, $rule) + { + $this->rules[$key] = $rule; + + return $this; + } + + /** + * Get total number of rows + * + * @return int + * @since 2.0.0 + **/ + public function total() + { + // Note that we do not need to parse includes at this stage, as includes do not effect + // the primary result set, and thus do not effect the count. whereRelated() could effect + // the count, but that method is not currently in use. + // + // We also reset the 'select' clause to avoid pulling unnecessary records and reset + // the 'order by' clause to avoid referenced fields in the aforementioned 'select' clauses + // that mgiht have been removed. Neither of these should have any effect on a count. + $first = $this->deselect() + ->select($this->getQualifiedFieldName($this->getPrimaryKey()), 'count', true) + ->unordered() + ->rows(false) + ->first(); + //->count; + + $total = $first ? (int)$first->count : 0; + + $this->reset(); + + return $total; + } + + /** + * Counts rows, fetching them first + * + * The {@link \Hubzero\Database\Rows} class also has a count method, which is used + * to count rows after they've already been fetched. + * + * If possible, you shouldn't use this method. We have to make a clone of the current + * query so that it won't be empty if you later try to fetch the results of the original + * query. It would be better to go ahead and fetch the results and call the count + * method on the rows object, thus potentially saving a query if you later plan + * to fetch the original rows that you were trying to count. + * + * @return int + * @since 2.0.0 + **/ + public function count() + { + return $this->copy()->rows()->count(); + } + + /** + * Gets the results of the established query + * + * @param bool $parseIncludes Whether or not to parse the includes + * @return \Hubzero\Database\Rows + * @since 2.0.0 + **/ + public function rows($parseIncludes = true) + { + // Fetch the results + $rows = $this->rowsFromRaw($this->query->fetch('rows', $this->noCache)); + + if ($parseIncludes) + { + $rows = $this->parseIncluding($rows); + } + + // Set a few things on the rows object that might be helpful + $rows->pagination = $this->pagination; + $rows->orderBy = $this->orderBy; + $rows->orderDir = $this->orderDir; + return $rows; + } + + /** + * Gets the first/only row from the established query + * + * Not quite the same as rows, in that we're assuming an intentional + * call to only get one row wouldn't want any pagination info included. + * + * @return \Hubzero\Database\Relational|static + * @since 2.0.0 + **/ + public function row() + { + $row = $this->query->fetch('row'); + + return ($row) ? self::newFromResults($row) : self::blank(); + } + + /** + * Sets the results of the query on new models and returns a Rows collection + * + * @param array $data The data to set on the model + * @return \Hubzero\Database\Rows + * @since 2.0.0 + **/ + public function rowsFromRaw($data) + { + $rows = new Rows; + + if ($data && count($data) > 0) + { + foreach ($data as $row) + { + $rows->push(self::newFromResults($row)); + } + } + + return $rows; + } + + /** + * Triggers when attempting to iterator over the object, so we know to fetch results + * + * We go ahead and use a copy, that way future calls to the same model will + * continue to have the initial query elements set in place + * + * @return \Hubzero\Database\Rows + * @since 2.0.0 + **/ + public function getIterator() + { + return $this->copy()->rows(); + } + + /** + * Sets the atrributes key with value + * + * @param array|string $key The key to set, or array of key/value pairs + * @param mixed $value The value to set if key is string + * @return void + * @since 2.0.0 + **/ + public function offsetSet($key, $value) + { + if (is_array($key) || is_object($key)) + { + foreach ($key as $k => $v) + { + $this->attributes[$k] = $v; + } + } + else + { + $this->attributes[$key] = $value; + } + } + + /** + * Checks to see if the requested attribute is set on the model + * + * @param string $key The offset to check for + * @return bool + * @since 2.0.0 + **/ + public function offsetExists($key) + { + return $this->hasAttribute($key); + } + + /** + * Unsets the requested attribute from the model + * + * @param string $key The offset to remove + * @return void + * @since 2.0.0 + **/ + public function offsetUnset($key) + { + unset($this->attributes[$key]); + } + + /** + * Gets an attribute by key + * + * @param string $key The attribute key to get + * @return mixed + * @since 2.0.0 + **/ + public function offsetGet($key) + { + return $this->get($key); + } + + /** + * Retrieves one row by primary key value provided + * + * @param mixed $id The primary key field value to use to retrieve one row + * @return \Hubzero\Database\Relational|static + * @since 2.0.0 + **/ + public static function one($id) + { + $instance = self::blank(); + return $instance->whereEquals($instance->getPrimaryKey(), $id)->rows()->seek($id); + } + + /** + * Retrieves one row by primary key, throwing a new exception if not found + * + * @param mixed $id The primary key field value to use to retrieve one row + * @return \Hubzero\Database\Relational|static + * @throws Hubzero\Error\Exception\RuntimeException + * @since 2.0.0 + **/ + public static function oneOrFail($id) + { + $row = self::one($id); + + // Make sure we have a valid row + if ($row === false) + { + throw new RuntimeException("Failed to retrieve a model with a primary key of {$id}", 404); + } + + return $row; + } + + /** + * Retrieves one row by primary key, returning an empty row if not found + * + * @param mixed $id The primary key field value to use to retrieve one row + * @return \Hubzero\Database\Relational|static + * @since 2.0.0 + **/ + public static function oneOrNew($id) + { + $row = self::one($id); + + // See if we have a valid row + if ($row === false) + { + $row = self::blank(); + } + + return $row; + } + + /** + * Retrieves one row loaded by an alias field + * + * @param string $alias The alias to load by + * @return mixed + **/ + public static function oneByAlias($alias) + { + $instance = self::blank(); + return $instance->whereEquals('alias', $alias)->row(); + } + + /** + * Returns all rows (unless otherwise limited) + * + * @param string|array $columns The columns to select + * @return \Hubzero\Database\Relational|static + * @since 2.0.0 + **/ + public static function all($columns = null) + { + return self::blank(); + } + + /** + * Retrieves only the most recent applicable row + * + * This orders results by the limiter, and grabs the first one. + * It by default assumes you want to order by created date. + * + * @param string $limiter The column name to use to determine the latest row + * @return \Hubzero\Database\Relational|static + * @since 2.0.0 + **/ + public function latest($limiter = 'created') + { + return $this->order($limiter, 'desc')->limit(1)->rows()->first(); + } + + /** + * Saves the current model to the database + * + * @return bool + * @since 2.0.0 + **/ + public function save() + { + // Validate + if (!$this->validate()) + { + return false; + } + + // Handle cases where the primary key might be an empty string + // For auto-increment in strict-mode DBs, this needs to be NULL + // instead. + if ($this->hasAttribute($this->getPrimaryKey()) + && !$this->get($this->getPrimaryKey())) + { + $this->set($this->getPrimaryKey(), null); + } + + // See if we're creating or updating + $method = $this->isNew() ? 'create' : 'modify'; + $result = $this->$method(); + + // Only perform the following upon success + if ($result) + { + // Purge cache + $this->purgeCache(); + + // If creating, result is our new id, so set that back on the model + if ($this->isNew()) + { + $this->set($this->getPrimaryKey(), $result); + \Event::trigger($this->getTableName() . '_new', ['model' => $this]); + } + + \Event::trigger('system.onContentSave', array($this->getTableName(), $this)); + } + + return $result; + } + + /** + * Get database table columns + * + * @return array + **/ + public function getTableColumns() + { + static $columns = null; + + if (is_null($columns)) + { + $columns = (array)$this->getStructure()->getTableColumns($this->getTableName(), false); + + if (empty($columns)) + { + throw new Exception(sprintf('Columns not found for table %s', $this->getTableName())); + } + } + + return $columns; + } + + /** + * Filters out fields that are not actually a table column + * + * @return array + **/ + public function getTableColumnsOnly() + { + return array_intersect_key($this->attributes, $this->getTableColumns()); + } + + /** + * Get the defined default value for a database table column + * + * @param string $col The name of the database table column + * @return mixed + **/ + public function getTableColumnDefault($col) + { + $columns = $this->getTableColumns(); + + if (isset($columns[$col])) + { + return $column[$col]['default']; + } + + return null; + } + + /** + * Inserts a new row into the database + * + * @return bool + * @since 2.0.0 + **/ + private function create() + { + // Add any automatic fields + $this->parseAutomatics('initiate'); + + $data = $this->getTableColumnsOnly(); + + return $this->query->push($this->getTableName(), $data); + } + + /** + * Updates an existing item in the database + * + * @return bool + * @since 2.0.0 + **/ + private function modify() + { + // Add any automatic fields + $this->parseAutomatics('renew'); + + $data = $this->getTableColumnsOnly(); + + // Return the result of the query + return $this->query->alter( + $this->getTableName(), + $this->getPrimaryKey(), + $this->getPkValue(), + $data + ); + } + + /** + * Parses for automatically fillable fields + * + * @param string $scope The scope of rules to parse and run + * @return $this + * @since 2.0.0 + **/ + private function parseAutomatics($scope = 'always') + { + $automatics = array_merge($this->$scope, $this->always); + + if (!empty($automatics)) + { + foreach ($automatics as $field) + { + if (strpos($field, '_')) + { + $bits = explode('_', $field); + $bits = array_map('ucfirst', $bits); + $method = implode('', $bits); + } + else + { + $method = ucfirst($field); + } + + $method = 'automatic' . $method; + // Pass the data to the method in case it needs to make use of another field's value + $this->set($field, $this->$method($this->attributes)); + } + } + + return $this; + } + + /** + * Saves the current model and any subsequent attached models + * + * @return bool + * @since 2.0.0 + **/ + public function saveAndPropagate() + { + if (!$this->save()) + { + return false; + } + + // Loop through the relationships and save + // Both rows and models know how to save, so it doesn't matter + // which of the two the particular relationship returned + foreach ($this->getRelationships() as $relationship) + { + if (!$relationship->save()) + { + $this->setErrors($relationship->getErrors()); + return false; + } + } + + return true; + } + + /** + * Deletes the existing/current model + * + * @return bool + * @since 2.0.0 + **/ + public function destroy() + { + // If it has an associated asset entry, try deleting that first + if ($this->hasAttribute('asset_id')) + { + if (!Asset::destroy($this)) + { + return false; + } + } + + \Event::trigger('system.onContentDestroy', array($this->getTableName(), $this)); + + return $this->query->remove( + $this->getTableName(), + $this->getPrimaryKey(), + $this->getPkValue() + ); + } + + /** + * Checks out the current model to the provided user + * + * @param integer $userId Optional userId for whom the row should be checked out + * @return boolean + * @since 2.0.0 + **/ + public function checkout($userId = null) + { + if (!$this->isNew()) + { + $columns = $this->getTableColumns(); + + $data = []; + + if (isset($columns['checked_out_time'])) + { + $now = new Date('now'); + $data['checked_out_time'] = $now->toSql(); + } + + if (isset($columns['checked_out'])) + { + $userId = $userId ?: \User::get('id'); + $data['checked_out'] = $userId; + } + + if (empty($data)) + { + // There is no 'checked_out_time' or 'checked_out' column + return true; + } + + $this->set($data); + + // We build a simple update query as calling save() + // can have unintended consequences when all we want + // is to update two columns + $query = $this->getQuery() + ->update($this->getTableName()) + ->set($data) + ->whereEquals($this->getPrimaryKey(), $this->get($this->getPrimaryKey())); + + // @FIXME: Maybe unnecessary? Database may throw an exception on error + // so this might be pointless. + if (!$query->execute()) + { + $this->addError(__CLASS__ . '::' . __METHOD__ . '() failed'); + return false; + } + } + + return true; + } + + /** + * Checks back in the current model + * + * @return boolean + * @since 2.0.0 + **/ + public function checkin() + { + if (!$this->isNew()) + { + $columns = $this->getTableColumns(); + + $data = []; + $orig = []; + + // We want to get the default values from the + // table's schema, rather than assuming + if (isset($columns['checked_out_time'])) + { + $orig['checked_out_time'] = $this->get('checked_out_time'); + $data['checked_out_time'] = $columns['checked_out_time']['default']; + } + + if (isset($columns['checked_out'])) + { + $orig['checked_out'] = $this->get('checked_out'); + $data['checked_out'] = $columns['checked_out']['default']; + } + + if (empty($data)) + { + // There is no 'checked_out_time' or 'checked_out' column + return true; + } + + $this->set($data); + + // We build a simple update query as calling save() + // can have unintended consequences when all we want + // is to update two columns + $query = $this->getQuery() + ->update($this->getTableName()) + ->set($data) + ->whereEquals($this->getPrimaryKey(), $this->get($this->getPrimaryKey())); + + // @FIXME: Maybe unnecessary? Database may throw an exception on error + // so this might be pointless. + if (!$query->execute()) + { + // Reset to original data + $this->set($orig); + + $this->addError(__CLASS__ . '::' . __METHOD__ . '() failed'); + return false; + } + } + + return true; + } + + /** + * Checks to see if the current model is checked out by someone else + * + * @return bool + * @since 2.0.0 + **/ + public function isCheckedOut() + { + return ($this->get('checked_out') && $this->get('checked_out') != \User::get('id')); + } + + /** + * Selects applicable rows on the relation and limits current query accordingly + * + * NOTE: whereas other 'where' clauses can be called statically due to their + * location in the query builder class, this method cannot be as it is attached + * directly to the model itself. + * + * @param string $relationship The relationship name + * @param closure $constraint The constraint to apply to the related query + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function whereRelatedHas($relationship, $constraint, $depth = 0) + { + $rel = $this->$relationship(); + $keys = $rel->getConstrainedKeys($constraint); + + return $this->where($rel->getLocalKey(), 'IN', $keys, 'and', $depth); + } + + /** + * Selects applicable rows on the relation and limits current query accordingly + * + * NOTE: whereas other 'where' clauses can be called statically due to their + * location in the query builder class, this method cannot be as it is attached + * directly to the model itself. + * + * @param string $relationship The relationship name + * @param closure $constraint The constraint to apply to the related query + * @param int $depth The depth level of the clause, for sub clauses + * @return $this + * @since 2.0.0 + **/ + public function orWhereRelatedHas($relationship, $constraint, $depth = 0) + { + $rel = $this->$relationship(); + $keys = $rel->getConstrainedKeys($constraint); + + return $this->where($rel->getLocalKey(), 'IN', $keys, 'or', $depth); + } + + /** + * Selects rows where related table has at least x number of entries + * + * NOTE: whereas other 'where' clauses can be called statically due to their + * location in the query builder class, this method cannot be as it is attached + * directly to the model itself. + * + * @param string $relationship The relationship name to constrain against + * @param int $count The minimum number of rows required + * @param int $depth The depth level of the clause, for sub clauses + * @param string $operator The comparison operator used between the column and the count + * @return $this + * @since 2.0.0 + **/ + public function whereRelatedHasCount($relationship, $count = 1, $depth = 0, $operator = '>=') + { + $rel = $this->$relationship(); + $keys = $rel->getConstrainedKeysByCount($count, $operator); + + return $this->where($rel->getLocalKey(), 'IN', $keys, 'and', $depth); + } + + /** + * Limits current model based on conditions of relationship + * + * @FIXME: decide whether or not to use this + * + * This is NOT currently used. The problem here has to do with relationship data. + * If you constrain based on a relationship, and then later on end up wanting to access + * properties of that relationship, it will currently do two queries. Instead, we + * could get the data with the original constraint and attach it to the models in a + * similar fashion to the way that including() works. + * + * To make this work, data would need to be stored on the object, and then seeded + * after the model rows are fetched (like parseIncludes() works now). + * + * @return $this + * @since 2.0.0 + **/ + private function whereRelated($relationship, $constraint) + { + $this->data = []; + $keys = null; + + // Parse for nested relationships + if (strpos($name, '.')) + { + // If we have a nested name, pull out the first one + list($name, $subs) = explode('.', $name, 2); + $relationship = $this->$name(); + $this->data[$name] = $relationship->whereRelated($subs, $constraint); + } + else + { + $relationship = $this->$name(); + $this->data[$name] = $relationship->getConstrainedRows($constraint); + } + + // Update keys to only include those in this and previous results + $keys = is_null($keys) ? $relationship->getRelatedKeysFromRows($this->data[$name]) + : array_intersect($keys, $relationship->getRelatedKeysFromRows($this->data[$name])); + + // Only keep unique keys + $keys = array_unique($keys); + + // Set our where clause if needed + if (!empty($keys)) + { + $this->whereIn($relationship->getLocalKey(), $keys); + } + + return $this; + } + + /** + * Seeds the rows with any pre-fetched data + * + * @FIXME: decide whether or not to use this + * + * @param \Hubzero\Database\Rows $rows The rows to seed + * @return \Hubzero\Database\Rows + * @since 2.0.0 + **/ + private function seed($rows) + { + // Set our constrained (pre-fetched data) back on the rows + foreach ($this->data as $relationship => $data) + { + $rows = $this->$relationship()->seedWithData($rows, $data, $relationship); + } + + return $rows; + } + + /** + * Applies a where clause comparing a field to the current user id + * + * NOTE: whereas other 'where' clauses can be called statically due to their + * location in the query builder class, this method cannot be as it is attached + * directly to the model itself. + * + * @param string $column The field to use for ownership, defaulting to 'created_by' + * @return $this + * @since 2.0.0 + **/ + public function whereIsMine($column = 'created_by') + { + $this->whereEquals($column, \User::get('id')); + return $this; + } + + /** + * Validates the set data attributes against the model rules + * + * @return bool + * @since 2.0.0 + **/ + public function validate() + { + $validity = Rules::validate($this->attributes, $this->getRules()); + + if ($validity === true) + { + return true; + } + + $this->setErrors($validity); + return false; + } + + /** + * Chunks the retrieved data based on a given chunk limit + * + * @param int $size The chunk size + * @return $this + * @since 2.0.0 + **/ + public function paginate($size) + { + // @FIXME: implement! + return $this; + } + + /** + * Retrieves a chuck of data based on standard pagination parameters + * + * @param string $start The request variable used to denote limit start + * @param string $limit The request variable used to denote limit of results to return + * @return $this + * @since 2.0.0 + **/ + public function paginated($start = 'start', $limit = 'limit') + { + $this->pagination = Pagination::init($this->getModelName(), $this->copy()->total(), $start, $limit); + + // Set start and limit on query + $this->start($this->pagination->start); + $this->limit($this->pagination->limit); + + return $this; + } + + /** + * Sets the ordering based on the established request variables + * + * @param string $orderBy The request variable used to denote ordering column + * @param string $orderDir The request variable used to denote ordering direction + * @return $this + * @since 2.0.0 + **/ + public function ordered($orderBy = 'orderby', $orderDir = 'orderdir') + { + // Look for our request vars of interest + $this->orderBy = \Request::getCmd($orderBy, $this->getState('orderby', $this->orderBy)); + $this->orderDir = \Request::getCmd($orderDir, $this->getState('orderdir', $this->orderDir)); + + $qualifiedOrderBy = $this->orderBy; + + // If we have a '.' we'll assume the prefix is a relationship name + if (strpos($this->orderBy, '.') !== false) + { + list($relationship, $field) = explode('.', $this->orderBy); + + // We have to join to apply the order by clause + $relationship = $this->$relationship()->join(); + $qualifiedOrderBy = $relationship->getQualifiedFieldName($field); + } + + // Apply order clause + $this->order($qualifiedOrderBy, $this->orderDir); + + // Set state for future use + $this->setState('orderby', $this->orderBy); + $this->setState('orderdir', $this->orderDir); + + return $this; + } + + /** + * Unsets the ordering + * + * @return $this + * @since 2.2.2 + **/ + public function unordered() + { + $this->unorder(); + + return $this; + } + + /** + * Retrieves state vars set in the model namespace + * + * @param string $var The var to attempt to retrieve + * @param mixed $default The default to return, should the var be unknown + * @return mixed + * @since 2.0.0 + **/ + public function getState($var, $default = null) + { + $key = str_replace('\\', '.', $this->getModelNamespace()) . '.' . $this->getModelName() . ".{$var}"; + return \User::getState($key, $default); + } + + /** + * Sets state vars on the model namespace + * + * @param string $key The key under which the value will go + * @param mixed $value The value to assign to the key + * @return void + * @since 2.0.0 + **/ + public function setState($key, $value) + { + $key = str_replace('\\', '.', $this->getModelNamespace()) . '.' . $this->getModelName() . ".{$key}"; + \User::setState($key, $value); + } + + /** + * Checks whether or not the current user is the owner/creator of the row + * + * @param string $field The field by which creation is determined + * @return bool + * @throws \Hubzero\Error\Exception\RuntimeException If rows have not first been fetched + * @since 2.0.0 + **/ + public function isCreator($field = 'created_by') + { + // Make sure we have a valid row + if (!$this->hasAttribute($field)) + { + throw new RuntimeException('Cannot determine creator of non-existant row(s)'); + } + + return $this->$field == \User::get('id'); + } + + /** + * Finds the named class, checking a handful of scopes + * + * @param string $name The name of the relationship to resolve + * @return object + * @since 2.0.0 + * @throws \Hubzero\Error\Exception\RuntimeException If a class of name cannot be found + **/ + private function resolve($name) + { + if (!class_exists($name)) + { + // Get the scope of the current class and check there too + $name = $this->getModelNamespace() . '\\' . $name; + + if (!class_exists($name)) + { + throw new RuntimeException("Relationship '{$name}' not found"); + } + } + + return new $name; + } + + /** + * Retrieves a one to one model relationship + * + * @param string $model The name of the primary model + * @param string|null $childKey The child key that point to the local key + * @param string|null $thisKey The local key on the model + * @return \Hubzero\Database\Relationship\OneToOne + * @since 2.0.0 + **/ + public function oneToOne($model, $childKey = null, $thisKey = null) + { + // Default the keys if not set + $thisKey = $thisKey ?: $this->getPrimaryKey(); + $childKey = $childKey ?: strtolower($this->getModelName()) . '_id'; + + return new OneToOne($this, $this->resolve($model), $thisKey, $childKey); + } + + /** + * Retrieves a one to many model relationship + * + * @param string $model The name of the model to relate to the current one + * @param string|null $foreignKey The foreign key used to associate the many back to the model + * @param string|null $thisKey The local key used to associate the many back to the model + * @return \Hubzero\Database\Relationship\OneToMany + * @since 2.0.0 + **/ + public function oneToMany($model, $relatedKey = null, $thisKey = null) + { + // Default the keys if not set + $thisKey = $thisKey ?: $this->getPrimaryKey(); + $relatedKey = $relatedKey ?: strtolower($this->getModelName()) . '_id'; + + return new OneToMany($this, $this->resolve($model), $thisKey, $relatedKey); + } + + /** + * Retrieves a one shifts to many model relationship + * + * This is very similar to a one to many relationship, except that we also need to + * constrain by a scope type. Additionally, the related key is actually most likely + * static (scope_id), rather than dynamic based on the model name. + * + * @param string $model The name of the model to relate to the current one + * @param string|null $relatedKey The foreign key used to associate the many back to the model + * @param string|null $shifter The many side field used to differentiate/shift models + * @param string|null $thisKey The local key used to associate the many back to the model + * @return \Hubzero\Database\Relationship\OneShiftsToMany + * @since 2.0.0 + **/ + public function oneShiftsToMany($model, $relatedKey = 'scope_id', $shifter = 'scope', $thisKey = null) + { + // Default the keys if not set + $thisKey = $thisKey ?: $this->getPrimaryKey(); + + return new OneShiftsToMany($this, $this->resolve($model), $thisKey, $relatedKey, $shifter); + } + + /** + * Retrieves a many to many model relationship + * + * @param string $model The name of the model to relate to the current one + * @param string $associativeTable The name of the intermediate table used to associate model->related + * @param string|null $thisKey The local key used on the associative table + * @param string|null $relatedKey The related key used on the associative table + * @return \Hubzero\Database\Relationship\ManyToMany + * @since 2.0.0 + **/ + public function manyToMany($model, $associativeTable = null, $thisKey = null, $relatedKey = null) + { + $related = $this->resolve($model); + $names = [strtolower($this->getModelName()), strtolower($related->getModelName())]; + $namespace = (!$this->namespace ? '' : $this->namespace . '_'); + + // Sort names alphabetically so both sides of manyToMany will resolve to the same table name + sort($names); + + // Default the keys and table if not set + $associativeTable = $associativeTable ?: '#__' . $namespace . implode('_', $names); + $thisKey = $thisKey ?: strtolower($this->getModelName()) . '_id'; + $relatedKey = $relatedKey ?: strtolower($related->getModelName()) . '_id'; + + return new ManyToMany($this, $related, $associativeTable, $thisKey, $relatedKey); + } + + /** + * Retrieves a many shifts to many model relationship + * + * @param string $model The name of the model to relate to the current one + * @param string $associativeTable The name of the intermediate table used to associate model->related + * @param string|null $thisKey The local key used on the associative table + * @param string $shifter The many side field used to differentiate/shift models + * @param string $relatedKey The related key used on the associative table + * @return \Hubzero\Database\Relationship\ManyShiftsToMany + * @since 2.0.0 + **/ + public function manyShiftsToMany($model, $associativeTable = null, $thisKey = 'scope_id', $shifter = 'scope', $relatedKey = null) + { + $related = $this->resolve($model); + + // Default the keys and table if not set + $associativeTable = $associativeTable ?: '#__' . strtolower($related->getModelName()) . '_object'; + $relatedKey = $relatedKey ?: strtolower($related->getModelName()) . '_id'; + + return new ManyShiftsToMany($this, $related, $associativeTable, $thisKey, $relatedKey, $shifter); + } + + /** + * Retrieves a belongs to one model relationship + * + * @param string $model The name of the model to relate to the current one + * @param string|null $thisKey The local key used to associate the many back to the model + * @param string|null $parentKey The parent key used to associate the model to its parent + * @return \Hubzero\Database\Relationship\BelongsToOne + * @since 2.0.0 + **/ + public function belongsToOne($model, $thisKey = null, $parentKey = null) + { + $parent = $this->resolve($model); + + // Default the keys if not set + $thisKey = $thisKey ?: strtolower($parent->getModelName()) . '_id'; + $parentKey = $parentKey ?: $this->getPrimaryKey(); + + return new BelongsToOne($this, $parent, $thisKey, $parentKey); + } + + /** + * Retrieves a one to many through model relationship + * + * Note that here, versus the manyToMany relationship, we assume the 'through' item + * actually has a formal model for it, rather than just an intermediate table name. + * + * @param string $model The name of the related model to associate to the current one + * @param string $through The name of the intermediate model + * @param string|null $relatedKey The related key used to associate the model to its parent + * @param string|null $localKey The local key used to associate the many back to the model + * @return \Hubzero\Database\Relationship\OneToManyThrough + * @since 2.0.0 + **/ + public function oneToManyThrough($model, $through, $relatedKey = null, $localKey = null) + { + // Format the model name and instantiate new object + $related = $this->resolve($model); + $through = $this->resolve($through); + + // Keys + $localKey = $localKey ?: strtolower($this->getModelName()) . '_id'; + $relatedKey = $relatedKey ?: strtolower($through->getModelName()) . '_id'; + + return new OneToManyThrough($this, $related, $through->getTableName(), $localKey, $relatedKey); + } + + /** + * Retrieves a belongs to one model relationship as the inverse of a oneShiftsToMany + * + * @param string $shifter The parent side field used to differentiate/shift models + * @param string $thisKey The local key used to associate the many back to the model + * @return \Hubzero\Database\Relationship\BelongsToOne + * @since 2.0.0 + **/ + public function shifter($shifter = 'scope', $thisKey = 'scope_id') + { + $parent = $this->resolve($this->$shifter); + + return new BelongsToOne($this, $parent, $thisKey, 'id'); + } + + /** + * Attaches the given model(s) to the current one via its relationship + * + * This is kind of like calling save on an individual relationship, + * except that we're attaching the models back to the parent entity. + * This is helpful if you're going to call saveAndPropagate and want + * to pass the parent object back to a view in the event of a save error. + * + * @param string $relationship The relationship to invoke + * @param array|object $models The model or models to attach + * @return $this + * @since 2.0.0 + **/ + public function attach($relationship, $models) + { + // If we have an array, we'll put it into a rows object + // (like we would if we were fetching the results from the db) + if (is_array($models)) + { + $rows = new Rows; + + foreach ($models as $model) + { + $rows->push($model); + } + } + else + { + // Otherwise it's just a single model + $rows = $models; + } + + // Get our rows associated according to their relationship type + // This means we add related keys, etc to the passed in rows + $rows = $this->$relationship()->associate($rows); + $this->addRelationship($relationship, $rows); + + return $this; + } + + /** + * Sets an associated relationship to be retrieved with the current model + * + * @return $this + * @since 2.0.0 + **/ + public function including() + { + // Divide our relationships into those that are constrained and those that are unconstrained + foreach (func_get_args() as $relationship) + { + $this->includes[] = $relationship; + } + + return $this; + } + + /** + * Retrieves an associated model in conjunction with the current one + * + * @param \Hubzero\Database\Rows $rows The rows to parse and augment + * @return \Hubzero\Database\Rows + * @since 2.0.0 + **/ + private function parseIncluding($rows) + { + $subs = null; + $constraint = null; + foreach ($this->includes as $relationship) + { + // Check for array, meaning we have relationship_name => constraint + if (is_array($relationship)) + { + list($relationship, $constraint) = $relationship; + } + + // Parse for nested relationships + if (strpos($relationship, '.')) + { + list($relationship, $subs) = explode('.', $relationship, 2); + } + + // If we have subs and a constraint, the constraint should apply to the subs, not the intermediate relation + if (isset($subs) && isset($constraint)) + { + $subs = [$subs, $constraint]; + $constraint = null; + } + + // Get the actual rows + $rows = $this->$relationship()->seedWithRelation($rows, $relationship, $constraint, $subs); + + // Reset some vars + $subs = null; + $constraint = null; + } + + return $rows; + } + + /** + * Adds alternate locations to look for model properties + * + * This method merely adds them to the list. See the __get + * method above for the code that actually checks for a + * valid attribute on the forwarding model. + * + * @return $this + * @since 2.0.0 + **/ + public function forwardTo() + { + foreach (func_get_args() as $relationship) + { + $this->forwards[] = $relationship; + } + + return $this; + } + + /** + * Adds a new relationship to the current model + * + * @param string $name The name of the relationship + * @param object $model The model or rows to add + * @return $this + * @since 2.0.0 + **/ + public function addRelationship($name, $model) + { + $this->relationships[$name] = $model; + + return $this; + } + + /** + * Gets all relationships + * + * @return array + * @since 2.0.0 + **/ + public function getRelationships() + { + return $this->relationships; + } + + /** + * Gets the defined relationship + * + * @param string $name The relationship to return + * @return \Hubzero\Database\Rows|\Hubzero\Database\Relational|static + * @since 2.0.0 + **/ + public function getRelationship($name) + { + return isset($this->relationships[$name]) ? $this->relationships[$name] : null; + } + + /** + * Establishes a relationship, fetching the rows as needed + * + * @param string $name The name of the relationship + * @return $this + * @since 2.0.0 + **/ + public function makeRelationship($name) + { + // See if the relationship already exists + if (!$this->getRelationship($name)) + { + // Get the child rows/row and set them back on the model as a relationship for future use + $rows = call_user_func_array(array($this, $name), array())->rows(); + $this->addRelationship($name, $rows); + } + + return $this; + } + + /** + * Establishes a relationship, based on the acquaintances, fetching the rows as needed + * + * @param string $name The name of the relationship + * @return $this + * @since 2.1.0 + **/ + public function makeAcquaintance($name) + { + // See if the relationship already exists + if (!$this->getRelationship($name)) + { + // Get the child rows/row and set them back on the model as a relationship for future use + $rows = call_user_func_array(self::$acquaintances[$name], [$this])->rows(); + $this->addRelationship($name, $rows); + } + + return $this; + } + + /** + * Registers a new relationship at runtime, rather than explicitly in model + * + * @param string $name The relationship name + * @param closure $response The relationship response function + * @return void + * @since 2.1.0 + **/ + public static function registerRelationship($name, $response) + { + self::$acquaintances[$name] = $response; + } + + /** + * Identifies known relationships on the model + * + * @return array + * @since 2.1.0 + **/ + public static function introspectRelationships() + { + $instance = self::blank(); + $methods = []; + $reflection = new \ReflectionClass($instance); + $relationship = __NAMESPACE__ . '\\Relationship\\Relationship'; + + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) + { + // If method exists on the base model, ignore it + if ($method->class == __CLASS__) + { + continue; + } + + $result = null; + + try + { + // Invoke method and get the result + $result = $method->invoke(new $reflection->name); + } + catch (\Exception $e) + { + // Ignore all errors - we'll assume that means we don't care about the method + } + + // If the method returned a relationship, we'll keep track of it + if ($result instanceof $relationship) + { + $methods[] = $method->name; + } + } + + $acquaintances = array_keys(self::$acquaintances); + + return array_merge($methods, $acquaintances); + } + + /** + * Generates automatic created field value + * + * @param array $data The data being saved + * @return string + * @since 2.0.0 + **/ + public function automaticCreated($data) + { + if (!isset($data['created']) || !$data['created']) + { + $now = new Date('now'); + $data['created'] = $now->toSql(); + } + return $data['created']; + } + + /** + * Generates automatic created by field value + * + * @param array $data The data being saved + * @return int + * @since 2.0.0 + **/ + public function automaticCreatedBy($data) + { + return (isset($data['created_by']) && $data['created_by'] ? (int)$data['created_by'] : (int)\User::get('id')); + } + + /** + * Generates automatic asset id field + * + * @return int + * @since 2.0.0 + **/ + public function automaticAssetId() + { + return Asset::resolve($this); + } +} diff --git a/core/libraries/Hubzero/Database/Relationship/BelongsToOne.php b/core/libraries/Hubzero/Database/Relationship/BelongsToOne.php new file mode 100644 index 00000000000..224c745dc3d --- /dev/null +++ b/core/libraries/Hubzero/Database/Relationship/BelongsToOne.php @@ -0,0 +1,15 @@ +shifter = $shifter; + } + + /** + * Joins the related table together with the intermediate table for the pending query + * + * This is primarily used when we're getting the related results and we need to work + * our way backwards through the intermediate table. + * + * @return $this + * @since 2.0.0 + **/ + public function mediate() + { + parent::mediate(); + + // Add this where clause in mediation as it's really a factor of the join itself + $this->related->whereEquals($this->associativeTable . '.' . $this->shifter, strtolower($this->model->getModelName())); + + return $this; + } + + /** + * Gets the constrained count + * + * @param int $count The count to limit by + * @param string $operator The comparison operator used between the column and the count + * @return array + * @since 2.0.0 + **/ + public function getConstrainedKeysByCount($count, $operator = '>=') + { + $associativeTable = $this->associativeTable; + $associativeLocal = $this->associativeLocal; + $shifter = $this->shifter; + $model = $this->model; + + return $this->getConstrainedKeys(function($related) use ($count, $associativeTable, $associativeLocal, $shifter, $model, $operator) + { + $related->whereEquals($associativeTable . '.' . $shifter, strtolower($model->getModelName())) + ->group($shifter) + ->group($associativeLocal) + ->having('COUNT(*)', $operator, $count); + }); + } + + /** + * Generates the connection data needed to create the associative entry + * + * @return array + * @since 2.0.0 + **/ + protected function getConnectionData() + { + return [$this->associativeLocal => $this->model->getPkValue(), $this->shifter => strtolower($this->model->getModelName())]; + } + + /** + * Removes the relationship between the two sides of the many to many + * (not deleting either of the actual sides of the models themselves) + * + * @param array $ids The identifiers to remove from the associative table + * @param closure $constraint Additional constraints to place on the query + * @return $this + * @since 2.0.0 + **/ + public function disconnect($ids, $constraint = null) + { + $associativeTable = $this->associativeTable; + $shifter = $this->shifter; + $model = $this->model; + + parent::disconnect($ids, function($query) use ($associativeTable, $model, $shifter) + { + $query->whereEquals($associativeTable . '.' . $shifter, strtolower($model->getModelName())); + }); + + return $this; + } +} diff --git a/core/libraries/Hubzero/Database/Relationship/ManyToMany.php b/core/libraries/Hubzero/Database/Relationship/ManyToMany.php new file mode 100644 index 00000000000..b6a6e4b09b9 --- /dev/null +++ b/core/libraries/Hubzero/Database/Relationship/ManyToMany.php @@ -0,0 +1,230 @@ +getAttributes() as $k => $v) + { + if (strpos($k, 'associative_') === 0) + { + $key = substr($k, 12); + $associatives->$key = $v; + $row->removeAttribute($k); + } + } + + if (!empty($associatives)) + { + $row->associated = $associatives; + } + } + + return $rows; + } + + /** + * Associates the model provided back to the model by way of their proper keys + * + * @param object $model The model to associate + * @param closure $callback A callback to potentially append additional data + * @return object + * @since 2.0.0 + **/ + public function associate($model, $callback = null) + { + $relationship = $this; + Event::listen( + function($event) use ($model, $relationship) + { + $relationship->connect([$model->id]); + }, + $model->getTableName() . '_new' + ); + + return $model; + } + + /** + * Joins the related table together with the intermediate table for the pending query + * + * This is primarily used when we're getting the related results and we need to work + * our way backwards through the intermediate table. + * + * @return $this + * @since 2.0.0 + **/ + public function mediate() + { + parent::mediate(); + + // We also want to grab any associative fields at this time, rather than having to come back for them later + // To do that, we'll prefix the columns and then strip them after the query + $columns = $this->model->getStructure()->getTableColumns($this->associativeTable); + + // Get rid of known columns (don't use a primary key other than id and you'll be fine here!) + if (isset($columns['id'])) + { + unset($columns['id']); + } + + if (isset($this->shifter)) + { + unset($columns[$this->shifter]); + } + + unset($columns[$this->associativeLocal]); + unset($columns[$this->associativeRelated]); + + // Add remaining fields to our select statement + if (count($columns) > 0) + { + foreach ($columns as $column => $type) + { + $this->related->select($this->associativeTable . '.' . $column, 'associative_' . $column); + } + } + + return $this; + } + + /** + * Connects the provided identifiers back to the parent model by way of associative entities + * + * This will add a new entry, irrelevant of whether or not a comparable entry is already there. + * To avoid this behavior, either use the sync function or set a constraint on your associative + * table. + * + * @param array $ids The identifiers to place in the associative table + * @return $this + * @since 2.0.0 + **/ + public function connect($ids) + { + if (is_array($ids) && count($ids) > 0) + { + foreach ($ids as $id => $associative) + { + // Build base data + $id = (is_array($associative)) ? $id : $associative; + $data = array_merge($this->getConnectionData(), [$this->associativeRelated => $id]); + + // If we have associative data, include that in the query + if (is_array($associative)) + { + $data = array_merge($data, $associative); + } + + // Save data + $query = $this->model->getQuery()->push($this->associativeTable, $data, true); + } + } + + return $this; + } + + /** + * Generates the connection data needed to create the associative entry + * + * @return array + * @since 2.0.0 + **/ + protected function getConnectionData() + { + return [$this->associativeLocal => $this->model->getPkValue()]; + } + + /** + * Removes the relationship between the two sides of the many to many + * (not deleting either of the actual sides of the models themselves) + * + * @param array $ids The identifiers to remove from the associative table + * @param closure $constraint Additional constraints to place on the query + * @return $this + * @since 2.0.0 + **/ + public function disconnect($ids, $constraint = null) + { + if (is_array($ids) && count($ids) > 0) + { + $query = $this->model->getQuery(); + $query->delete($this->associativeTable) + ->whereEquals($this->associativeLocal, $this->model->getPkValue()) + ->whereIn($this->associativeRelated, $ids); + + if (isset($constraint) && is_callable($constraint)) + { + call_user_func_array($constraint, [$query]); + } + + $query->execute(); + } + + return $this; + } + + /** + * Syncs the provided identifiers back to the parent model by way of associative entities, + * deleting ones that should no longer be there, and adding ones that are missing. + * + * @param array $ids The identifiers to place in the associative table + * @return $this + * @since 2.0.0 + **/ + public function sync($ids) + { + if (is_array($ids)) + { + // Get a query instance + $query = $this->model->getQuery(); + + // Get the parent primary key value + $localKeyValue = $this->model->getPkValue(); + + // Get any existing entries + $existing = $query->select($this->associativeRelated) + ->from($this->associativeTable) + ->whereEquals($this->associativeLocal, $localKeyValue) + ->fetch('column'); + + // See if there's anything to delete + $deletes = array_diff($existing, $ids); + if (!empty($deletes)) + { + $query->delete($this->associativeTable) + ->whereEquals($this->associativeLocal, $localKeyValue) + ->whereIn($this->associativeRelated, $deletes) + ->execute(); + } + + // Now see if there's anything to add + $inserts = array_diff($ids, $existing); + $this->connect($inserts); + } + + return $this; + } +} diff --git a/core/libraries/Hubzero/Database/Relationship/OneShiftsToMany.php b/core/libraries/Hubzero/Database/Relationship/OneShiftsToMany.php new file mode 100644 index 00000000000..22128d14423 --- /dev/null +++ b/core/libraries/Hubzero/Database/Relationship/OneShiftsToMany.php @@ -0,0 +1,95 @@ +shifter = $shifter; + } + + /** + * Associates the models provided back to the model by way of their proper keys + * + * We use this time to also set a callback where we define our shifter. + * + * @param object|array $models A single model or array of models to associate + * @param closure $callback A callback to potentially append additional data + * @return object|array + * @since 2.0.0 + **/ + public function associate($models, $callback = null) + { + $modelName = $this->model->getModelName(); + $shifter = $this->shifter; + + parent::associate($models, function($model) use ($modelName, $shifter) + { + $model->set($shifter, strtolower($modelName)); + }); + + return $models; + } + + /** + * Constrains the relationship content to the applicable rows on the related model + * + * @return object + * @since 2.0.0 + **/ + public function constrain() + { + return $this->related + ->whereEquals($this->relatedKey, $this->model->{$this->localKey}) + ->whereEquals($this->shifter, strtolower($this->model->getModelName())); + } + + /** + * Gets the relations that will be seeded on to the provided rows + * + * @param array $keys The keys for which to fetch related items + * @param closure $constraint The constraint function to limit related items + * @return array + * @since 2.0.0 + **/ + protected function getRelations($keys, $constraint = null) + { + if (isset($constraint)) + { + call_user_func_array($constraint, array($this->related)); + } + + return $this->related + ->whereIn($this->relatedKey, array_unique($keys)) + ->whereEquals($this->shifter, strtolower($this->model->getModelName())); + } +} diff --git a/core/libraries/Hubzero/Database/Relationship/OneToMany.php b/core/libraries/Hubzero/Database/Relationship/OneToMany.php new file mode 100644 index 00000000000..0f68c2a6522 --- /dev/null +++ b/core/libraries/Hubzero/Database/Relationship/OneToMany.php @@ -0,0 +1,215 @@ +constrain()->rows(); + } + + /** + * Associates the models provided back to the model by way of their proper keys + * + * Because this is a one to many relationship, we could be setting either one + * or many items on the related side at a given time. We must then be prepared + * to loop over the items. + * + * @param object|array $models A single model or array of models to associate + * @param closure $callback A callback to potentially append additional data + * @return object|array + * @since 2.0.0 + **/ + public function associate($models, $callback = null) + { + if (is_array($models) || $models instanceof \Hubzero\Database\Rows) + { + foreach ($models as $model) + { + parent::associate($model, $callback); + } + } + else + { + parent::associate($models, $callback); + } + + return $models; + } + + /** + * Saves new related models with the given data + * + * @param array $data An array of datasets being saved to new models + * @return bool + * @since 2.0.0 + **/ + public function save($data) + { + // Check and make sure this is an array of arrays + if (!is_array($data)) + { + return false; + } + + if (isset($data[0]) && is_array($data[0])) + { + foreach ($data as $d) + { + if (!parent::save($d)) + { + return false; + } + } + } + else + { + // If not an array of arrays, we'll assume it's just one item to save + if (!parent::save($data)) + { + return false; + } + } + + return true; + } + + /** + * Saves all of the given models + * + * @param array $models An array of models being associated and saved + * @return array + * @since 2.0.0 + **/ + public function saveAll($models) + { + foreach ($models as $model) + { + if (!$this->associate($model)->save()) + { + return false; + } + } + + return true; + } + + /** + * Deletes all rows attached to the current model + * + * @return bool + * @since 2.0.0 + **/ + public function destroyAll() + { + // @FIXME: could make this a single query...i.e. delete where id in (...) + foreach ($this->related as $model) + { + if (!$model->destroy()) + { + return false; + } + } + + return true; + } + + /** + * Get keys based on given constraint + * + * @param closure $constraint The constraint function to apply + * @return array + * @since 2.0.0 + **/ + public function getConstrainedKeys($constraint) + { + //$this->related->select($this->related->getPrimaryKey()) + $this->related->select($this->relatedKey); + + return $this->getConstrained($constraint)->fieldsByKey($this->relatedKey); + } + + /** + * Loads the relationship content with the provided data + * + * @param array $rows The rows that we'll be seeding + * @param string $data The data to seed + * @param string $name The name of the relationship + * @return object + * @since 2.0.0 + **/ + public function seedWithData($rows, $data, $name) + { + $resultsByRelatedKey = $this->getResultsByRelatedKey($data); + + return $this->seed($rows, $resultsByRelatedKey, $name); + } + + /** + * Seeds the given rows with data + * + * @param array $rows The rows to seed on to + * @param array $data The data from which to seed + * @param string $name The relationship name + * @return array + * @since 2.0.0 + **/ + protected function seed($rows, $data, $name) + { + // Add the relationships back to the original models + foreach ($rows as $row) + { + if (isset($data[$row->{$this->localKey}])) + { + $row->addRelationship($name, $data[$row->{$this->localKey}]); + } + else + { + $row->addRelationship($name, new Rows); + } + } + + return $rows; + } + + /** + * Sorts the relations into arrays keyed by the related key + * + * @param array $relations The relations to sort + * @return array + * @since 2.0.0 + **/ + protected function getResultsByRelatedKey($relations) + { + $resultsByRelatedKey = []; + + foreach ($relations as $relation) + { + if (!isset($resultsByRelatedKey[$relation->{$this->relatedKey}])) + { + $resultsByRelatedKey[$relation->{$this->relatedKey}] = new Rows; + } + + $resultsByRelatedKey[$relation->{$this->relatedKey}]->push($relation); + } + + return $resultsByRelatedKey; + } +} diff --git a/core/libraries/Hubzero/Database/Relationship/OneToManyThrough.php b/core/libraries/Hubzero/Database/Relationship/OneToManyThrough.php new file mode 100644 index 00000000000..e2bbba27454 --- /dev/null +++ b/core/libraries/Hubzero/Database/Relationship/OneToManyThrough.php @@ -0,0 +1,195 @@ +getPrimaryKey(), $related->getPrimaryKey()); + + $this->associativeTable = $associativeTable; + $this->associativeLocal = $associativeLocal; + $this->associativeRelated = $associativeRelated; + } + + /** + * Loads the relationship content and returns the related side of the model + * + * @return object + * @since 2.0.0 + **/ + public function constrain() + { + $this->mediate(); + + $this->related->whereEquals($this->associativeTable . '.' . $this->associativeLocal, $this->model->getPkValue()); + + return $this->related; + } + + /** + * Get keys based on a given constraint + * + * @param closure $constraint The constraint function to apply + * @return array + * @since 2.0.0 + **/ + public function getConstrainedKeys($constraint) + { + $this->mediate(); + + return array_unique($this->getConstrained($constraint)->fieldsByKey($this->associativeLocal)); + } + + /** + * Gets the constrained count + * + * @param int $count The count to limit by + * @param string $operator The comparison operator used between the column and the count + * @return array + * @since 2.0.0 + **/ + public function getConstrainedKeysByCount($count, $operator = '>=') + { + $associativeLocal = $this->associativeLocal; + + return $this->getConstrainedKeys(function($related) use ($count, $associativeLocal, $operator) + { + $related->group($associativeLocal)->having('COUNT(*)', $operator, $count); + }); + } + + /** + * Joins the intermediate and related tables together to the model for the pending query + * + * @return $this + * @since 2.0.0 + **/ + public function join() + { + // We do a left outer join here because we're not trying to limit the primary table's results + // This function is primarily used when needing to sort by a field in the joined table + $this->model->select($this->model->getQualifiedFieldName('*')) + ->select($this->related->getQualifiedFieldName('*')) + ->join($this->associativeTable, + $this->model->getQualifiedFieldName($this->localKey), + $this->associativeLocal, + 'LEFT OUTER') + ->join($this->related->getTableName(), + $this->associativeRelated, + $this->related->getQualifiedFieldName($this->relatedKey), + 'LEFT OUTER'); + + return $this; + } + + /** + * Joins the related table together with the intermediate table for the pending query + * + * This is primarily used when we're getting the related results and we need to work + * our way backwards through the intermediate table. + * + * @return $this + * @since 2.0.0 + **/ + public function mediate() + { + $this->related->select($this->related->getQualifiedFieldName('*')) + ->select($this->associativeLocal) + ->join($this->associativeTable, + $this->related->getQualifiedFieldName($this->relatedKey), + $this->associativeRelated); + + return $this; + } + + /** + * Gets the relations that will be seeded on to the provided rows + * + * @param array $keys The keys for which to fetch related items + * @param closure $constraint The constraint function to limit related items + * @return array + * @since 2.0.0 + **/ + protected function getRelations($keys, $constraint = null) + { + $this->mediate(); + + if (isset($constraint)) + { + call_user_func_array($constraint, array($this->related)); + } + + return $this->related->whereIn($this->associativeTable . '.' . $this->associativeLocal, array_unique($keys)); + } + + /** + * Sorts the relations into arrays keyed by the related key + * + * @param array $relations The relations to sort + * @return array + * @since 2.0.0 + **/ + protected function getResultsByRelatedKey($relations) + { + $resultsByRelatedKey = []; + + foreach ($relations as $relation) + { + if (!isset($resultsByRelatedKey[$relation->{$this->associativeLocal}])) + { + $resultsByRelatedKey[$relation->{$this->associativeLocal}] = new Rows; + } + + $resultsByRelatedKey[$relation->{$this->associativeLocal}]->push($relation); + } + + return $resultsByRelatedKey; + } +} diff --git a/core/libraries/Hubzero/Database/Relationship/OneToOne.php b/core/libraries/Hubzero/Database/Relationship/OneToOne.php new file mode 100644 index 00000000000..dca51dc0840 --- /dev/null +++ b/core/libraries/Hubzero/Database/Relationship/OneToOne.php @@ -0,0 +1,15 @@ +model = $model; + $this->related = $related; + $this->localKey = $localKey; + $this->relatedKey = $relatedKey; + } + + /** + * Handles calls to undefined methods, assuming they should be passed up to the model + * + * @param string $name The method name being called + * @param array $arguments The method arguments provided + * @return mixed + * @since 2.0.0 + **/ + public function __call($name, $arguments) + { + return call_user_func_array(array($this->constrain(), $name), $arguments); + } + + /** + * Returns the key name of the primary table + * + * @return string + * @since 2.0.0 + **/ + public function getLocalKey() + { + return $this->localKey; + } + + /** + * Returns the key name of the related table + * + * @return string + * @since 2.0.0 + **/ + public function getRelatedKey() + { + return $this->relatedKey; + } + + /** + * Fetch results of relationship + * + * @return \Hubzero\Database\Relational + * @since 2.0.0 + **/ + public function rows() + { + return $this->constrain()->row(); + } + + /** + * Constrains the relationship content to the applicable rows on the related model + * + * @return object + * @since 2.0.0 + **/ + public function constrain() + { + return $this->related->whereEquals($this->relatedKey, $this->model->{$this->localKey}); + } + + /** + * Gets keys based on a given constraint + * + * @param closure $constraint The constraint function to apply + * @return array + * @since 2.0.0 + **/ + public function getConstrainedKeys($constraint) + { + $this->related->select($this->relatedKey); + + return $this->getConstrained($constraint)->fieldsByKey($this->relatedKey); + } + + /** + * Gets rows based on given constraint + * + * @param closure $constraint The constraint function to apply + * @return \Hubzero\Database\Rows + * @since 2.0.0 + **/ + public function getConstrainedRows($constraint) + { + $this->related->select($this->related->getQualifiedFieldName('*')); + + return $this->getConstrained($constraint); + } + + /** + * Gets the constrained count + * + * @param int $count The count to limit by + * @param string $operator The comparison operator used between the column and the count + * @return array + * @since 2.0.0 + **/ + public function getConstrainedKeysByCount($count, $operator = '>=') + { + $relatedKey = $this->relatedKey; + + return $this->getConstrainedKeys(function($related) use ($count, $relatedKey, $operator) + { + $related->group($relatedKey)->having('COUNT(*)', $operator, $count); + }); + } + + /** + * Gets the constrained items + * + * @param closure $constraint The constraint function to apply + * @return \Hubzero\Database\Rows + * @since 2.0.0 + **/ + protected function getConstrained($constraint) + { + call_user_func_array($constraint, array($this->related)); + + // Note that rows is called on the base relational model, not on this relationship, + // thus it is not calling the constrain method...which is how we want it to work. + // Constraining here would not make sense as that would limit our result to 1 entry. + return $this->related->rows(); + } + + /** + * Get related keys from a given row set + * + * @param \Hubzero\Database\Rows $rows The rows from which to grab the related keys + * @return array + * @since 2.0.0 + **/ + public function getRelatedKeysFromRows($rows) + { + return $rows->fieldsByKey($this->getRelatedKey()); + } + + /** + * Joins the related table together for the pending query + * + * @return $this + * @since 2.0.0 + **/ + public function join() + { + // We do a left outer join here because we're not trying to limit the primary table's results + // This function is primarily used when needing to sort by a field in the joined table + $this->model->select($this->model->getQualifiedFieldName('*')) + ->join($this->related->getTableName(), + $this->model->getQualifiedFieldName($this->localKey), + $this->related->getQualifiedFieldName($this->relatedKey), + 'LEFT OUTER'); + + return $this; + } + + /** + * Associates the model provided back to the model by way of their proper keys + * + * Because this is a singular relationship, we never expect to have more than one + * model at at time. + * + * @param object $model The model to associate + * @param closure $callback A callback to potentially append additional data + * @return object + * @since 2.0.0 + **/ + public function associate($model, $callback = null) + { + $model->set($this->relatedKey, $this->model->getPkValue()); + + if (isset($callback) && is_callable($callback)) + { + call_user_func_array($callback, [$model]); + } + + return $model; + } + + /** + * Saves a new related model with the given data + * + * @param array $data The data being saved on the new model + * @return bool + * @since 2.0.0 + **/ + public function save($data) + { + $related = $this->related; + $model = $related::newFromResults($data); + + return $this->associate($model)->save(); + } + + /** + * Loads the relationship content with the provided data + * + * @param array $rows The rows that we'll be seeding + * @param string $data The data to seed + * @param string $name The name of the relationship + * @return object + * @since 2.0.0 + **/ + public function seedWithData($rows, $data, $name) + { + return $this->seed($rows, $data, $name); + } + + /** + * Loads the relationship content, and sets it on the related model + * + * This is used when pre-loading relationship content + * via ({@link \Hubzero\Database\Relational::including()}) + * + * @param array $rows The rows that we'll be seeding + * @param string $name The relationship name that we'll use to attach to the rows + * @param closure $constraint The constraint function to limit related items + * @param string $subs The nested relationships that should be passed on to the child + * @return object + * @since 2.0.0 + **/ + public function seedWithRelation($rows, $name, $constraint = null, $subs = null) + { + if (!$keys = $rows->fieldsByKey($this->localKey)) + { + return $rows; + } + + $relations = $this->getRelations($keys, $constraint); + + if (isset($subs)) + { + $relations->including($subs); + } + + $resultsByRelatedKey = $this->getResultsByRelatedKey($relations); + + return $this->seed($rows, $resultsByRelatedKey, $name); + } + + /** + * Gets the relations that will be seeded on to the provided rows + * + * @param array $keys The keys for which to fetch related items + * @param closure $constraint The constraint function to limit related items + * @return array + * @since 2.0.0 + **/ + protected function getRelations($keys, $constraint = null) + { + if (isset($constraint)) + { + call_user_func_array($constraint, array($this->related)); + } + + return $this->related->whereIn($this->relatedKey, array_unique($keys)); + } + + /** + * Sorts the relations into arrays keyed by the related key + * + * @param array $relations The relations to sort + * @return array + * @since 2.0.0 + **/ + protected function getResultsByRelatedKey($relations) + { + return $relations->rows(); + } + + /** + * Seeds the given rows with data + * + * @param \Hubzero\Database\Rows $rows The rows to seed on to + * @param \Hubzero\Database\Rows $data The data from which to seed + * @param string $name The relationship name + * @return array + * @since 2.0.0 + **/ + protected function seed($rows, $data, $name) + { + foreach ($rows as $row) + { + if ($related = $data->seek($row->{$this->localKey})) + { + $row->addRelationship($name, $related); + } + else + { + $related = $this->related; + $row->addRelationship($name, $related::blank()); + } + } + + return $rows; + } +} diff --git a/core/libraries/Hubzero/Database/Rows.php b/core/libraries/Hubzero/Database/Rows.php new file mode 100644 index 00000000000..741d4918fee --- /dev/null +++ b/core/libraries/Hubzero/Database/Rows.php @@ -0,0 +1,484 @@ +push($row); + } + } + } + + /* + * Errors trait for error handling + **/ + use Traits\ErrorBag; + + /** + * Internal array of iterable data + * + * @var array + **/ + private $rows = array(); + + /** + * Order by used to retrieve these rows + * + * @var string + **/ + public $orderBy = 'id'; + + /** + * Order direction used to retrieve these rows + * + * @var string + **/ + public $orderDir = 'asc'; + + /** + * The pagination object based on these rows + * + * @var \Hubzero\Database\Pagination + **/ + public $pagination = null; + + /** + * Calls the given array function on the rows object and attaches itself to the model + * + * @return mixed + * @since 2.1.0 + **/ + private function callArrayFunc($function) + { + $row = $function($this->rows); + + return ($row) ? $row->setIterator($this) : $row; + } + + /** + * Pushes a new model on to the stack + * + * @param \Hubzero\Database\Relational|static $model The model to add + * @return void + * @since 2.0.0 + **/ + public function push(Relational $model) + { + // Index by primary key if possible, otherwise plain incremental array + // Also check to see if that key already exists. If so, we'll just start + // appending items to the array. This will result in a mixed array and + // subsequent items will not be seekable. + if ($model->getPkValue() && (!is_array($this->rows) || !array_key_exists($model->getPkValue(), $this->rows))) + { + $this->rows[$model->getPkValue()] = $model; + } + else + { + $this->rows[] = $model; + } + } + + /** + * Removes model from the stack + * + * @return void + * @since 2.0.0 + **/ + public function drop($key) + { + unset($this->rows[$key]); + } + + /** + * Clears out any existing rows + * + * @return void + * @since 2.0.0 + **/ + public function clear() + { + $this->rows = array(); + } + + /** + * Selects a number of randomly selected rows + * + * @param integer $n The number of rows to randomly select + * @return Rows + * @since 2.1.13 + **/ + public function pickRandom($n) + { + $rows = $this->rows; + + shuffle($rows); + $randomRows = array_slice($rows, 0, $n); + $rowsObject = new self($randomRows); + + return $rowsObject; + } + + /** + * Transforms rows into a JSON array + * + * @return array + * @since 2.0.0 + **/ + public function toJson() + { + return $this->to('json'); + } + + /** + * Transforms rows into an object array + * + * @return array + * @since 2.0.0 + **/ + public function toObject() + { + return $this->to('object'); + } + + /** + * Transforms rows into an array of arrays + * + * @return array + * @since 2.0.0 + **/ + public function toArray() + { + return $this->to(); + } + + /** + * Outputs rows as given type + * + * @return array + * @since 2.0.0 + **/ + public function to($type = 'array') + { + $rows = []; + + if ($this->rows && $this->count()) + { + foreach ($this->rows as $row) + { + $method = 'to' . ucfirst($type); + $rows[] = $row->$method(); + } + } + + return $rows; + } + + /** + * Grabs the raw rows out of the iterator + * + * @return array + * @since 2.0.0 + **/ + public function raw() + { + return $this->rows; + } + + /** + * Gets current row in array of rows + * + * @return mixed + * @since 2.0.0 + **/ + public function current() + { + return $this->callArrayFunc('current'); + } + + /** + * Gets the current key + * + * @return string + * @since 2.0.0 + **/ + public function key() + { + if (isset($this->rows)) + { + return key($this->rows); + } + } + + /** + * Returns the result keys for the current dataset + * + * @param string $key The key for which to pull all values + * @return array + * @since 2.0.0 + **/ + public function fieldsByKey($key) + { + $keys = array(); + + if ($this->rows && $this->count()) + { + foreach ($this->rows as $row) + { + $keys[] = $row->$key; + } + } + + return $keys; + } + + /** + * Gets first item from rows property, if set + * + * @return mixed + * @since 2.0.0 + **/ + public function first() + { + return $this->callArrayFunc('reset'); + } + + /** + * Gets previous item in iterable list + * + * @return mixed + * @since 2.1.0 + **/ + public function prev() + { + return $this->callArrayFunc('prev'); + } + + /** + * Gets next item in iterable list + * + * @return mixed + * @since 2.0.0 + **/ + public function next() + { + return $this->callArrayFunc('next'); + } + + /** + * Rewinds rows back to start + * + * @return void + * @since 2.0.0 + **/ + public function rewind() + { + if (isset($this->rows)) + { + reset($this->rows); + } + } + + /** + * Fast-forwards to the end of the iterable list + * + * @return mixed + * @since 2.0.0 + **/ + public function last() + { + return $this->callArrayFunc('end'); + } + + /** + * Checks to see if the current item is the first in the list + * + * @param int $key The key to check against + * @return bool + * @since 2.1.0 + **/ + public function isFirst($key) + { + if ($this->rows && $this->count()) + { + return $key == array_slice($this->rows, 0, 1)[0]->getPkValue(); + } + + return false; + } + + /** + * Checks to see if the current item is the last in the list + * + * @param int $key The key to check against + * @return bool + * @since 2.1.0 + **/ + public function isLast($key) + { + if ($this->rows && $this->count()) + { + return $key == array_slice($this->rows, -1, 1)[0]->getPkValue(); + } + + return false; + } + + /** + * Validates current key + * + * @return bool + * @since 2.0.0 + **/ + public function valid() + { + $valid = false; + + if ($this->rows && $this->count()) + { + $key = key($this->rows); + $valid = ($key !== null && $key !== false); + } + + return $valid; + } + + /** + * Counts the number of rows + * + * @return int number of rows + * @since 2.0.0 + **/ + public function count() + { + return count($this->rows); + } + + /** + * Seeks to the given key + * + * @return mixed + * @since 2.0.0 + **/ + public function seek($key) + { + return isset($this->rows[$key]) ? $this->rows[$key] : false; + } + + /** + * Search for the given key/value pair, returning false if not found + * + * @return mixed + * @since 2.0.0 + **/ + public function search($key, $value) + { + foreach ($this->rows as $row) + { + if ($row->$key == $value) + { + return true; + } + } + + return false; + } + + /** + * Sorts the rows by a given field + * + * @param string $field The field to sort by + * @param bool $asc True if sort direction is ascending, false for descending + * @return $this + * @since 2.0.0 + **/ + public function sort($field, $asc = true) + { + usort($this->rows, function($a, $b) use ($field, $asc) + { + $result = strcmp($a->$field, $b->$field); + + return ($asc) ? $result : $result * -1; + }); + + return $this; + } + + /** + * Retrieves only the most recent applicable row + * + * @param string $limiter The column name to use to determine the latest row + * @return \Hubzero\Database\Relational|static + * @since 2.0.0 + **/ + public function latest($limiter = 'created') + { + return $this->sort($limiter, false)->first(); + } + + /** + * Saves a collection of models + * + * @return bool + * @since 2.0.0 + **/ + public function save() + { + if ($this->count()) + { + foreach ($this->rows as $model) + { + if (!$model->save()) + { + $this->setErrors($model->getErrors()); + return false; + } + } + } + + return true; + } + + /** + * Deletes all models in this collection + * + * @return bool + * @since 2.0.0 + **/ + public function destroyAll() + { + // @FIXME: could make this a single query... + if ($this->count()) + { + foreach ($this->rows as $model) + { + if (!$model->destroy()) + { + $this->setErrors($model->getErrors()); + return false; + } + } + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Database/Rules.php b/core/libraries/Hubzero/Database/Rules.php new file mode 100644 index 00000000000..bb8fb2ac0d1 --- /dev/null +++ b/core/libraries/Hubzero/Database/Rules.php @@ -0,0 +1,146 @@ + $v) + { + if (array_key_exists($k, $rules)) + { + // (Re)set rule variable + $rule = null; + + if (is_callable($rules[$k])) + { + if ($error = call_user_func_array($rules[$k], [$data])) + { + $errors[] = $error; + } + } + else if (strpos($rules[$k], '|')) + { + $rule = explode('|', $rules[$k]); + } + else + { + $rule = array($rules[$k]); + } + + if (isset($rule)) + { + foreach ($rule as $r) + { + if (method_exists(__CLASS__, $r)) + { + if ($error = self::$r($k, $v)) + { + $errors[] = $error; + } + } + } + } + } + } + + return (count($errors) > 0) ? $errors : true; + } + + /** + * Checks that var isn't empty + * + * @param string $key The field name + * @param mixed $var The field content + * @return bool|string + * @since 2.0.0 + **/ + private static function notempty($key, $var) + { + return !empty($var) ? false : "{$key} cannot be empty"; + } + + /** + * Checks that var is positive + * + * @param string $key The field name + * @param mixed $var The field content + * @return bool|string + * @since 2.0.0 + **/ + private static function positive($key, $var) + { + return ($var >= 0) ? false : "{$key} must be a positive integer"; + } + + /** + * Checks that var is non-zero + * + * @param string $key The field name + * @param mixed $var The field content + * @return bool|string + * @since 2.0.0 + **/ + private static function nonzero($key, $var) + { + return ($var > 0 || $var < 0) ? false : "{$key} cannot be zero"; + } + + /** + * Checks that var is alphabetical + * + * @param string $key The field name + * @param mixed $var The field content + * @return bool|string + * @since 2.0.0 + **/ + private static function alpha($key, $var) + { + return (preg_match('/^[[:alpha:] ]+$/', $var)) ? false : "{$key} can only contain alphabetical characters"; + } + + /** + * Checks that var is phone + * + * @param string $key The field name + * @param mixed $var The field content + * @return bool|string + * @since 2.0.0 + **/ + private static function phone($key, $var) + { + return (\Hubzero\Utility\Validate::phone($var)) ? false : "{$key} does not appear to be a valid phone number"; + } + + /** + * Checks that var is email + * + * @param string $key The field name + * @param mixed $var The field content + * @return bool|string + * @since 2.0.0 + **/ + private static function email($key, $var) + { + return (\Hubzero\Utility\Validate::email($var)) ? false : "{$key} does not appear to be a valid email address"; + } +} diff --git a/core/libraries/Hubzero/Database/Structure.php b/core/libraries/Hubzero/Database/Structure.php new file mode 100644 index 00000000000..5d107500ff2 --- /dev/null +++ b/core/libraries/Hubzero/Database/Structure.php @@ -0,0 +1,29 @@ +query($this->syntax->getColumnsQuery($table), 'rows'); + + return $this->syntax->normalizeColumns($columns, $typeOnly); + } +} diff --git a/core/libraries/Hubzero/Database/Syntax/Mariadb.php b/core/libraries/Hubzero/Database/Syntax/Mariadb.php new file mode 100644 index 00000000000..a089447c9a7 --- /dev/null +++ b/core/libraries/Hubzero/Database/Syntax/Mariadb.php @@ -0,0 +1,18 @@ +connection = $connection; + } + + /** + * Grabs the params bindings + * + * @return array + * @since 2.0.0 + **/ + public function getBindings() + { + return $this->bindings; + } + + /** + * Sets a new bind value + * + * @param string $value The value to bind + * @param string $type The value type + * @return $this + * @since 2.0.0 + **/ + public function bind($value, $type = null) + { + $this->bindings[] = $value; + } + + /** + * Empty select values + * + * @return void + * @since 2.2.2 + **/ + public function resetSelect() + { + $this->select = []; + } + + /** + * Sets a select element on the query + * + * @param string $column The column to select + * @param string $as What to call the return val + * @param bool $count Whether or not to count column + * @return void + * @since 2.0.0 + **/ + public function setSelect($column, $as = null, $count = false) + { + // A default * is often added, get rid of it if anything else is added + // This wouldn't get rid of table.* as that is likely added intentionally + if (isset($this->select[0]) && $this->select[0]['column'] == '*') + { + $this->resetSelect(); + } + + $this->select[] = [ + 'column' => $column, + 'as' => $as, + 'count' => $count + ]; + } + + /** + * Sets an insert element on the query + * + * @param string $table The table into which we will be inserting + * @param bool $ignore Whether or not to ignore errors produced related to things like duplicate keys + * @return void + * @since 2.0.0 + **/ + public function setInsert($table, $ignore = false) + { + $this->insert = $table; + $this->ignore = $ignore; + } + + /** + * Empty insert values + * + * @return void + * @since 2.2.15 + **/ + public function resetInsert() + { + $this->insert = ''; + $this->ignore = false; + } + + /** + * Sets an update element on the query + * + * @param string $table The table whose fields will be updated + * @return void + * @since 2.0.0 + **/ + public function setUpdate($table) + { + $this->update = $table; + } + + /** + * Empty update values + * + * @return void + * @since 2.2.15 + **/ + public function resetUpdate() + { + $this->update = ''; + } + + /** + * Sets a delete element on the query + * + * @param string $table The table whose row will be deleted + * @return void + * @since 2.0.0 + **/ + public function setDelete($table) + { + $this->delete = $table; + } + + /** + * Empty update values + * + * @return void + * @since 2.2.15 + **/ + public function resetDelete() + { + $this->delete = ''; + } + + /** + * Sets a from element on the query + * + * @param string $table The table of interest + * @param string $as What to call the table + * @return void + * @since 2.0.0 + **/ + public function setFrom($table, $as = null) + { + $this->from[] = [ + 'table' => $table, + 'as' => $as + ]; + } + + /** + * Empty from values + * + * @return void + * @since 2.2.15 + **/ + public function resetFrom() + { + $this->from = []; + } + + /** + * Sets a join element on the query + * + * @param string $table The table join + * @param string $leftKey The left side of the join condition + * @param string $rightKey The right side of the join condition + * @param string $type The join type to perform + * @return void + * @since 2.0.0 + **/ + public function setJoin($table, $leftKey, $rightKey, $type = 'inner') + { + $this->join[] = [ + 'table' => $table, + 'left' => $leftKey, + 'right' => $rightKey, + 'type' => $type + ]; + } + + /** + * Empty join values + * + * @return void + * @since 2.2.15 + **/ + public function resetJoin() + { + $this->join = []; + } + + /** + * Sets a join element on the query + * + * @param string $table The table join + * @param string $raw The join clause + * @param string $type The join type to perform + * @return $this + **/ + public function setRawJoin($table, $raw, $type = 'inner') + { + $this->join[] = [ + 'table' => $table, + 'raw' => $raw, + 'type' => $type + ]; + } + + /** + * Sets a set element on the query + * + * @param array $data The data to be modified + * @return void + * @since 2.0.0 + **/ + public function setSet($data) + { + $this->set = $data; + } + + /** + * Empty join values + * + * @return void + * @since 2.2.15 + **/ + public function resetSet() + { + $this->set = []; + } + + /** + * Sets a values element on the query + * + * @param array $data The data to be inserted + * @return void + * @since 2.0.0 + **/ + public function setValues($data) + { + $this->values = $data; + } + + /** + * Empty values + * + * @return void + * @since 2.2.15 + **/ + public function resetValues() + { + $this->values = []; + } + + /** + * Sets a group element on the query + * + * @param string $column The column on which to apply the group by + * @return void + * @since 2.0.0 + **/ + public function setGroup($column) + { + $this->group[] = $column; + } + + /** + * Empty group values + * + * @return void + * @since 2.2.15 + **/ + public function resetGroup() + { + $this->group = []; + } + + /** + * Sets a having element on the query + * + * @param string $column The column to which the clause will apply + * @param string $operator The operation that will compare column to value + * @param string $value The value to which the column will be evaluated + * @return void + * @since 2.0.0 + **/ + public function setHaving($column, $operator, $value) + { + $this->having[] = [ + 'column' => $column, + 'operator' => $operator, + 'value' => $value + ]; + } + + /** + * Empty having values + * + * @return void + * @since 2.2.15 + **/ + public function resetHaving() + { + $this->having = []; + } + + /** + * Sets a where element on the query + * + * @param string $column The column to which the clause will apply + * @param string $operator The operation that will compare column to value + * @param string $value The value to which the column will be evaluated + * @param string $logical The operator between multiple clauses + * @param int $depth The depth level of the clause, for sub clauses + * @return void + * @since 2.0.0 + **/ + public function setWhere($column, $operator, $value, $logical = 'and', $depth = 0) + { + $this->where[] = [ + 'column' => $column, + 'operator' => $operator, + 'value' => $value, + 'logical' => $logical, + 'depth' => $depth + ]; + } + + /** + * Sets a raw where element on the query + * + * @param string $raw The raw where clause + * @param array $bindings The clause bindings, if any + * @param string $logical The operator between multiple clauses + * @param int $depth The depth level of the clause, for sub clauses + * @return void + * @since 2.0.0 + **/ + public function setRawWhere($raw, $bindings = [], $logical = 'and', $depth = 0) + { + $this->where[] = [ + 'raw' => $raw, + 'bindings' => $bindings, + 'logical' => $logical, + 'depth' => $depth + ]; + } + + /** + * Sets the query clause depth + * + * @param int $depth The depth to set to + * @return void + * @since 2.1.0 + **/ + public function resetDepth($depth = 0) + { + $this->where[] = [ + 'resetdepth' => true, + 'depth' => $depth + ]; + } + + /** + * Empty where values + * + * @return void + * @since 2.2.15 + **/ + public function resetWhere() + { + $this->where = []; + } + + /** + * Sets a limit element on the query + * + * @param int $limit Number of results to return on next query + * @return void + * @since 2.0.0 + **/ + public function setLimit($limit) + { + if (!is_int($limit) || $limit < 0) + { + throw new InvalidArgumentException('Limit must be a whole integer of 0 or greater.'); + } + + $this->limit = $limit; + } + + /** + * Sets a start element on the query + * + * @param int $start Position to start from + * @return void + * @since 2.0.0 + **/ + public function setStart($start) + { + if (!is_int($start) || $start < 0) + { + throw new InvalidArgumentException('Start must be a whole integer of 0 or greater.'); + } + + $this->start = $start; + } + + /** + * Sets an order element on the query + * + * @param string $column The column to which the order by will apply + * @param string $dir The direction in which the results will be ordered + * @return void + * @since 2.0.0 + **/ + public function setOrder($column, $dir) + { + if (!in_array(strtolower($dir), ['asc', 'desc'])) + { + throw new InvalidArgumentException('Order direction must be one of ASC or DESC.'); + } + + $this->order[] = [ + 'column' => $column, + 'dir' => $dir + ]; + } + + /** + * Resets an order element on the query + * + * @return void + * @since 2.2.2 + **/ + public function resetOrder() + { + $this->order = []; + } + + /** + * Builds the given query element + * + * @param string $type The query element to build + * @return string + * @since 2.0.0 + **/ + public function build($type) + { + if (empty($this->{$type})) + { + return false; + } + + $method = 'build' . ucfirst($type); + + return $this->{$method}(); + } + + /** + * Builds a select statement from the set params + * + * @return string + * @since 2.0.0 + **/ + private function buildSelect() + { + $selects = []; + + foreach ($this->select as $select) + { + $string = ($select['count']) ? "COUNT({$select['column']})" : $select['column']; + + // See if we're including an alias + if (isset($select['as'])) + { + $string .= " AS {$select['as']}"; + } + + // @FIXME: not quoting name here because we could have a function here as well + // $selects[] = $this->connection->quoteName($string, $select['as']); + $selects[] = $string; + } + + return 'SELECT ' . implode(',', $selects); + } + + /** + * Builds an insert statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildInsert() + { + return 'INSERT ' . (($this->ignore) ? 'IGNORE ' : '') . 'INTO ' . $this->connection->quoteName($this->insert); + } + + /** + * Builds an update statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildUpdate() + { + return 'UPDATE ' . $this->connection->quoteName($this->update); + } + + /** + * Builds a delete statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildDelete() + { + return 'DELETE FROM ' . $this->connection->quoteName($this->delete); + } + + /** + * Builds a from statement from the set params + * + * @return string + * @since 2.0.0 + **/ + private function buildFrom() + { + $froms = []; + + foreach ($this->from as $from) + { + $string = $this->connection->quoteName($from['table']); + + // See if we're including an alias + if (isset($from['as'])) + { + $string .= ' AS ' . $this->connection->quoteName($from['as']); + } + + $froms[] = $string; + } + + return 'FROM ' . implode(',', $froms); + } + + /** + * Builds a join statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildJoin() + { + $joins = []; + + foreach ($this->join as $join) + { + if (isset($join['raw'])) + { + $joins[] = strtoupper($join['type']) . ' JOIN ' . $join['table'] . ' ON ' . $join['raw']; + } + else + { + $joins[] = strtoupper($join['type']) . ' JOIN ' . $join['table'] . ' ON ' . $join['left'] . ' = ' . $join['right']; + } + } + + return implode("\n", $joins); + } + + /** + * Builds a where statement from the set params + * + * @return string + * @since 2.0.0 + **/ + private function buildWhere() + { + $strings = []; + $first = true; + $depth = 0; + + foreach ($this->where as $constraint) + { + $string = ''; + + if (array_key_exists('resetdepth', $constraint)) + { + $string .= str_repeat(')', ($depth - $constraint['depth'])); + } + else + { + $string .= ($constraint['depth'] < $depth) ? ') ' : ''; + $string .= ($first) ? 'WHERE ' : strtoupper($constraint['logical']) . ' '; + $string .= ($constraint['depth'] > $depth) ? '(' : ''; + + // Make sure this isn't a 'raw' where clause + if (array_key_exists('raw', $constraint)) + { + $string .= $constraint['raw']; + + foreach ($constraint['bindings'] as $binding) + { + $this->bind($binding); + } + } + else + { + $string .= $this->connection->quoteName($constraint['column']); + $string .= ' ' . $constraint['operator']; + if (is_array($constraint['value'])) + { + $values = array(); + foreach ($constraint['value'] as $value) + { + $values[] = '?'; + $this->bind($value); + } + $string .= ' (' . ((!empty($values)) ? implode(',', $values) : "''") . ')'; + } + else + { + $string .= ' ?'; + $this->bind($constraint['value']); + } + } + } + + $strings[] = $string; + $first = false; + $depth = $constraint['depth']; + } + + // Catch instance where last item was at a greater depth and never got a closing ')' + if ($depth > 0) + { + $strings[] = str_repeat(')', $depth); + } + + return implode("\n", $strings); + } + + /** + * Builds a set statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildSet() + { + $updates = []; + + foreach ($this->set as $field => $value) + { + if (is_object($value)) + { + $updates[] = $this->connection->quoteName($field) . ' = ' . $value->build($this); + } + else + { + $updates[] = $this->connection->quoteName($field) . ' = ?'; + $this->bind(is_string($value) ? trim($value) : $value); + } + } + + return 'SET ' . implode(',', $updates); + } + + /** + * Builds a values statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildValues() + { + $fields = []; + $values = []; + + foreach ($this->values as $field => $value) + { + $fields[] = $this->connection->quoteName($field); + $values[] = '?'; + $this->bind(is_string($value) ? trim($value) : $value); + } + + return '(' . implode(',', $fields) . ') VALUES (' . implode(',', $values) . ')'; + } + + /** + * Builds a group statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildGroup() + { + return 'GROUP BY ' . implode(',', $this->group); + } + + /** + * Builds a having statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildHaving() + { + $havings = []; + + foreach ($this->having as $having) + { + $havings[] = $having['column'] . ' ' . $having['operator'] . ' ?'; + + $this->bind(is_string($having['value']) ? trim($having['value']) : $having['value']); + } + + return 'HAVING ' . implode(" AND ", $havings); + } + + /** + * Builds a limit statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildLimit() + { + $string = 'LIMIT '; + $string .= ((!empty($this->start)) ? (int)$this->start . ',' : ''); + $string .= ((!empty($this->limit)) ? (int)$this->limit : '18446744073709551615'); + + return $string; + } + + /** + * Builds an order statement from the set params + * + * @return string + * @since 2.0.0 + **/ + public function buildOrder() + { + $orders = []; + + foreach ($this->order as $order) + { + if (is_object($order['column'])) + { + $string = $order['column']->build($this); + } + else + { + $string = $this->connection->quoteName($order['column']); + } + + $string .= ' ' . strtoupper($order['dir']); + $orders[] = $string; + } + + return 'ORDER BY ' . implode(',', $orders); + } + + /** + * Returns the proper query for generating a list of table columns per this syntax + * + * @param string $table The name of the database table + * @return array + * @since 2.0.0 + */ + public function getColumnsQuery($table) + { + return 'SHOW FULL COLUMNS FROM ' . $this->connection->quoteName($table); + } + + /** + * Normalizes the results of the above query + * + * @param array $data The raw column data + * @param bool $typeOnly True (default) to only return field types + * @return array + * @since 2.0.0 + **/ + public function normalizeColumns($data, $typeOnly = true) + { + $results = []; + + // If we only want the type as the value add just that to the list + if ($typeOnly) + { + foreach ($data as $field) + { + // @FIXME: should we try to normalize types too? + $results[$field->Field] = $field->Type; + } + } + // If we want the whole field data object add that to the list + else + { + foreach ($data as $field) + { + $results[$field->Field] = + [ + 'name' => $field->Field, + 'type' => $field->Type, + 'allownull' => ($field->Null == 'NO') ? false : true, + 'default' => $field->Default, + 'pk' => ($field->Key == 'PRI') ? true : false + ]; + } + } + + return $results; + } +} diff --git a/core/libraries/Hubzero/Database/Syntax/Pgsql.php b/core/libraries/Hubzero/Database/Syntax/Pgsql.php new file mode 100644 index 00000000000..1238101a126 --- /dev/null +++ b/core/libraries/Hubzero/Database/Syntax/Pgsql.php @@ -0,0 +1,15 @@ +ignore) ? 'OR IGNORE ' : '') . 'INTO ' . $this->connection->quoteName($this->insert); + } + + /** + * Sets a join element on the query + * + * @param string $table The table join + * @param string $leftKey The left side of the join condition + * @param string $rightKey The right side of the join condition + * @param string $type The join type to perform + * @return void + * @throws Hubzero\Database\Exception\UnsupportedSyntaxException + * @since 2.2.15 + **/ + public function setJoin($table, $leftKey, $rightKey, $type = 'inner') + { + if (in_array($type, ['right', 'full', 'full outer'])) + { + throw new UnsupportedSyntaxException('RIGHT and FULL OUTER JOINs are not currently supported for SQLite', 500); + } + + parent::setJoin($table, $leftKey, $rightKey, $type); + } + + /** + * Sets a join element on the query + * + * @param string $table The table join + * @param string $raw The join clause + * @param string $type The join type to perform + * @throws Hubzero\Database\Exception\UnsupportedSyntaxException + * @return $this + **/ + public function setRawJoin($table, $raw, $type = 'inner') + { + if (in_array($type, ['right', 'full', 'full outer'])) + { + throw new UnsupportedSyntaxException('RIGHT and FULL OUTER JOINs are not currently supported for SQLite', 500); + } + + parent::setRawJoin($table, $raw, $type); + } + + /** + * Returns the proper query for generating a list of table columns per this syntax + * + * @param string $table The name of the database table + * @return array + * @since 2.0.0 + */ + public function getColumnsQuery($table) + { + return 'PRAGMA table_info(' . $this->connection->quoteName($table) . ')'; + } + + /** + * Normalizes the results of the above query + * + * @param array $data The raw column data + * @param bool $typeOnly True (default) to only return field types + * @return array + * @since 2.0.0 + **/ + public function normalizeColumns($data, $typeOnly = true) + { + $results = []; + + // If we only want the type as the value add just that to the list + if ($typeOnly) + { + foreach ($data as $field) + { + // @FIXME: should we try to normalize types too? + $results[$field->name] = $field->type; + } + } + // If we want the whole field data object add that to the list + else + { + foreach ($data as $field) + { + $results[$field->name] = + [ + 'name' => $field->name, + 'type' => $field->type, + 'allownull' => $field->notnull ? false : true, + 'default' => $field->dflt_value, + 'pk' => $field->pk ? true : false + ]; + } + } + + return $results; + } +} diff --git a/core/libraries/Hubzero/Database/Table.php b/core/libraries/Hubzero/Database/Table.php new file mode 100644 index 00000000000..e0788b0af83 --- /dev/null +++ b/core/libraries/Hubzero/Database/Table.php @@ -0,0 +1,1469 @@ +_tbl = $table; + $this->_tbl_key = $key; + $this->_db = $db; + + // Initialise the table properties. + if ($fields = $this->getFields()) + { + foreach ($fields as $name => $v) + { + // Add the field if it is not already present. + if (!property_exists($this, $name)) + { + $this->$name = null; + } + } + } + + // If we are tracking assets, make sure an access field exists and initially set the default. + if (property_exists($this, 'asset_id')) + { + $this->_trackAssets = true; + } + + // If the access property exists, set the default. + if (property_exists($this, 'access')) + { + $this->access = (int) \Config::get('access'); + } + } + + /** + * Get the columns from database table. + * + * @return mixed An array of the field names, or false if an error occurs. + * @since 2.1.12 + */ + public function getFields() + { + static $cache = null; + + if ($cache === null) + { + // Lookup the fields for this table only once. + $name = $this->_tbl; + $fields = $this->_db->getTableColumns($name, false); + + if (empty($fields)) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_COLUMNS_NOT_FOUND')); + $this->setError($e); + return false; + } + $cache = $fields; + } + + return $cache; + } + + /** + * Static method to get an instance of a Table class if it can be found in + * the table include paths. To add include paths for searching for Table + * classes @see self::addIncludePath(). + * + * @param string $type The type (name) of the Table class to get an instance of. + * @param string $prefix An optional prefix for the table class name. + * @param array $config An optional array of configuration values for the Table object. + * @return mixed A Table object if found or boolean false if one could not be found. + * @since 2.1.12 + */ + public static function getInstance($type, $prefix = 'Table', $config = array()) + { + // Sanitize and prepare the table class name. + $type = preg_replace('/[^A-Z0-9_\.-]/i', '', $type); + $tableClass = $prefix . ucfirst($type); + + // Only try to load the class if it doesn't already exist. + if (!class_exists($tableClass)) + { + // Search for the class file in the Table include paths. + $paths = self::addIncludePath(); + $pathIndex = 0; + while (!class_exists($tableClass) && $pathIndex < count($paths)) + { + if ($tryThis = \Filesystem::find($paths[$pathIndex++], strtolower($type) . '.php')) + { + // Import the class file. + include_once $tryThis; + } + } + if (!class_exists($tableClass)) + { + // If we were unable to find the class file in the Table include paths, raise a warning and return false. + return false; + } + } + + // If a database object was passed in the configuration array use it, otherwise get the global one from JFactory. + $db = isset($config['dbo']) ? $config['dbo'] : App::get('db'); + + // Instantiate a new table class and return it. + return new $tableClass($db); + } + + /** + * Add a filesystem path where Table should search for table class files. + * You may either pass a string or an array of paths. + * + * @param mixed $path A filesystem path or array of filesystem paths to add. + * @return array An array of filesystem paths to find Table classes in. + * @since 2.1.12 + */ + public static function addIncludePath($path = null) + { + // Declare the internal paths as a static variable. + static $_paths; + + // If the internal paths have not been initialised, do so with the base table path. + if (!isset($_paths)) + { + $_paths = array(__DIR__ . '/table'); + } + + // Convert the passed path(s) to add to an array. + settype($path, 'array'); + + // If we have new paths to add, do so. + if (!empty($path) && !in_array($path, $_paths)) + { + // Check and add each individual new path. + foreach ($path as $dir) + { + // Sanitize path. + $dir = trim($dir); + + // Add to the front of the list so that custom paths are searched first. + array_unshift($_paths, $dir); + } + } + + return $_paths; + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * @since 2.1.12 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + return $this->_tbl . '.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. In + * tracking the assets a title is kept for each asset so that there is some + * context available in a unified access manager. Usually this would just + * return $this->title or $this->name or whatever is being used for the + * primary name of the row. If this method is not overridden, the asset name is used. + * + * @return string The string to use as the title in the asset table. + * @since 2.1.12 + */ + protected function _getAssetTitle() + { + return $this->_getAssetName(); + } + + /** + * Method to get the parent asset under which to register this one. + * By default, all assets are registered to the ROOT node with ID, + * which will default to 1 if none exists. + * The extended class can define a table and id to lookup. If the + * asset does not exist it will be created. + * + * @param object $table A Table object for the asset parent. + * @param integer $id Id to look up + * @return integer + * @since 2.1.12 + */ + protected function _getAssetParentId($table = null, $id = null) + { + // For simple cases, parent to the asset root. + $rootId = \Hubzero\Access\Asset::getRootId(); + if (!empty($rootId)) + { + return $rootId; + } + + return 1; + } + + /** + * Method to get the database table name for the class. + * + * @return string The name of the database table being modeled. + */ + public function getTableName() + { + return $this->_tbl; + } + + /** + * Method to get the primary key field name for the table. + * + * @return string The name of the primary key for the table. + * @since 2.1.12 + */ + public function getKeyName() + { + return $this->_tbl_key; + } + + /** + * Method to get the Database connector object. + * + * @return object The internal database connector object. + * @since 2.1.12 + */ + public function getDbo() + { + return $this->_db; + } + + /** + * Method to set the Database connector object. + * + * @param object &$db A Database connector object to be used by the table object. + * @return boolean True on success. + * @since 2.1.12 + */ + public function setDBO(&$db) + { + // Make sure the new database object is a Database. + if (!($db instanceof \Hubzero\Database\Driver)) + { + return false; + } + + $this->_db = &$db; + + return true; + } + + /** + * Method to set rules for the record. + * + * @param mixed $input A Hubzero\Access\Rules object, JSON string, or array. + * @return void + * @since 2.1.12 + */ + public function setRules($input) + { + if ($input instanceof \Hubzero\Access\Rules) + { + $this->_rules = $input; + } + else + { + $this->_rules = new \Hubzero\Access\Rules($input); + } + } + + /** + * Method to get the rules for the record. + * + * @return object + * @since 2.1.12 + */ + public function getRules() + { + return $this->_rules; + } + + /** + * Method to reset class properties to the defaults set in the class + * definition. It will ignore the primary key as well as any private class + * properties. + * + * @return void + * @since 2.1.12 + */ + public function reset() + { + // Get the default values for the class from the table. + foreach ($this->getFields() as $k => $v) + { + // If the property is not the primary key or private, reset it. + if ($k != $this->_tbl_key && (strpos($k, '_') !== 0)) + { + $this->$k = $v->Default; + } + } + } + + /** + * Method to bind an associative array or object to the Table instance.This + * method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param mixed $src An associative array or object to bind to the Table instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * @return boolean True on success. + * @since 2.1.12 + */ + public function bind($src, $ignore = array()) + { + // If the source value is not an array or object return false. + if (!is_object($src) && !is_array($src)) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_BIND_FAILED_INVALID_SOURCE_ARGUMENT', get_class($this))); + $this->setError($e); + return false; + } + + // If the source value is an object, get its accessible properties. + if (is_object($src)) + { + $src = get_object_vars($src); + } + + // If the ignore value is a string, explode it over spaces. + if (!is_array($ignore)) + { + $ignore = explode(' ', $ignore); + } + + // Bind the source value, excluding the ignored fields. + foreach ($this->getProperties() as $k => $v) + { + // Only process fields not in the ignore array. + if (!in_array($k, $ignore)) + { + if (isset($src[$k])) + { + $this->$k = $src[$k]; + } + } + } + + return true; + } + + /** + * Method to load a row from the database by primary key and bind the fields + * to the Table instance properties. + * + * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. If not + * set the instance property value is used. + * @param boolean $reset True to reset the default values before loading the new row. + * @return boolean True if successful. False if row not found or on error (internal error state set in that case). + * @since 2.1.12 + */ + public function load($keys = null, $reset = true) + { + if (empty($keys)) + { + // If empty, use the value of the current key + $keyName = $this->_tbl_key; + $keyValue = $this->$keyName; + + // If empty primary key there's is no need to load anything + if (empty($keyValue)) + { + return true; + } + + $keys = array($keyName => $keyValue); + } + elseif (!is_array($keys)) + { + // Load by primary key. + $keys = array($this->_tbl_key => $keys); + } + + if ($reset) + { + $this->reset(); + } + + // Initialise the query. + $query = $this->_db->getQuery(); + $query->select('*'); + $query->from($this->_tbl); + $fields = array_keys($this->getProperties()); + + foreach ($keys as $field => $value) + { + // Check that $field is in the table. + if (!in_array($field, $fields)) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_CLASS_IS_MISSING_FIELD', get_class($this), $field)); + $this->setError($e); + return false; + } + // Add the search tuple to the query. + $query->whereEquals($field, $value); + } + + $this->_db->setQuery($query->toString()); + + try + { + $row = $this->_db->loadAssoc(); + } + catch (Exception $e) + { + $je = new Exception($e->getMessage()); + $this->setError($je); + return false; + } + + // Check that we have a result. + if (empty($row)) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_EMPTY_ROW_RETURNED')); + $this->setError($e); + return false; + } + + // Bind the object with the row and return. + return $this->bind($row); + } + + /** + * Method to perform sanity checks on the Table instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * @since 2.1.12 + */ + public function check() + { + return true; + } + + /** + * Method to store a row in the database from the Table instance properties. + * If a primary key value is set the row with that primary key value will be + * updated with the instance property values. If no primary key value is set + * a new row will be inserted into the database with the properties from the + * Table instance. + * + * @param boolean $updateNulls True to update fields even if they are null. + * @return boolean True on success. + * @since 2.1.12 + */ + public function store($updateNulls = false) + { + $currentAssetId = null; + + // Initialise variables. + $k = $this->_tbl_key; + if (!empty($this->asset_id)) + { + $currentAssetId = $this->asset_id; + } + + // The asset id field is managed privately by this class. + if ($this->_trackAssets) + { + unset($this->asset_id); + } + + // If a primary key exists update the object, otherwise insert it. + if ($this->$k) + { + $stored = $this->_db->updateObject($this->_tbl, $this, $this->_tbl_key, $updateNulls); + } + else + { + $stored = $this->_db->insertObject($this->_tbl, $this, $this->_tbl_key); + \Event::trigger($this->getTableName() . '_new', ['table' => $this]); + } + + // If the store failed return false. + if (!$stored) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_STORE_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + return false; + } + + \Event::trigger('system.onContentSave', array($this->getTableName(), $this)); + + // If the table is not set to track assets return true. + if (!$this->_trackAssets) + { + return true; + } + + if ($this->_locked) + { + $this->_unlock(); + } + + // + // Asset Tracking + // + + $parentId = $this->_getAssetParentId(); + $name = $this->_getAssetName(); + $title = $this->_getAssetTitle(); + + $asset = \Hubzero\Access\Asset::oneByName($name); + + // Re-inject the asset id. + $this->asset_id = $asset->get('id'); + + // Check for an error. + if ($error = $asset->getError()) + { + $this->setError($error); + return false; + } + + // Specify how a new or moved node asset is inserted into the tree. + if (empty($this->asset_id) || $asset->get('parent_id') != $parentId) + { + $asset->setLocation($parentId, 'last-child'); + } + + // Prepare the asset to be stored. + $asset->set('parent_id', $parentId); + $asset->set('name', $name); + $asset->set('title', $title); + + if ($this->_rules instanceof \Hubzero\Access\Rules) + { + $asset->set('rules', (string) $this->_rules); + } + + if (!$asset->save()) + { + $this->setError($asset->getError()); + return false; + } + + // Create an asset_id or heal one that is corrupted. + if (empty($this->asset_id) || ($currentAssetId != $this->asset_id && !empty($this->asset_id))) + { + // Update the asset_id field in this table. + $this->asset_id = (int) $asset->get('id'); + + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array( + 'asset_id' => (int) $this->asset_id + )); + $query->whereEquals($k, (int) $this->$k); + $this->_db->setQuery($query->toString()); + + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_STORE_FAILED_UPDATE_ASSET_ID', $this->_db->getErrorMsg())); + $this->setError($e); + return false; + } + } + + return true; + } + + /** + * Method to provide a shortcut to binding, checking and storing a Table + * instance to the database table. The method will check a row in once the + * data has been stored and if an ordering filter is present will attempt to + * reorder the table rows based on the filter. The ordering filter is an instance + * property name. The rows that will be reordered are those whose value matches + * the Table instance for the property specified. + * + * @param mixed $src An associative array or object to bind to the Table instance. + * @param string $orderingFilter Filter for the order updating + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * @return boolean True on success. + * @since 2.1.12 + */ + public function save($src, $orderingFilter = '', $ignore = '') + { + // Attempt to bind the source to the instance. + if (!$this->bind($src, $ignore)) + { + return false; + } + + // Run any sanity checks on the instance and verify that it is ready for storage. + if (!$this->check()) + { + return false; + } + + // Attempt to store the properties to the database table. + if (!$this->store()) + { + return false; + } + + // Attempt to check the row in, just in case it was checked out. + if (!$this->checkin()) + { + return false; + } + + // If an ordering filter is set, attempt reorder the rows in the table based on the filter and value. + if ($orderingFilter) + { + $filterValue = $this->$orderingFilter; + $this->reorder($orderingFilter ? $this->_db->quoteName($orderingFilter) . ' = ' . $this->_db->Quote($filterValue) : ''); + } + + // Set the error to empty and return true. + $this->setError(''); + + return true; + } + + /** + * Method to delete a row from the database table by primary key value. + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * @return boolean True on success. + * @since 2.1.12 + */ + public function delete($pk = null) + { + // Initialise variables. + $k = $this->_tbl_key; + $pk = (is_null($pk)) ? $this->$k : $pk; + + // If no primary key is given, return false. + if ($pk === null) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_NULL_PRIMARY_KEY')); + $this->setError($e); + return false; + } + + // If tracking assets, remove the asset first. + if ($this->_trackAssets) + { + // Get and the asset name. + $this->$k = $pk; + $name = $this->_getAssetName(); + $asset = self::getInstance('Asset'); + + if ($asset->loadByName($name)) + { + if (!$asset->delete()) + { + $this->setError($asset->getError()); + return false; + } + } + else + { + $this->setError($asset->getError()); + // [!] Hubzero - Record doesn't exist. Since we're + // deleting entries, it shouldn't matter. + //return false; + } + } + + // Delete the row by primary key. + $query = $this->_db->getQuery(); + $query->delete($this->_tbl); + $query->whereEquals($this->_tbl_key, $pk); + $this->_db->setQuery($query->toString()); + + // Check for a database error. + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_DELETE_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + return false; + } + + return true; + } + + /** + * Method to check a row out if the necessary properties/fields exist. To + * prevent race conditions while editing rows in a database, a row can be + * checked out if the fields 'checked_out' and 'checked_out_time' are available. + * While a row is checked out, any attempt to store the row by a user other + * than the one who checked the row out should be held until the row is checked + * in again. + * + * @param integer $userId The Id of the user checking out the row. + * @param mixed $pk An optional primary key value to check out. If not set the instance property value is used. + * @return boolean True on success. + * @since 2.1.12 + */ + public function checkOut($userId, $pk = null) + { + // If there is no checked_out or checked_out_time field, just return true. + if (!property_exists($this, 'checked_out') || !property_exists($this, 'checked_out_time')) + { + return true; + } + + // Initialise variables. + $k = $this->_tbl_key; + $pk = (is_null($pk)) ? $this->$k : $pk; + + // If no primary key is given, return false. + if ($pk === null) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_NULL_PRIMARY_KEY')); + $this->setError($e); + return false; + } + + // Get the current time in MySQL format. + $time = \Date::of('now')->toSql(); + + // Check the row out by primary key. + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array( + 'checked_out' => (int) $userId, + 'checked_out_time' => $time + )); + $query->whereEquals($this->_tbl_key, $pk); + $this->_db->setQuery($query->toString()); + + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_CHECKOUT_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + return false; + } + + // Set table values in the object. + $this->checked_out = (int) $userId; + $this->checked_out_time = $time; + + return true; + } + + /** + * Method to check a row in if the necessary properties/fields exist. Checking + * a row in will allow other users the ability to edit the row. + * + * @param mixed $pk An optional primary key value to check out. If not set the instance property value is used. + * @return boolean True on success. + * @since 2.1.12 + */ + public function checkIn($pk = null) + { + // If there is no checked_out or checked_out_time field, just return true. + if (!property_exists($this, 'checked_out') || !property_exists($this, 'checked_out_time')) + { + return true; + } + + // Initialise variables. + $k = $this->_tbl_key; + $pk = (is_null($pk)) ? $this->$k : $pk; + + // If no primary key is given, return false. + if ($pk === null) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_NULL_PRIMARY_KEY')); + $this->setError($e); + return false; + } + + $columns = $this->getFields(); + + $data = []; + foreach ($columns as $name => $column) + { + // We want to get the default values from the + // table's schema, rather than assuming + if ($name == 'checked_out_time' || $name == 'checked_out') + { + $data[$name] = $column->Default; + } + } + + // Check the row in by primary key. + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array( + 'checked_out' => $data['checked_out'], + 'checked_out_time' => $data['checked_out_time'] + )); + $query->whereEquals($this->_tbl_key, $pk); + $this->_db->setQuery($query->toString()); + + // Check for a database error. + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_CHECKIN_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + return false; + } + + // Set table values in the object. + $this->checked_out = $data['checked_out']; + $this->checked_out_time = $data['checked_out_time']; + + return true; + } + + /** + * Method to increment the hits for a row if the necessary property/field exists. + * + * @param mixed $pk An optional primary key value to increment. If not set the instance property value is used. + * @return boolean True on success. + * @since 2.1.12 + */ + public function hit($pk = null) + { + // If there is no hits field, just return true. + if (!property_exists($this, 'hits')) + { + return true; + } + + // Initialise variables. + $k = $this->_tbl_key; + $pk = (is_null($pk)) ? $this->$k : $pk; + + // If no primary key is given, return false. + if ($pk === null) + { + return false; + } + + // Check the row in by primary key. + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array( + 'hits' => new \Hubzero\Database\Value\Raw('(hits + 1)') + )); + $query->whereEquals($this->_tbl_key, $pk); + $this->_db->setQuery($query->toString()); + + // Check for a database error. + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_HIT_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + return false; + } + + // Set table values in the object. + $this->hits++; + + return true; + } + + /** + * Method to determine if a row is checked out and therefore uneditable by + * a user. If the row is checked out by the same user, then it is considered + * not checked out -- as the user can still edit it. + * + * @param integer $with The userid to preform the match with, if an item is checked out by this user the function will return false. + * @param integer $against The userid to perform the match against when the function is used as a static function. + * @return boolean True if checked out. + * @since 2.1.12 + * @todo This either needs to be static or not. + */ + public function isCheckedOut($with = 0, $against = null) + { + // Handle the non-static case. + if (isset($this) && ($this instanceof Table) && is_null($against)) + { + $against = $this->get('checked_out'); + } + + // The item is not checked out or is checked out by the same user. + if (!$against || ($against == $with)) + { + return false; + } + + $db = App::get('db'); + $db->setQuery('SELECT COUNT(userid)' . ' FROM ' . $db->quoteName('#__session') . ' WHERE ' . $db->quoteName('userid') . ' = ' . (int) $against); + $checkedOut = (boolean) $db->loadResult(); + + // If a session exists for the user then it is checked out. + return $checkedOut; + } + + /** + * Method to get the next ordering value for a group of rows defined by an SQL WHERE clause. + * This is useful for placing a new item last in a group of items in the table. + * + * @param string $where WHERE clause to use for selecting the MAX(ordering) for the table. + * @return mixed Boolean false an failure or the next ordering value as an integer. + * @since 2.1.12 + */ + public function getNextOrder($where = '') + { + // If there is no ordering field set an error and return false. + if (!property_exists($this, 'ordering')) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_CLASS_DOES_NOT_SUPPORT_ORDERING', get_class($this))); + $this->setError($e); + return false; + } + + // Get the largest ordering value for a given where clause. + $query = $this->_db->getQuery(); + $query->select('MAX(ordering)'); + $query->from($this->_tbl); + + if ($where) + { + $query->whereRaw($where); + } + + $this->_db->setQuery($query->toString()); + $max = (int) $this->_db->loadResult(); + + // Check for a database error. + if ($this->_db->getErrorNum()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_GET_NEXT_ORDER_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + + return false; + } + + // Return the largest ordering value + 1. + return ($max + 1); + } + + /** + * Method to compact the ordering values of rows in a group of rows + * defined by an SQL WHERE clause. + * + * @param string $where WHERE clause to use for limiting the selection of rows to compact the ordering values. + * @return mixed Boolean true on success. + * @since 2.1.12 + */ + public function reorder($where = '') + { + // If there is no ordering field set an error and return false. + if (!property_exists($this, 'ordering')) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_CLASS_DOES_NOT_SUPPORT_ORDERING', get_class($this))); + $this->setError($e); + return false; + } + + // Initialise variables. + $k = $this->_tbl_key; + + // Get the primary keys and ordering values for the selection. + $query = $this->_db->getQuery(); + $query->select($this->_tbl_key); + $query->select('ordering'); + $query->from($this->_tbl); + $query->where('ordering', '>=', '0'); + $query->order('ordering', 'asc'); + + // Setup the extra where and ordering clause data. + if ($where) + { + $query->whereRaw($where); + } + + $this->_db->setQuery($query->toString()); + $rows = $this->_db->loadObjectList(); + + // Check for a database error. + if ($this->_db->getErrorNum()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_REORDER_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + + return false; + } + + // Compact the ordering values. + foreach ($rows as $i => $row) + { + // Make sure the ordering is a positive integer. + if ($row->ordering >= 0) + { + // Only update rows that are necessary. + if ($row->ordering != $i + 1) + { + // Update the row ordering field. + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array( + 'ordering' => ($i + 1) + )); + $query->whereEquals($this->_tbl_key, $row->$k); + $this->_db->setQuery($query->toString()); + + // Check for a database error. + if (!$this->_db->execute()) + { + $e = new Exception( + Lang::txt('JLIB_DATABASE_ERROR_REORDER_UPDATE_ROW_FAILED', get_class($this), $i, $this->_db->getErrorMsg()) + ); + $this->setError($e); + + return false; + } + } + } + } + + return true; + } + + /** + * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause. + * Negative numbers move the row up in the sequence and positive numbers move it down. + * + * @param integer $delta The direction and magnitude to move the row in the ordering sequence. + * @param string $where WHERE clause to use for limiting the selection of rows to compact the ordering values. + * @return mixed Boolean true on success. + * @since 2.1.12 + */ + public function move($delta, $where = '') + { + // If there is no ordering field set an error and return false. + if (!property_exists($this, 'ordering')) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_CLASS_DOES_NOT_SUPPORT_ORDERING', get_class($this))); + $this->setError($e); + return false; + } + + // If the change is none, do nothing. + if (empty($delta)) + { + return true; + } + + // Initialise variables. + $k = $this->_tbl_key; + $row = null; + $query = $this->_db->getQuery(); + + // Select the primary key and ordering values from the table. + $query->select($this->_tbl_key); + $query->select('ordering'); + $query->from($this->_tbl); + + // If the movement delta is negative move the row up. + if ($delta < 0) + { + $query->where('ordering', '<', (int) $this->ordering); + $query->order('ordering', 'DESC'); + } + // If the movement delta is positive move the row down. + elseif ($delta > 0) + { + $query->where('ordering', '>', (int) $this->ordering); + $query->order('ordering', 'ASC'); + } + + // Add the custom WHERE clause if set. + if ($where) + { + $query->whereRaw($where); + } + + // Select the first row with the criteria. + $this->_db->setQuery($query->toString(), 0, 1); + $row = $this->_db->loadObject(); + + // If a row is found, move the item. + if (!empty($row)) + { + // Update the ordering field for this instance to the row's ordering value. + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array('ordering' => (int) $row->ordering)); + $query->whereEquals($this->_tbl_key, $this->$k); + $this->_db->setQuery($query->toString()); + + // Check for a database error. + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_MOVE_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + + return false; + } + + // Update the ordering field for the row to this instance's ordering value. + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array('ordering' => (int) $this->ordering)); + $query->whereEquals($this->_tbl_key, $row->$k); + $this->_db->setQuery($query->toString()); + + // Check for a database error. + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_MOVE_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + + return false; + } + + // Update the instance value. + $this->ordering = $row->ordering; + } + else + { + // Update the ordering field for this instance. + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array('ordering' => (int) $this->ordering)); + $query->whereEquals($this->_tbl_key, $this->$k); + $this->_db->setQuery($query->toString()); + + // Check for a database error. + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_MOVE_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + + return false; + } + } + + return true; + } + + /** + * Method to set the publishing state for a row or list of rows in the database + * table. The method respects checked out rows by other users and will attempt + * to checkin rows that it can after adjustments are made. + * + * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used. + * @param integer $state The publishing state. eg. [0 = unpublished, 1 = published] + * @param integer $userId The user id of the user performing the operation. + * @return boolean True on success. + * @since 2.1.12 + */ + public function publish($pks = null, $state = 1, $userId = 0) + { + // Initialise variables. + $k = $this->_tbl_key; + + // Sanitize input. + \Hubzero\Utility\Arr::toInteger($pks); + $userId = (int) $userId; + $state = (int) $state; + + // If there are no primary keys set check to see if the instance key is set. + if (empty($pks)) + { + if ($this->$k) + { + $pks = array($this->$k); + } + // Nothing to set publishing state on, return false. + else + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); + $this->setError($e); + + return false; + } + } + + // Update the publishing state for rows with the given primary keys. + $query = $this->_db->getQuery(); + $query->update($this->_tbl); + $query->set(array('published' => (int) $state)); + + // Determine if there is checkin support for the table. + if (property_exists($this, 'checked_out') || property_exists($this, 'checked_out_time')) + { + $query->whereEquals('checked_out', 0, 1) + ->orWhereEquals('checked_out', (int) $userId, 1) + ->resetDepth(); + $checkin = true; + } + else + { + $checkin = false; + } + + // Build the WHERE clause for the primary keys. + $query->whereRaw($k . ' = ' . implode(' OR ' . $k . ' = ', $pks)); + + $this->_db->setQuery($query->toString()); + + // Check for a database error. + if (!$this->_db->execute()) + { + $e = new Exception(Lang::txt('JLIB_DATABASE_ERROR_PUBLISH_FAILED', get_class($this), $this->_db->getErrorMsg())); + $this->setError($e); + + return false; + } + + // If checkin is supported and all rows were adjusted, check them in. + if ($checkin && (count($pks) == $this->_db->getAffectedRows())) + { + // Checkin the rows. + foreach ($pks as $pk) + { + $this->checkin($pk); + } + } + + // If the Table instance value is in the list of primary keys that were set, set the instance. + if (in_array($this->$k, $pks)) + { + $this->published = $state; + } + + $this->setError(''); + return true; + } + + /** + * Generic check for whether dependencies exist for this object in the database schema + * + * Can be overloaded/supplemented by the child class + * + * @param mixed $pk An optional primary key value check the row for. If not + * set the instance property value is used. + * @param array $joins An optional array to compiles standard joins formatted like: + * [label => 'Label', name => 'table name' , idfield => 'field', joinfield => 'field'] + * @return boolean True on success. + * @deprecated 2.1.12 + * @since 2.1.12 + */ + public function canDelete($pk = null, $joins = null) + { + // Deprecation warning. + Log::debug('Hubzero\Database\Table::canDelete() is deprecated.'); + + // Initialise variables. + $k = $this->_tbl_key; + $pk = (is_null($pk)) ? $this->$k : $pk; + + // If no primary key is given, return false. + if ($pk === null) + { + return false; + } + + if (is_array($joins)) + { + // Get a query object. + $query = $this->_db->getQuery(); + + // Setup the basic query. + $query->select($this->_tbl_key); + $query->from($this->_tbl); + $query->whereEquals($this->_tbl_key, $this->$k); + $query->group($this->_tbl_key); + + // For each join add the select and join clauses to the query object. + foreach ($joins as $table) + { + $query->select('COUNT(DISTINCT ' . $table['idfield'] . ')', $table['idfield']); + $query->join($table['name'], $table['joinfield'], $k, 'left'); + } + + // Get the row object from the query. + $this->_db->setQuery((string) $query->toString(), 0, 1); + $row = $this->_db->loadObject(); + + // Check for a database error. + if ($this->_db->getErrorNum()) + { + $this->setError($this->_db->getErrorMsg()); + + return false; + } + + $msg = array(); + $i = 0; + + foreach ($joins as $table) + { + $k = $table['idfield'] . $i; + + if ($row->$k) + { + $msg[] = Lang::txt($table['label']); + } + + $i++; + } + + if (count($msg)) + { + $this->setError("noDeleteRecord" . ": " . implode(', ', $msg)); + + return false; + } + else + { + return true; + } + } + + return true; + } + + /** + * Method to export the Table instance properties to an XML string. + * + * @param boolean $mapKeysToText True to map foreign keys to text values. + * @return string XML string representation of the instance. + * @deprecated 2.1.12 + * @since 2.1.12 + */ + public function toXML($mapKeysToText = false) + { + // Deprecation warning. + Log::debug('Hubzero\Database\Table::toXML() is deprecated.'); + + // Initialise variables. + $xml = array(); + $map = $mapKeysToText ? ' mapkeystotext="true"' : ''; + + // Open root node. + $xml[] = ''; + + // Get the publicly accessible instance properties. + foreach (get_object_vars($this) as $k => $v) + { + // If the value is null or non-scalar, or the field is internal ignore it. + if (!is_scalar($v) || ($v === null) || ($k[0] == '_')) + { + continue; + } + + $xml[] = ' <' . $k . '>'; + } + + // Close root node. + $xml[] = ''; + + // Return the XML array imploded over new lines. + return implode("\n", $xml); + } + + /** + * Method to lock the database table for writing. + * + * @return boolean True on success. + * @since 2.1.12 + * @throws Exception + */ + protected function _lock() + { + $this->_db->lockTable($this->_tbl); + $this->_locked = true; + + return true; + } + + /** + * Method to unlock the database table for writing. + * + * @return boolean True on success. + * @since 2.1.12 + */ + protected function _unlock() + { + $this->_db->unlockTables(); + $this->_locked = false; + + return true; + } +} diff --git a/core/libraries/Hubzero/Database/Tests/ErrorBagTraitTest.php b/core/libraries/Hubzero/Database/Tests/ErrorBagTraitTest.php new file mode 100644 index 00000000000..8dba84fc818 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/ErrorBagTraitTest.php @@ -0,0 +1,81 @@ +obj = $this->getObjectForTrait('Hubzero\Database\Traits\ErrorBag'); + + parent::setUp(); + } + + /** + * Test ErrorBag methods + * + * @covers \Hubzero\Database\Traits\ErrorBag::addError + * @covers \Hubzero\Database\Traits\ErrorBag::setErrors + * @covers \Hubzero\Database\Traits\ErrorBag::getError + * @covers \Hubzero\Database\Traits\ErrorBag::getErrors + * @return void + **/ + public function testErrorBag() + { + // Test that an array is returned + $errors = $this->obj->getErrors(); + + // Test that the array is empty + $this->assertTrue(is_array($errors)); + $this->assertCount(0, $errors); + + // Set some errors + $this->obj->addError('Donec sed odio dui.'); + $this->obj->addError(new Exception('Aenean lacinia bibendum.')); + $this->obj->addError('Nulla sed consectetur.'); + + // Get the list of set errors + $errors = $this->obj->getErrors(); + + // Make sure: + // - the list of errors matches the number of errors set + // - getError() returns the first error set + $this->assertCount(3, $errors); + $this->assertEquals($this->obj->getError(), 'Donec sed odio dui.'); + + // Test setting the entire list + $newerrors = array( + 'Integer posuere erat', + 'Ante venenatis dapibus', + 'Posuere velit aliquet.' + ); + + $this->obj->setErrors($newerrors); + + $this->assertEquals($this->obj->getErrors(), $newerrors); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/seed.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/seed.xml new file mode 100644 index 00000000000..ab1a0cebe21 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/seed.xml @@ -0,0 +1,354 @@ + + + + id + user_id + text + + 1 + 1 + This is my bio about me. + +
+ + id + name + lft + rgt + parent_id + level +
+ + id + user_id + post_id + content + + 1 + 1 + 1 + This is the greatest thing ever! + + + 2 + 1 + 1 + Actually, I take that back. + + + 3 + 2 + 1 + You can't take it back! + +
+ + id + user_id + title + content + lft + rgt + parent_id + level + scope + scope_id + + 1 + 3 + My first discussion + Tell me everything! + 0 + 3 + 0 + 0 + group + 1 + + + 2 + 3 + Confused + Is this really a good idea? + 1 + 2 + 1 + 1 + group + 1 + +
+ + id + name + + 1 + Test Group + + + 2 + Awesome Group + +
+ + id + user_id + scope + scope_id + + 1 + 1 + group + 1 + + + 2 + 2 + group + 1 + + + 3 + 3 + group + 1 + + + 4 + 1 + project + 1 + + + 5 + 3 + project + 1 + + + 6 + 4 + project + 1 + + + 7 + 1 + project + 2 + + + 8 + 4 + project + 2 + +
+ + id + permission_id + scope + scope_id + permitted + + 1 + 1 + group + 1 + + + + 2 + 1 + project + 1 + + + + 3 + 2 + member + 1 + + + + 4 + 2 + group + 1 + + + + 5 + 1 + group + 2 + + + + 6 + 3 + member + 1 + + + + 7 + 1 + member + 2 + + + + 8 + 1 + member + 3 + + + + 9 + 1 + project + 2 + + + + 10 + 2 + project + 1 + + + + 11 + 3 + project + 3 + + +
+ + id + name + + 1 + read + + + 2 + write + + + 3 + execute + +
+ + id + post_id + tag_id + tagged + + 1 + 1 + 1 + + + + 2 + 1 + 2 + + + + 3 + 1 + 3 + + + + 4 + 2 + 1 + + +
+ + id + user_id + content + + 1 + 1 + This is why I want to be a teacher. + + + 2 + 1 + This is why I want to be a computer programmer. + + + 3 + 2 + This is why I don't have Facebook. + +
+ + id + name + + 1 + Fix Stuff + + + 2 + Break Stuff + + + 3 + Make new Stuff + +
+ + id + name + + 1 + fun stuff + + + 2 + smart stuff + + + 3 + important stuff + +
+ + id + name + email + + 1 + Test User + testuser@gmail.com + + + 2 + Admin + admin@hub.com + + + 3 + You + you@you.com + + + 4 + Me + me@me.com + +
+
\ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/test.sqlite3 b/core/libraries/Hubzero/Database/Tests/Fixtures/test.sqlite3 new file mode 100644 index 00000000000..2c622fcc9d4 Binary files /dev/null and b/core/libraries/Hubzero/Database/Tests/Fixtures/test.sqlite3 differ diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussions.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussions.xml new file mode 100644 index 00000000000..c4caff9ef3f --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussions.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDelete.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDelete.xml new file mode 100644 index 00000000000..6abcada5be2 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDelete.xml @@ -0,0 +1,63 @@ + + + + + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDeleteParent.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDeleteParent.xml new file mode 100644 index 00000000000..ee40d481b31 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDeleteParent.xml @@ -0,0 +1,39 @@ + + + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNode.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNode.xml new file mode 100644 index 00000000000..fc993b0956a --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNode.xml @@ -0,0 +1,39 @@ + + + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNodeByParentId.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNodeByParentId.xml new file mode 100644 index 00000000000..b32aca30615 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNodeByParentId.xml @@ -0,0 +1,51 @@ + + + + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewFirstChild.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewFirstChild.xml new file mode 100644 index 00000000000..2b6d579dab0 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewFirstChild.xml @@ -0,0 +1,63 @@ + + + + + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewRootNode.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewRootNode.xml new file mode 100644 index 00000000000..f6be387c032 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewRootNode.xml @@ -0,0 +1,75 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedUsers.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedUsers.xml new file mode 100644 index 00000000000..baac409fd66 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedUsers.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Bio.php b/core/libraries/Hubzero/Database/Tests/Mock/Bio.php new file mode 100644 index 00000000000..35af1a8904e --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Mock/Bio.php @@ -0,0 +1,19 @@ +oneShiftsToMany('Member'); + } + + /** + * Many shifts to many relationship with permissions + * + * @return \Hubzero\Database\Relationship\ManyShiftsToMany + **/ + public function permissions() + { + return $this->manyShiftsToMany('Permission'); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Member.php b/core/libraries/Hubzero/Database/Tests/Mock/Member.php new file mode 100644 index 00000000000..11a99e1572d --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Mock/Member.php @@ -0,0 +1,38 @@ +shifter(); + } + + /** + * Many shifts to many relationship with permissions + * + * @return \Hubzero\Database\Relationship\ManyShiftsToMany + **/ + public function permissions() + { + return $this->manyShiftsToMany('Permission'); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Permission.php b/core/libraries/Hubzero/Database/Tests/Mock/Permission.php new file mode 100644 index 00000000000..4bedeb2162c --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Mock/Permission.php @@ -0,0 +1,19 @@ +belongsToOne('Hubzero\Database\Tests\Mock\User'); + } + + /** + * Many to many relationship with tags + * + * @return \Hubzero\Database\Relationship\ManyToMany + **/ + public function tags() + { + return $this->manyToMany('Tag'); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Project.php b/core/libraries/Hubzero/Database/Tests/Mock/Project.php new file mode 100644 index 00000000000..64c183b7c96 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Mock/Project.php @@ -0,0 +1,38 @@ +oneShiftsToMany('Member'); + } + + /** + * Many shifts to many relationship with permissions + * + * @return \Hubzero\Database\Relationship\ManyShiftsToMany + **/ + public function permissions() + { + return $this->manyShiftsToMany('Permission'); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Tag.php b/core/libraries/Hubzero/Database/Tests/Mock/Tag.php new file mode 100644 index 00000000000..ec552f060a7 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/Mock/Tag.php @@ -0,0 +1,19 @@ +name, ' ')) ? explode(' ', $this->name)[0] : $this->name; + } + + /** + * Transforms name to a silly nickname + * + * @return string + **/ + public function transformNickname() + { + return $this->getFirstName() . 'er'; + } + + /** + * One to many relationship with posts + * + * @return \Hubzero\Database\Relationship\OneToMany + **/ + public function posts() + { + return $this->oneToMany('Post'); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/NestedTest.php b/core/libraries/Hubzero/Database/Tests/NestedTest.php new file mode 100644 index 00000000000..03e50c8cfed --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/NestedTest.php @@ -0,0 +1,253 @@ +getMockDriver()); + } + + /** + * Tests object construction and variable initialization + * + * @return void + **/ + public function testConstruct() + { + $model = Discussion::blank(); + + $this->assertInstanceOf('\Hubzero\Database\Nested', $model, 'Model is not an instance of \Hubzero\Database\Nested'); + $this->assertEquals($model->getModelName(), 'Discussion', 'Model should have a model name of "Discussion"'); + } + + /** + * Tests to make sure we can add a new generic child node + * + * @return void + **/ + public function testCanAddNewChildNodeFromParentModel() + { + $parent = Discussion::oneOrFail(1); + + $new = Discussion::blank()->set([ + 'user_id' => 3, + 'title' => 'Testing Stuff', + 'content' => 'This is a test additional child node' + ]); + + $new->saveAsChildOf($parent); + + // Get the current state of the database + $queryTable = $this->getConnection()->createQueryTable( + 'discussions', 'SELECT * FROM discussions' + ); + + // Get our expected state of the database + $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsWithNewChildNode.xml') + ->getTable('discussions'); + + // Now assert that updated and expected are the same + $this->assertTablesEqual($expectedTable, $queryTable); + } + + /** + * Tests to make sure we can add a new generic child node + * + * @return void + **/ + public function testCanAddNewChildNodeFromParentId() + { + $new = Discussion::blank()->set([ + 'user_id' => 3, + 'title' => 'More Testing', + 'content' => 'This is a node added by parent id' + ]); + + $new->saveAsChildOf(3); + + // Get the current state of the database + $queryTable = $this->getConnection()->createQueryTable( + 'discussions', 'SELECT * FROM discussions' + ); + + // Get our expected state of the database + $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsWithNewChildNodeByParentId.xml') + ->getTable('discussions'); + + // Now assert that updated and expected are the same + $this->assertTablesEqual($expectedTable, $queryTable); + } + + /** + * Tests to make sure we can add a new first child node + * + * @return void + **/ + public function testCanAddNewFirstChildNode() + { + $new = Discussion::blank()->set([ + 'user_id' => 3, + 'title' => 'Left node', + 'content' => 'This should be located as the first child of the parent' + ]); + + $new->saveAsFirstChildOf(1); + + // Get the current state of the database + $queryTable = $this->getConnection()->createQueryTable( + 'discussions', 'SELECT * FROM discussions' + ); + + // Get our expected state of the database + $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsWithNewFirstChild.xml') + ->getTable('discussions'); + + // Now assert that updated and expected are the same + $this->assertTablesEqual($expectedTable, $queryTable); + } + + /** + * Tests to see if we can add a new root node + * + * @return void + **/ + public function testCanAddNewRootNode() + { + Discussion::blank()->set([ + 'user_id' => 3, + 'title' => 'This is a new discussion', + 'content' => 'Tell me about life', + 'scope' => 'group', + 'scope_id' => 2 + ])->saveAsRoot(); + + // Get the current state of the database + $queryTable = $this->getConnection()->createQueryTable( + 'discussions', 'SELECT * FROM discussions' + ); + + // Get our expected state of the database + $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsWithNewRootNode.xml') + ->getTable('discussions'); + + // Now assert that updated and expected are the same + $this->assertTablesEqual($expectedTable, $queryTable); + } + + /** + * Tests to see if we can get the children of a given parent + * + * @return void + **/ + public function testCanGetChildren() + { + // Clear that cache here from our earlier request for the same model + \Hubzero\Database\Query::purgeCache(); + + $discussion = Discussion::oneOrFail(1); + $children = $discussion->getChildren()->raw(); + + $this->assertCount(3, $children, 'Discussion 1 should have had 3 children'); + + foreach ([2, 3, 5] as $expected) + { + $this->assertArrayHasKey($expected, $children, "Expected a discussion with id {$expected}"); + } + } + + /** + * Tests to see if we can get all the descendants of a given parent + * + * @return void + **/ + public function testCanGetDescendants() + { + $discussion = Discussion::oneOrFail(1); + $descendants = $discussion->getDescendants()->raw(); + + $this->assertCount(4, $descendants, 'Discussion 1 should have had 4 descendants'); + + foreach ([2, 3, 4, 5] as $expected) + { + $this->assertArrayHasKey($expected, $descendants, "Expected a discussion with id {$expected}"); + } + } + + /** + * Tests to see if we can get a limited set of the descendants of a given parent + * + * @return void + **/ + public function testCanGetLimitedDescendants() + { + $discussion = Discussion::oneOrFail(1); + $descendants = $discussion->descendants()->limit(2)->rows()->raw(); + + foreach ([2, 5] as $expected) + { + $this->assertArrayHasKey($expected, $descendants, "Expected a discussion with id {$expected}"); + } + } + + /** + * Tests to make sure we can delete a low/bottom level child node (i.e. with no children) + * + * @return void + **/ + public function testCanDeleteLeafNode() + { + $discussion = Discussion::oneOrFail(5)->destroy(); + + // Get the current state of the database + $queryTable = $this->getConnection()->createQueryTable( + 'discussions', 'SELECT * FROM discussions' + ); + + // Get our expected state of the database + $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsAfterDelete.xml') + ->getTable('discussions'); + + // Now assert that updated and expected are the same + $this->assertTablesEqual($expectedTable, $queryTable); + } + + /** + * Tests to make sure we can delete a parent node and cascade the delete as appropriate + * + * @return void + **/ + public function testCanDeleteParentNode() + { + $discussion = Discussion::oneOrFail(3)->destroy(); + + // Get the current state of the database + $queryTable = $this->getConnection()->createQueryTable( + 'discussions', 'SELECT * FROM discussions' + ); + + // Get our expected state of the database + $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsAfterDeleteParent.xml') + ->getTable('discussions'); + + // Now assert that updated and expected are the same + $this->assertTablesEqual($expectedTable, $queryTable); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/QueryTest.php b/core/libraries/Hubzero/Database/Tests/QueryTest.php new file mode 100644 index 00000000000..e6bcb5cb656 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/QueryTest.php @@ -0,0 +1,820 @@ +getMockDriver(); + $query = new Query($dbo); + + // Try to actually fetch some rows + $rows = $query->select('*') + ->from('users') + ->whereEquals('id', '1') + ->fetch(); + + // Basically, as long as we don't get false here, we're good + $this->assertCount(1, $rows, 'Query should have returned one result'); + } + + /** + * Test to make sure we can run a basic insert statement + * + * @return void + **/ + public function testBasicPush() + { + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + // Try to add a new row + $query->push('users', [ + 'name' => 'new user', + 'email' => 'newuser@gmail.com' + ]); + + // There are 4 default users in the seed data, and adding a new one should a rowcount of 5 + $this->assertEquals(5, $this->getConnection()->getRowCount('users'), 'Push did not return the expected row count of 5'); + } + + /** + * Test to make sure we can run a basic update statement + * + * @return void + **/ + public function testBasicAlter() + { + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + // Try to update an existing row + $query->alter('users', 'id', 1, [ + 'name' => 'Updated User', + 'email' => 'updateduser@gmail.com' + ]); + + // Get the current state of the database + $queryTable = $this->getConnection()->createQueryTable( + 'users', 'SELECT * FROM users' + ); + + // Get our expected state of the database + $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedUsers.xml') + ->getTable('users'); + + // Now assert that updated and expected are the same + $this->assertTablesEqual($expectedTable, $queryTable); + } + + /** + * Test to make sure we can set a basic value on update + * + * @return void + **/ + public function testSetWithBasicValue() + { + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + // Try to update an existing row + $query->alter('users', 'id', 1, [ + 'name' => new Basic('Updated User'), + 'email' => new Basic('updateduser@gmail.com') + ]); + + // Get the current state of the database + $queryTable = $this->getConnection()->createQueryTable( + 'users', 'SELECT * FROM users' + ); + + // Get our expected state of the database + $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedUsers.xml') + ->getTable('users'); + + // Now assert that updated and expected are the same + $this->assertTablesEqual($expectedTable, $queryTable); + } + + /** + * Test to make sure we can run a basic delete statement + * + * @return void + **/ + public function testBasicRemove() + { + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $result = $query->remove('users', null, null); + $this->assertFalse($result); + + // Try to update an existing row + $query->remove('users', 'id', 1); + + $this->assertEquals(3, $this->getConnection()->getRowCount('users'), 'Remove did not return the expected row count of 3'); + } + + /** + * Test to make sure we can build a query with aliased from statements + * + * @return void + **/ + public function testBuildQueryWithAliasedFrom() + { + // Here's the query we're trying to write... + $expected = "SELECT * FROM `users` AS `u` WHERE `u`.`name` = 'awesome'"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users', 'u') + ->whereEquals('u.name', 'awesome'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build a query with a raw WHERE statement + * + * @return void + **/ + public function testBuildQueryWithRawWhere() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` WHERE LOWER(`name`)='awesome'"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereRaw('LOWER(`name`)=?', ['awesome']); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $expected = "SELECT * FROM `users` WHERE `name` = 'foo' OR LOWER(`name`)='awesome'"; + + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereEquals('name', 'foo') + ->orWhereRaw('LOWER(`name`)=?', ['awesome']); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build a query with where like statements + * + * @return void + **/ + public function testBuildQueryWithWhereLike() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` WHERE `name` LIKE '%awesome%'"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereLike('name', 'awesome'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $expected = "SELECT * FROM `users` WHERE `name` LIKE '%awesome%' OR `name` LIKE '%amazing%'"; + + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereLike('name', 'awesome') + ->orWhereLike('name', 'amazing'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build a query with where IS NULL statements + * + * @return void + **/ + public function testBuildQueryWithWhereIsNull() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` WHERE `name` IS NULL"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereIsNull('name'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $expected = "SELECT * FROM `users` WHERE `name` = '' OR `name` IS NULL"; + + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereEquals('name', '') + ->orWhereIsNull('name'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build a query with where IS NULL statements + * + * @return void + **/ + public function testBuildQueryWithWhereIsNotNull() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` WHERE `name` IS NOT NULL"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereIsNotNull('name'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $expected = "SELECT * FROM `users` WHERE `name` = 'bar' OR `name` IS NOT NULL"; + + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereEquals('name', 'bar') + ->orWhereIsNotNull('name'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build a query with where IS NULL statements + * + * @return void + **/ + public function testBuildQueryWithWhereIn() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` WHERE `name` IN ('one','two','three')"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereIn('name', ['one', 'two', 'three']); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $expected = "SELECT * FROM `users` WHERE `name` = 'amazing' OR `name` IN ('one','two','three')"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereEquals('name', 'amazing') + ->orWhereIn('name', ['one', 'two', 'three']); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $expected = "SELECT * FROM `users` WHERE `name` NOT IN ('one','two','three')"; + + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereNotIn('name', ['one', 'two', 'three']); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $expected = "SELECT * FROM `users` WHERE `name` = 'amazing' OR `name` NOT IN ('one','two','three')"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereEquals('name', 'amazing') + ->orWhereNotIn('name', ['one', 'two', 'three']); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build a query with complex nested where statements + * + * @return void + **/ + public function testBuildQueryWithNestedWheres() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` WHERE (`name` = 'a' OR `name` = 'b' ) AND (`email` = 'c' OR `email` = 'd' )"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->whereEquals('name', 'a', 1) + ->orWhereEquals('name', 'b', 1) + ->resetDepth(0) + ->whereEquals('email', 'c', 1) + ->orWhereEquals('email', 'd', 1); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build a query with a raw JOIN statement + * + * @return void + **/ + public function testBuildQueryWithJoinClause() + { + $dbo = $this->getMockDriver(); + + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` INNER JOIN posts ON `users`.id = `posts`.user_id"; + + $query = new Query($dbo); + $query->select('*') + ->from('users') + ->join('posts', '`users`.id', '`posts`.user_id'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'join Query did not build the expected result'); + + $query = new Query($dbo); + $query->select('*') + ->from('users') + ->innerJoin('posts', '`users`.id', '`posts`.user_id'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'innerJoin Query did not build the expected result'); + + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` LEFT JOIN posts ON `users`.id = `posts`.user_id"; + + $query = new Query($dbo); + $query->select('*') + ->from('users') + ->leftJoin('posts', '`users`.id', '`posts`.user_id'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'leftJoin Query did not build the expected result'); + + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` RIGHT JOIN posts ON `users`.id = `posts`.user_id"; + + $query = new Query($dbo); + + if ($dbo->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'sqlite') + { + $this->setExpectedException('\Hubzero\Database\Exception\UnsupportedSyntaxException'); + + $query->select('*') + ->from('users') + ->rightJoin('posts', '`users`.id', '`posts`.user_id'); + } + else + { + $query->select('*') + ->from('users') + ->rightJoin('posts', '`users`.id', '`posts`.user_id'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'rightJoin Query did not build the expected result'); + } + + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` RIGHT JOIN posts ON `users`.id = `posts`.user_id"; + + $query = new Query($dbo); + $query->select('*') + ->from('users') + ->rightJoin('posts', '`users`.id', '`posts`.user_id'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'rightJoin Query did not build the expected result'); + + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` FULL JOIN posts ON `users`.id = `posts`.user_id"; + + $query = new Query($dbo); + + if ($dbo->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'sqlite') + { + $this->setExpectedException('\Hubzero\Database\Exception\UnsupportedSyntaxException'); + + $query->select('*') + ->from('users') + ->fullJoin('posts', '`users`.id', '`posts`.user_id'); + } + else + { + $query->select('*') + ->from('users') + ->fullJoin('posts', '`users`.id', '`posts`.user_id'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'fullJoin Query did not build the expected result'); + } + } + + /** + * Test to make sure we can build a query with a raw JOIN statement + * + * @return void + **/ + public function testBuildQueryWithRawJoinClause() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `users` INNER JOIN posts ON `users`.id = `posts`.user_id AND `users`.id > 1"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->joinRaw('posts', '`users`.id = `posts`.user_id AND `users`.id > 1'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build an INSERT statement + * + * @return void + **/ + public function testBuildInsert() + { + // Here's the query we're trying to write... + $expected = "INSERT INTO `users` (`name`) VALUES ('danger')"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->insert('users') + ->values(array( + 'name' => 'danger' + )); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + if ($dbo->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'sqlite') + { + $expected = "INSERT OR IGNORE INTO `users` (`name`) VALUES ('awesome')"; + } + else + { + $expected = "INSERT IGNORE INTO `users` (`name`) VALUES ('awesome')"; + } + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->insert('users', true) + ->values(array( + 'name' => 'awesome' + )); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure that fetch properly caches a query + * + * @return void + **/ + public function testFetchCachesQueriesByDefault() + { + // Mock a database driver + $dbo = $this->getMockDriver(); + + // Mock the query builder and tell it we only want to override the query method + $query = $this->getMockBuilder('Hubzero\Database\Query') + ->setConstructorArgs([$dbo]) + ->setMethods(['query']) + ->getMock(); + + // Now set that we should only be calling the query method one time + // We also tell it to return something from the query method, otherwise + // the cache will fail. + $query->expects($this->once()) + ->method('query') + ->willReturn('foo'); + + // The query itself here is irrelavent, we just need to make sure + // that calling the same query twice doesn't hit the driver twice + $query->fetch(); + $query->fetch(); + } + + /** + * Test to make sure that fetch properly caches a query + * + * @return void + **/ + public function testFetchDoesNotCacheQueries() + { + // Mock a database driver + $dbo = $this->getMockDriver(); + + // Mock the query builder and tell it we only want to override the query method + $query = $this->getMockBuilder('Hubzero\Database\Query') + ->setConstructorArgs([$dbo]) + ->setMethods(['query']) + ->getMock(); + + // Now set that we should be calling the query exactly 2 times + // We also tell it to return something from the query method, otherwise + // the cache will fail and we could get a false positive. + $query->expects($this->exactly(2)) + ->method('query') + ->willReturn('foo'); + + // The query itself here is irrelavent, we just need to make sure + // that calling fetch results in a call to the query method. + // We call it twice to ensure that the result is in the cache. + // If the result were not in the cache, we could get a false positive. + $query->fetch('rows', true); + $query->fetch('rows', true); + } + + /** + * Test to make sure we can build a query with where IS NULL statements + * + * @return void + **/ + public function testBuildQueryClear() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `groups`"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('users') + ->join('groups', 'created_by', 'id', 'inner') + ->whereIsNotNull('name') + ->group('cn', 'id') + ->having('foo', '=', 3) + ->clear('from') + ->clear('where') + ->clear('join') + ->clear('group') + ->clear('having') + ->from('groups'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + // Here's the query we're try to write... + $expected = "SELECT id FROM `users` WHERE `name` IS NOT NULL"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('name') + ->from('users') + ->whereIsNotNull('name') + ->deselect() + ->select('id'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + // Here's the query we're try to write... + $expected = "INSERT INTO `users` (`name`) VALUES ('Jimmy')"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->insert('groups') + ->values(array('cn' => 'lorem')) + ->clear('insert') + ->clear('values') + ->insert('users') + ->values(array('name' => 'Jimmy')); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + // Here's the query we're try to write... + $expected = "DELETE FROM `users` WHERE `name` = 'Frank'"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->delete('groups') + ->whereEquals('cn', 'lorem') + ->clear('delete') + ->clear('where') + ->delete('users') + ->whereEquals('name', 'Frank'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + // Here's the query we're try to write... + $expected = "UPDATE `users` SET `name` = 'Frank' WHERE `id` = '2'"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->update('groups') + ->set(array('cn' => 'lorem')) + ->whereEquals('cn', 'lorem') + ->clear('update') + ->clear('where') + ->update('users') + ->set(array('name' => 'Frank')) + ->whereEquals('id', 2); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test to make sure we can build a query with LIMIT + * + * @return void + **/ + public function testBuildQueryLimitStart() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `groups` LIMIT 10"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('groups') + ->limit(10); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $query->start(5); + + $expected = "SELECT * FROM `groups` LIMIT 5,10"; + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + } + + /** + * Test that START is an integer + * + * @return void + **/ + public function testNonNumericStart() + { + $dbo = $this->getMockDriver(); + + $query = new Query($dbo); + $query->select('*') + ->from('groups') + ->start('beginning'); + + $expected = "SELECT * FROM `groups`"; + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + // NOTE: We directly test the Syntax class as the `start()` method on + // the Query class casts values as integers. + $syntax = '\\Hubzero\\Database\\Syntax\\' . ucfirst($dbo->getSyntax()); + $syntax = new $syntax($dbo); + + $this->setExpectedException('InvalidArgumentException'); + + $syntax->setStart('beginning'); + } + + /** + * Test that START is greater than or equal to zero + * + * @return void + **/ + public function testNegativeStart() + { + $dbo = $this->getMockDriver(); + + $query = new Query($dbo); + + $this->setExpectedException('InvalidArgumentException'); + + $query->select('*') + ->from('groups') + ->start(-50); + + /* + $syntax = '\\Hubzero\\Database\\Syntax\\' . ucfirst($dbo->getSyntax()); + $syntax = new $syntax($dbo); + + $this->setExpectedException('InvalidArgumentException'); + + $syntax->setStart(-50); + */ + } + + /** + * Test that LIMIT is an integer + * + * @return void + **/ + public function testNonNumericLimit() + { + $dbo = $this->getMockDriver(); + + $query = new Query($dbo); + $query->select('*') + ->from('groups') + ->limit('all'); + + $expected = "SELECT * FROM `groups`"; + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + // NOTE: We directly test the Syntax class as the `limit()` method on + // the Query class casts values as integers. + $syntax = '\\Hubzero\\Database\\Syntax\\' . ucfirst($dbo->getSyntax()); + $syntax = new $syntax($dbo); + + $this->setExpectedException('InvalidArgumentException'); + + $syntax->setLimit('all'); + } + + /** + * Test that LIMIT is greater than or equal to zero + * + * @return void + **/ + public function testNegativeLimit() + { + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $this->setExpectedException('InvalidArgumentException'); + + $query->select('*') + ->from('groups') + ->limit(-50); + } + + /** + * Test to make sure we can build a query with ORDER BY + * + * @return void + **/ + public function testBuildQueryOrder() + { + // Here's the query we're try to write... + $expected = "SELECT * FROM `groups` ORDER BY `id` ASC"; + + $dbo = $this->getMockDriver(); + $query = new Query($dbo); + + $query->select('*') + ->from('groups') + ->order('id', 'asc'); + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + $query->unorder() + ->order('name', 'desc'); + + $expected = "SELECT * FROM `groups` ORDER BY `name` DESC"; + + $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result'); + + // Test that an exception is thrown if order is not asc or desc + $this->setExpectedException('InvalidArgumentException'); + + $query = new Query($dbo); + $query->select('*') + ->from('groups') + ->order('id', 'foo'); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/RelationalTest.php b/core/libraries/Hubzero/Database/Tests/RelationalTest.php new file mode 100644 index 00000000000..f0c45c5bf06 --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/RelationalTest.php @@ -0,0 +1,556 @@ +getMockDriver()); + } + + /** + * Tests object construction and variable initialization + * + * @return void + **/ + public function testConstruct() + { + $model = User::blank(); + + $this->assertInstanceOf('\Hubzero\Database\Relational', $model, 'Model is not an instance of \Hubzero\Database\Relational'); + $this->assertEquals($model->getModelName(), 'User', 'Model should have a model name of "User"'); + } + + /** + * Tests to make sure a call to a helper function actually finds the function + * + * @return void + **/ + public function testCallHelper() + { + $this->assertEquals('Test', User::one(1)->getFirstName(), 'Model should have returned a first name of "Test"'); + } + + /** + * Tests to make sure a call to a transformer actually finds the transformer + * + * @return void + **/ + public function testCallTransformer() + { + $this->assertEquals('Tester', User::one(1)->nickname, 'Model should have returned a nickname of "Tester"'); + } + + /** + * Tests to make sure that a result can be retrieved + * + * @return void + **/ + public function testOneReturnsResult() + { + $this->assertEquals(1, User::one(1)->id, 'Model should have returned an instance with id of 1'); + } + + /** + * Tests that a call for a non-existant row via oneOrFail method throws an exception + * + * @expectedException RuntimeException + * @return void + **/ + public function testOneOrFailThrowsException() + { + User::oneOrFail(0); + } + + /** + * Tests that a request for a non-existant row via oneOrNew method returns new model + * + * @return void + **/ + public function testOneOrNewCreatesNew() + { + $this->assertTrue(User::oneOrNew(0)->isNew(), 'Model should have stated that it was new'); + } + + /** + * Tests that a belongsToOne relationship properly grabs the related side of the relationship + * + * @return void + **/ + public function testBelongsToOneReturnsRelationship() + { + $this->assertEquals(1, Post::oneOrFail(1)->user->id, 'Model should have returned a user id of 1'); + } + + /** + * Tests that the belongs to one relationship can properly constrain the belongs to side + * + * @return void + **/ + public function testBelongsToOneCanBeConstrained() + { + // Get all users that have 2 or more posts - this should return 1 result + $posts = Post::all()->whereRelatedHas('user', function($user) + { + $user->whereEquals('name', 'Test User'); + })->rows(); + + $this->assertCount(2, $posts, 'Model should have returned a count of 2 posts for the user by the name of "Test User"'); + } + + /** + * Tests that a oneToMany relationship properly grabs the many side of the relationship + * + * @return void + **/ + public function testOneToManyReturnsRelationship() + { + $this->assertCount(2, User::oneOrFail(1)->posts, 'Model should have returned a count of 2 posts for user 1'); + } + + /** + * Tests that the one side of the relationship can be properly constrained by the many side + * + * @return void + **/ + public function testOneToManyCanBeConstrainedByCount() + { + // Get all users that have 2 or more posts - this should return 1 result + $users = User::all()->whereRelatedHasCount('posts', 2)->rows(); + + $this->assertCount(1, $users, 'Model should have returned a count of 1 user with 2 or more posts'); + } + + /** + * Tests that a manyToMany relationship properly grabs the many side of the relationship + * + * @return void + **/ + public function testManyToManyReturnsRelationship() + { + $this->assertCount(3, Post::oneOrFail(1)->tags, 'Model should have returned a count of 3 tags for post 1'); + } + + /** + * Tests that the local/left side of the m2m relationship can be properly constrained by the related/right side + * + * @return void + **/ + public function testManyToManyCanBeConstrainedByCount() + { + $posts = Post::all()->whereRelatedHasCount('tags', 3)->rows(); + + $this->assertCount(1, $posts, 'Model should have returned a count of 1 post with 3 or more tags'); + } + + /** + * Tests that the local/left side of the m2m relationship can be properly constrained by the related/right side + * + * @return void + **/ + public function testManyToManyCanBeConstrained() + { + $posts = Post::all()->whereRelatedHas('tags', function($tags) + { + $tags->whereEquals('name', 'fun stuff'); + })->rows(); + + $this->assertCount(2, $posts, 'Model should have returned a count of 2 post with the tag "fun stuff"'); + } + + /** + * Tests that a oneShiftsToMany relationship properly grabs the many side of the relationship + * + * @return void + **/ + public function testOneShiftsToManyReturnsRelationship() + { + $this->assertCount(3, Group::oneOrFail(1)->members, 'Model should have returned a count of 3 members for group 1'); + } + + /** + * Tests that a manyShiftsToMany relationship properly grabs the many (right) side of the relationship + * + * @return void + **/ + public function testManyShiftsToManyReturnsRelationship() + { + $this->assertCount(2, Group::oneOrFail(1)->permissions, 'Model should have returned a count of 2 permissions for group 1'); + } + + /** + * Tests that the local/left side of the os2m relationship can be properly constrained by the related/right side + * + * @return void + **/ + public function testOneShiftsToManyCanBeConstrainedByCount() + { + $projects = Project::all()->whereRelatedHasCount('members', 3)->rows(); + + $this->assertCount(1, $projects, 'Model should have returned a count of 1 project with 3 or more members'); + } + + /** + * Tests that the local/left side of the ms2m relationship can be properly constrained by the related/right side + * + * @return void + **/ + public function testManyShiftsToManyCanBeConstrainedByCount() + { + $projects = Project::all()->whereRelatedHasCount('permissions', 2)->rows(); + + $this->assertCount(1, $projects, 'Model should have returned a count of 1 project with 2 or more permissions'); + } + + /** + * Tests that the local/left side of the os2m relationship can be properly constrained by the related/right side + * + * @return void + **/ + public function testOneShiftsToManyCanBeConstrained() + { + $projects = Project::all()->whereRelatedHas('members', function($members) + { + $members->whereEquals('user_id', 3); + })->rows(); + + $this->assertCount(1, $projects, 'Model should have returned a count of 1 project with a member whose user_id is 3'); + } + + /** + * Tests that the local/left side of the ms2m relationship can be properly constrained by the related/right side + * + * @return void + **/ + public function testManyShiftsToManyCanBeConstrained() + { + $projects = Project::all()->whereRelatedHas('permissions', function($permissions) + { + $permissions->whereEquals('name', 'read'); + })->rows(); + + $this->assertCount(2, $projects, 'Model should have returned a count of 2 project with read permissions'); + } + + /** + * Tests that an including call can properly preload a simple one to many relationship + * + * @return void + **/ + public function testIncludingOneToManyPreloadsRelationship() + { + $users = User::all()->including('posts')->rows()->first(); + + $this->assertNotNull($users->getRelationship('posts'), 'Model should have had a relationship named posts defined'); + } + + /** + * Tests that an including call can properly preload a one shifts to many relationship + * + * @return void + **/ + public function testIncludingOneShiftsToManyPreloadsRelationship() + { + $projects = Project::all()->including('members')->rows()->first(); + + $this->assertNotNull($projects->getRelationship('members'), 'Model should have had a relationship named members defined'); + } + + /** + * Tests that an including call can properly preload a many shifts to many relationship + * + * @return void + **/ + public function testIncludingManyShiftsToManyPreloadsRelationship() + { + $projects = Project::all()->including('permissions')->rows()->first(); + + $this->assertNotNull($projects->getRelationship('permissions'), 'Model should have had a relationship named permissions defined'); + } + + /** + * Tests that an including call can properly preload a simple many to many relationship + * + * @return void + **/ + public function testIncludingManyToManyPreloadsRelationship() + { + $posts = Post::all()->including('tags')->rows()->first(); + + $this->assertNotNull($posts->getRelationship('tags'), 'Model should have had a relationship named tags defined'); + } + + /** + * Tests that an including call can be constrained on a one to many relationship + * + * @return void + **/ + public function testIncludingOneToManyCanBeConstrained() + { + $users = User::all()->including(['posts', function($posts) + { + $posts->where('content', 'LIKE', '%computer%'); + }])->rows(); + + $this->assertCount(1, $users->seek(1)->posts, 'Model should have had 1 post that met the constraint'); + $this->assertCount(0, $users->seek(2)->posts, 'Model should have had 0 posts that met the constraint'); + } + + /** + * Tests that an including call can be constrained on a one shifts to many relationship + * + * @return void + **/ + public function testIncludingOneShiftsToManyCanBeConstrained() + { + $projects = Project::all()->including(['members', function($posts) + { + $posts->whereEquals('user_id', 1); + }])->rows(); + + $this->assertCount(1, $projects->seek(1)->members, 'Model should have had 1 member that met the constraint'); + $this->assertCount(1, $projects->seek(2)->members, 'Model should have had 1 member that met the constraint'); + } + + /** + * Tests that an including call can be constrained on a many shifts to many relationship + * + * @return void + **/ + public function testIncludingManyShiftsToManyCanBeConstrained() + { + $projects = Project::all()->including(['permissions', function($permissions) + { + $permissions->whereEquals('name', 'read'); + }])->rows(); + + $this->assertCount(1, $projects->seek(1)->permissions, 'Model should have had 1 permission that met the constraint'); + $this->assertCount(0, $projects->seek(3)->permissions, 'Model should have had 0 permissions that met the constraint'); + } + + /** + * Tests to make sure saving a one to many relationship properly sets the associated field on the related side + * + * @return void + **/ + public function testSaveOneToManyAssociatesRelated() + { + User::oneOrFail(1)->posts()->save(['content' => 'This is a test post']); + + $this->assertArrayHasKey('user_id', User::oneOrFail(1)->posts->last(), 'Saved item should have automatically included a user_id'); + } + + /** + * Tests to make sure saving a one shifts to many relationship properly sets the associated fields on the related side + * + * @return void + **/ + public function testSaveOneShiftsToManyAssociatesRelated() + { + Project::oneOrFail(1)->members()->save(['user_id' => 2]); + + $this->assertCount(4, Project::oneOrFail(1)->members, 'Saved item should have automatically included a scope and scope_id'); + } + + /** + * Tests to make sure connecting a many to many properly creates the relationship + * + * @return void + **/ + public function testConnectManyToManyCreatesAssociation() + { + // Tag post 2 with tag 3 + Post::oneOrFail(2)->tags()->connect([3]); + + $this->assertCount(2, Post::oneOrFail(2)->tags, 'Post should have had a total of 2 tags'); + } + + /** + * Tests to make sure connecting a many shifts to many properly creates the relationship + * + * @return void + **/ + public function testConnectManyShiftsToManyCreatesAssociation() + { + // Tag post 2 with tag 3 + Member::oneOrFail(1)->permissions()->connect([1]); + + $this->assertCount(3, Member::oneOrFail(1)->permissions, 'Member should have had a total of 3 permissions'); + } + + /** + * Tests to make sure disconnecting a many to many properly destroys the relationship + * + * @return void + **/ + public function testDisconnectManyToManyDestroysAssociation() + { + // Tag post 2 with tag 3 + Post::oneOrFail(2)->tags()->disconnect([3]); + + $this->assertCount(1, Post::oneOrFail(2)->purgeCache()->tags, 'Post should have had a total of 1 tags'); + } + + /** + * Tests to make sure disconnecting a many shifts to many properly destroys the relationship + * + * @return void + **/ + public function testDisconnectManyShiftsToManyDestroysAssociation() + { + // Tag post 2 with tag 3 + Member::oneOrFail(1)->permissions()->disconnect([1]); + + $this->assertCount(2, Member::oneOrFail(1)->purgeCache()->permissions, 'Member should have had a total of 2 permissions'); + } + + /** + * Tests to make sure many to many save automatically connects + * + * @return void + **/ + public function testManyToManySaveAutomaticallyConnects() + { + // Tag post 2 with tag 3 + Post::oneOrFail(2)->tags()->save(['name' => 'automatically created']); + + $this->assertCount(1, Tag::whereEquals('name', 'automatically created')->rows(), 'A tag with the name of "automatically created" should exist'); + $this->assertCount(2, Post::oneOrFail(2)->purgeCache()->tags, 'Post should have had a total of 2 tags'); + } + + /** + * Tests to make sure many shifts to many save automatically connects + * + * @return void + **/ + public function testManyShiftsToManySaveAutomaticallyConnects() + { + // Tag post 2 with tag 3 + Member::oneOrFail(1)->permissions()->save(['name' => 'do awesome stuff']); + + $this->assertCount(1, Permission::whereEquals('name', 'do awesome stuff')->rows(), 'A permission with the name of "do awesome stuff" should exist'); + $this->assertCount(3, Member::oneOrFail(1)->purgeCache()->permissions, 'Member should have had a total of 3 permissions'); + } + + /** + * Tests to make sure connecting a many to many can also add additional fields to the intermediary table + * + * @return void + **/ + public function testConnectManyToManyCanAddAdditionalFields() + { + // Tag post 2 with tag 4 + $now = uniqid(); + Post::oneOrFail(3)->tags()->connect([1 => ['tagged' => $now]]); + + $result = Post::oneOrFail(3)->tags->seek(1); + + $this->assertFalse($result->hasAttribute('tagged'), 'Post should not have had an attributed for "tagged"'); + $this->assertArrayHasKey('tagged', (array)$result->associated, 'Post should have had an associated key of "tagged"'); + $this->assertEquals($now, $result->associated->tagged, 'Post tagged date should have equaled ' . $now); + } + + /** + * Tests to make sure connecting a many shifts to many can also add additional fields to the intermediary table + * + * @return void + **/ + public function testConnectManyShiftsToManyCanAddAdditionalFields() + { + $now = uniqid(); + Group::oneOrFail(1)->permissions()->connect([3 => ['permitted' => $now]]); + + $result = Group::oneOrFail(1)->permissions->seek(3); + + $this->assertFalse($result->hasAttribute('permitted'), 'Group should not have had an attributed for "permitted"'); + $this->assertArrayHasKey('permitted', (array)$result->associated, 'Group should have had an associated key of "permitted"'); + $this->assertEquals($now, $result->associated->permitted, 'Group permitted date should have equaled ' . $now); + } + + /** + * Tests to make sure we can retrieve the shifter of a many to many shifting relationship + * + * @return void + **/ + public function testOneShiftsToManyCanGetShifter() + { + $group = Member::oneOrFail(1)->memberable; + + $this->assertEquals($group->id, '1', 'Member should have returned a group association id of 1'); + } + + /** + * Tests to make sure we can get relationships defined on a model + * + * @return void + **/ + public function testCanIntrospectRelationships() + { + $this->assertEquals(['posts'], User::introspectRelationships(), 'User should have had one relationships of "posts"'); + } + + /** + * Tests to make sure we can add a dynamic relationship at runtime + * + * @return void + **/ + public function testCanAddRuntimeRelationship() + { + User::registerRelationship('bio', function ($model) + { + return $model->oneToOne('Bio'); + }); + + $this->assertEquals('This is my bio about me.', User::oneOrFail(1)->bio->text, 'Bio call should have returned the bio for user 1'); + $this->assertEquals('This is my bio about me.', User::oneOrFail(1)->bio()->rows()->text, 'Bio call should have returned the bio for user 1'); + } + + /** + * Tests to make sure we can get relationships defined on a model, including runtime relationships + * + * @return void + **/ + public function testCanIntrospectRelationshipsIncludingRuntimeRelationships() + { + // Note that we're expecting the relationship registered in the previous test + $this->assertEquals(['posts', 'bio'], User::introspectRelationships(), 'User should have had two relationships of "posts" and "bio"'); + } + + /** + * Tests to make sure we can serialize and unserialize a model + * + * @return void + **/ + public function testCanDoSimpleSerialization() + { + $user = User::oneOrFail(1); + + $serialized = serialize($user); + $unserialized = unserialize($serialized); + + $this->assertEquals($user, $unserialized, 'Unserialize did not result in the original model'); + } +} diff --git a/core/libraries/Hubzero/Database/Tests/RulesTest.php b/core/libraries/Hubzero/Database/Tests/RulesTest.php new file mode 100644 index 00000000000..1937ed8dd9b --- /dev/null +++ b/core/libraries/Hubzero/Database/Tests/RulesTest.php @@ -0,0 +1,160 @@ + 'Me'], ['name' => 'notempty']); + $fail = Rules::validate(['name' => ''], ['name' => 'notempty']); + + $this->assertTrue($pass, 'Rule (notempty) should have validated with a name of "Me"'); + $this->assertCount(1, $fail, 'Rule (notempty) should have returned one error for empty name'); + } + + /** + * Test to make sure a negative number fails validation + * + * @return void + **/ + public function testPositive() + { + $pass = Rules::validate(['age' => 25], ['age' => 'positive']); + $fail = Rules::validate(['age' => -1], ['age' => 'positive']); + + $this->assertTrue($pass, 'Rule (positive) should have validated with an age of 25'); + $this->assertCount(1, $fail, 'Rule (positive) should have returned one error for an age of -1'); + } + + /** + * Test to make sure a zero fails validation + * + * @return void + **/ + public function testNonzero() + { + $passPos = Rules::validate(['score' => 1], ['score' => 'nonzero']); + $passNeg = Rules::validate(['score' => -1], ['score' => 'nonzero']); + $fail = Rules::validate(['score' => 0], ['score' => 'nonzero']); + + $this->assertTrue($passPos, 'Rule (nonzero) should have validated with a score of 1'); + $this->assertTrue($passNeg, 'Rule (nonzero) should have validated with a score of -1'); + $this->assertCount(1, $fail, 'Rule (nonzero) should have returned one error for a score of 0'); + } + + /** + * Test to make sure a number fails validation + * + * @return void + **/ + public function testAlpha() + { + $pass = Rules::validate(['name' => "John Awesome"], ['name' => 'alpha']); + $fail = Rules::validate(['name' => "MI6"], ['name' => 'alpha']); + + $this->assertTrue($pass, 'Rule (alpha) should have validated with a name of "John Awesome"'); + $this->assertCount(1, $fail, 'Rule (alpha) should have returned one error for a name of "MI6"'); + } + + /** + * Test to make sure a bad phone fails validation + * + * @return void + **/ + public function testPhone() + { + $pass1 = Rules::validate(['phone' => "765-494-4000"], ['phone' => 'phone']); + $pass2 = Rules::validate(['phone' => "(765) 494-4000"], ['phone' => 'phone']); + $pass3 = Rules::validate(['phone' => "7654944000"], ['phone' => 'phone']); + $fail1 = Rules::validate(['phone' => "12345"], ['phone' => 'phone']); + $fail2 = Rules::validate(['phone' => "123-456-7890"], ['phone' => 'phone']); + + $this->assertTrue($pass1, 'Rule (phone) should have validated with a phone of "765-494-4000"'); + $this->assertTrue($pass2, 'Rule (phone) should have validated with a phone of "(765) 494-4000"'); + $this->assertTrue($pass3, 'Rule (phone) should have validated with a phone of "7654944000"'); + $this->assertCount(1, $fail1, 'Rule (phone) should have returned one error for a phone of "12345"'); + $this->assertCount(1, $fail2, 'Rule (phone) should have returned one error for a phone of "123-456-7890"'); + } + + /** + * Test to make sure an improper email fails validation + * + * @return void + **/ + public function testEmail() + { + $pass = Rules::validate(['email' => "you@gmail.com"], ['email' => 'email']); + $fail = Rules::validate(['email' => "me.com"], ['email' => 'email']); + + $this->assertTrue($pass, 'Rule (email) should have validated with a email of "you@gmail.com"'); + $this->assertCount(1, $fail, 'Rule (email) should have returned one error for a email of "me.com"'); + } + + /** + * Test to make sure an empty string fails validation + * + * @return void + **/ + public function testCompoundRules() + { + $pass = Rules::validate(['name' => "mr cool"], ['name' => 'notempty|alpha']); + $fail = Rules::validate(['name' => ""], ['name' => 'notempty|alpha']); + + $this->assertTrue($pass, 'Rule (notempty|alpha) should have validated with a name of "mr cool"'); + $this->assertCount(2, $fail, 'Rules (notempty|alpha) should have returned two errors for a name of ""'); + } + + /** + * Test to make sure partial failure still results in failure + * + * @return void + **/ + public function testPartialFailure() + { + $fail = Rules::validate(['name' => "Mr. Awesome"], ['name' => 'notempty|alpha']); + + $this->assertCount(1, $fail, 'Rules (notempty|alpha) should have returned one error for a name of "Mr. Awesome"'); + } + + /** + * Test to make sure custom validation rules result in failure + * + * @return void + **/ + public function testCustomRules() + { + $endAfterStart = function($data) + { + return $data['end'] > $data['start'] ? false : 'The end must be after the beginning'; + }; + + $fail = Rules::validate(['start' => '2015-07-01 00:00:00', 'end' => '2015-06-01 00:00:00'], ['end' => $endAfterStart]); + + $beSquare = function($data) + { + return $data['height'] == $data['width'] ? false : 'It\'s not a square!'; + }; + + $pass = Rules::validate(['height' => 5, 'width' => 5], ['width' => $beSquare]); + + $this->assertCount(1, $fail, 'Rules (custom) should have returned one error for an end date before the beginning date'); + $this->assertTrue($pass, 'Rule (custom) should have validated with an equal height and width'); + } +} diff --git a/core/libraries/Hubzero/Database/Traits/ErrorBag.php b/core/libraries/Hubzero/Database/Traits/ErrorBag.php new file mode 100644 index 00000000000..5a3f70b3b63 --- /dev/null +++ b/core/libraries/Hubzero/Database/Traits/ErrorBag.php @@ -0,0 +1,69 @@ +errors = $errors; + return $this; + } + + /** + * Adds error to the existing set + * + * @param string $error The error to add + * @return $this + * @since 2.0.0 + **/ + public function addError($error) + { + $this->errors[] = $error; + return $this; + } + + /** + * Returns all errors + * + * @return array + * @since 2.0.0 + **/ + public function getErrors() + { + return $this->errors; + } + + /** + * Returns the first error + * + * @return string + * @since 2.0.0 + **/ + public function getError() + { + return (isset($this->errors[0])) ? $this->errors[0] : ''; + } +} diff --git a/core/libraries/Hubzero/Database/Value/Basic.php b/core/libraries/Hubzero/Database/Value/Basic.php new file mode 100644 index 00000000000..7ff1426db08 --- /dev/null +++ b/core/libraries/Hubzero/Database/Value/Basic.php @@ -0,0 +1,47 @@ +content = $content; + } + + /** + * Builds the given string representation of the value object + * + * @param object $syntax The syntax object with which the query is being built + * @return string + * @since 2.1.0 + **/ + public function build($syntax) + { + $syntax->bind(is_string($this->content) ? trim($this->content) : $this->content); + + return '?'; + } +} diff --git a/core/libraries/Hubzero/Database/Value/Raw.php b/core/libraries/Hubzero/Database/Value/Raw.php new file mode 100644 index 00000000000..9dfd6078410 --- /dev/null +++ b/core/libraries/Hubzero/Database/Value/Raw.php @@ -0,0 +1,26 @@ +content; + } +} diff --git a/core/libraries/Hubzero/Debug/Dumper.php b/core/libraries/Hubzero/Debug/Dumper.php new file mode 100644 index 00000000000..e5355f51d95 --- /dev/null +++ b/core/libraries/Hubzero/Debug/Dumper.php @@ -0,0 +1,265 @@ + '', + '_' => '', + ' ' => '', + '\\' => '', + '/' => '' + ); + + /** + * Constructor + * + * @return void + */ + public function __construct() + { + $this->clear(); + } + + /** + * Returns a reference to a global object, only creating it + * if it doesn't already exist. + * + * @return object + */ + public static function &getInstance() + { + static $instance; + + if (!$instance) + { + $instance = new self(); + } + + return $instance; + } + + /** + * Returns the renderer + * + * @return object Renderer\Renderable + */ + public function getRenderer() + { + return $this->_renderer; + } + + /** + * Sets the renderer + * + * @param mixed $renderer string or Renderable + * @return object + */ + public function setRenderer($renderer) + { + if (is_string($renderer)) + { + $cName = $this->scrub($renderer); + + $invokable = __NAMESPACE__ . '\\Dumper\\' . ucfirst($cName); + + if (!class_exists($invokable)) + { + throw new RendererNotFoundException(sprintf( + '%s: failed retrieving renderer via invokable class "%s"; class does not exist', + __CLASS__ . '::' . __FUNCTION__, + $invokable + )); + } + $renderer = new $invokable; + } + + if (!($renderer instanceof Renderable)) + { + throw new InvalidArgumentException(sprintf( + '%s was unable to fetch renderer or renderer was not an instance of %s', + get_class($this) . '::' . __FUNCTION__, + __NAMESPACE__ . '\\Dumper\\Renderable' + )); + } + + $this->_renderer = $renderer; + + return $this; + } + + /** + * Canonicalize name + * + * @param string $name + * @return string + */ + protected function scrub($name) + { + // this is just for performance instead of using str_replace + return strtolower(strtr($name, $this->_nameReplacements)); + } + + /** + * Adds a message + * + * @param mixed $message + * @param string $label + * @return object + */ + public function addVar($var) + { + $varc = (is_object($var) ? clone $var : $var); + $this->_messages[] = array( + 'var' => $varc, + 'time' => microtime(true) + ); + + return $this; + } + + /** + * Get a list of messages + * + * @return array + */ + public function messages() + { + $messages = $this->_messages; + + // sort messages by their timestamp + usort($messages, function($a, $b) + { + if ($a['time'] === $b['time']) + { + return 0; + } + return $a['time'] < $b['time'] ? -1 : 1; + }); + + return $messages; + } + + /** + * Deletes all messages + * + * @return void + */ + public function clear() + { + $this->_messages = array(); + } + + /** + * Get a count and list of messages + * + * @return array + */ + public function collect() + { + $messages = $this->messages(); + + return array( + 'count' => count($messages), + 'messages' => $messages + ); + } + + /** + * Does the log have any messages? + * + * @return integer + */ + public function hasMessages() + { + return count($this->messages()); + } + + /** + * Render + * + * @param object $renderer + * @return string + */ + public function render($renderer=null) + { + if (!$renderer) + { + $renderer = php_sapi_name() === 'cli' ? 'console' : 'html'; + } + + return $this->setRenderer($renderer) + ->getRenderer() + ->render($this->messages()); + } + + /** + * Adds a message + * + * @param mixed $var + * @return void + */ + public static function log($var) + { + $console = self::getInstance(); + $console->addVar($var); + } + + /** + * Dumps a var + * + * @param mixed $var + * @param string $to + * @return void + */ + public static function dump($var, $to=null) + { + $console = self::getInstance(); + $console->addVar($var); + + echo $console->render($to); + $console->clear(); + } + + /** + * Dumps a var and dies(); + * + * @param mixed $var + * @param string $to + * @return void + */ + public static function stop($var, $to=null) + { + self::dump($var, $to); + die(); + } +} diff --git a/core/libraries/Hubzero/Debug/Dumper/AbstractRenderer.php b/core/libraries/Hubzero/Debug/Dumper/AbstractRenderer.php new file mode 100644 index 00000000000..5c7a87b0110 --- /dev/null +++ b/core/libraries/Hubzero/Debug/Dumper/AbstractRenderer.php @@ -0,0 +1,114 @@ +setMessages($messages); + } + } + + /** + * Returns renderer name + * + * @return string + */ + public function getName() + { + return '__abstract__'; + } + + /** + * Get the list of messages + * + * @return array + */ + public function getMessages() + { + return $this->_messages; + } + + /** + * Set the list of messages + * + * @param mixed $messages + * @return object + */ + public function setMessages($messages) + { + if (!is_array($messages)) + { + throw new InvalidArgumentException(sprintf( + 'Messages must be an array. Type of "%s" passed.', + gettype($messages) + )); + } + + $this->_messages = $messages; + + return $this; + } + + /** + * Render a list of messages + * + * @param array $messages + * @return string + */ + public function render($messages = null) + { + if ($messages) + { + $this->setMessages($messages); + } + + $messages = $this->getMessages(); + + $output = array(); + foreach ($messages as $item) + { + $output[] = print_r($item['var'], true); + } + + return implode("\n", $output); + } + + /** + * Turn an array into a pretty print format + * + * @param array $arr + * @return string + */ + protected function _deflate($arr) + { + $arr = str_replace(array("\n", "\r", "\t"), ' ', $arr); + return preg_replace('/\s+/', ' ', $arr); + } +} diff --git a/core/libraries/Hubzero/Debug/Dumper/Console.php b/core/libraries/Hubzero/Debug/Dumper/Console.php new file mode 100644 index 00000000000..d5340ed4b44 --- /dev/null +++ b/core/libraries/Hubzero/Debug/Dumper/Console.php @@ -0,0 +1,73 @@ +setMessages($messages); + } + + $messages = $this->getMessages(); + + echo '-----'; + foreach ($messages as $item) + { + echo print_r($item['var'], true); //$this->_deflate($item['var']); + } + echo '-----'; + } + + /** + * Turn an array into a pretty print format + * + * @param array $arr + * @return string + */ + protected function _deflate($arr) + { + $output = 'Array( ' . "\n"; + $a = array(); + foreach ($arr as $key => $val) + { + if (is_array($val)) + { + $a[] = "\t" . $key . ' => ' . $this->_deflate($val); + } + else + { + $a[] = "\t" . $key . ' => ' . htmlentities($val, ENT_COMPAT, 'UTF-8') . ''; + } + } + $output .= implode(", \n", $a) . "\n" . ' )' . "\n"; + + return $output; + } +} diff --git a/core/libraries/Hubzero/Debug/Dumper/Html.php b/core/libraries/Hubzero/Debug/Dumper/Html.php new file mode 100644 index 00000000000..fb0a8e616ae --- /dev/null +++ b/core/libraries/Hubzero/Debug/Dumper/Html.php @@ -0,0 +1,100 @@ +setMessages($messages); + } + + $messages = $this->getMessages(); + + $output = array(); + $output[] = '
'; + foreach ($messages as $item) + { + $output[] = $this->line($item); + } + $output[] = '
'; + return implode("\n", $output); + } + + /** + * Render a list of messages + * + * @param array $messages + * @return string + */ + public function line(array $item) + { + $val = print_r($item['var'], true); + $val = preg_replace('/\[(.+?)\] =>/i', '[$1] =>', $val); + return '
' . $val . '
'; + } + + /** + * Turn an array into a pretty print format + * + * @param array $arr + * @return string + */ + protected function _deflate($arr) + { + if (is_string($arr)) + { + $arr = htmlentities($arr, ENT_COMPAT, 'UTF-8'); + $arr = preg_replace('/\[(.+?)\] =>/i', '[$1] =>', $arr); + return $arr; + } + + $output = 'Array( ' . "\n"; + $a = array(); + if (is_array($arr)) + { + foreach ($arr as $key => $val) + { + if (is_array($val)) + { + $a[] = "\t" . '' . $key . ' => ' . $this->_deflate($val) . ''; + } + else + { + $val = htmlentities($val, ENT_COMPAT, 'UTF-8'); + $val = preg_replace('/\[(.+?)\] =>/i', '[$1] =>', $val); + $a[] = "\t" . '' . $key . ' => ' . $val . ''; + } + } + } + $output .= implode(", \n", $a) . "\n" . ' )' . "\n"; + + return $output; + } +} diff --git a/core/libraries/Hubzero/Debug/Dumper/Javascript.php b/core/libraries/Hubzero/Debug/Dumper/Javascript.php new file mode 100644 index 00000000000..a60e7f813c7 --- /dev/null +++ b/core/libraries/Hubzero/Debug/Dumper/Javascript.php @@ -0,0 +1,55 @@ +setMessages($messages); + } + + $messages = $this->getMessages(); + + $output = array(); + $output[] = ''; + return implode("\n", $output); + } +} diff --git a/core/libraries/Hubzero/Debug/Dumper/Logs.php b/core/libraries/Hubzero/Debug/Dumper/Logs.php new file mode 100644 index 00000000000..831aac6e3de --- /dev/null +++ b/core/libraries/Hubzero/Debug/Dumper/Logs.php @@ -0,0 +1,91 @@ +_logger = \Log::getRoot(); + } + + /** + * Returns renderer name + * + * @return string + */ + public function getName() + { + return 'logs'; + } + + /** + * Render a list of messages + * + * @param array $messages + * @return string + */ + public function render($messages = null) + { + if ($messages) + { + $this->setMessages($messages); + } + + $messages = $this->getMessages(); + + foreach ($messages as $item) + { + $this->_logger->debug(print_r($item['var'], true)); + } + } + + /** + * Turn an array into a pretty print format + * + * @param array $arr + * @return string + */ + protected function _deflate($arr) + { + $output = 'Array( ' . "\n"; + $a = array(); + foreach ($arr as $key => $val) + { + if (is_array($val)) + { + $a[] = "\t" . '' . $key . ' => ' . $this->_deflate($val) . ''; + } + else + { + $a[] = "\t" . '' . $key . ' => ' . htmlentities($val, ENT_COMPAT, 'UTF-8') . ''; + } + } + $output .= implode(", \n", $a) . "\n" . ' )' . "\n"; + + return $output; + } +} diff --git a/core/libraries/Hubzero/Debug/Dumper/Renderable.php b/core/libraries/Hubzero/Debug/Dumper/Renderable.php new file mode 100644 index 00000000000..a1092962719 --- /dev/null +++ b/core/libraries/Hubzero/Debug/Dumper/Renderable.php @@ -0,0 +1,44 @@ +label = (string) $label; + $this->start = (float) $start; + $this->end = (float) $end; + $this->memory = (int) $memory; + } + + /** + * Gets the label. + * + * @return string The label + */ + public function label() + { + return $this->label; + } + + /** + * Gets the relative time of the start of the period. + * + * @return integer The time (in milliseconds) + */ + public function started() + { + return $this->start; + } + + /** + * Gets the relative time of the end of the period. + * + * @return integer The time (in milliseconds) + */ + public function ended() + { + return $this->end; + } + + /** + * Gets the time spent in this period. + * + * @return integer The period duration (in milliseconds) + */ + public function duration() + { + return $this->end - $this->start; + } + + /** + * Gets the memory usage. + * + * @return integer The memory usage (in bytes) + */ + public function memory() + { + return $this->memory; + } + + /** + * Get string output + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * Get string output + * + * @return string + */ + public function toString() + { + return sprintf('%s: %.2F MiB - %d ms', $this->label(), $this->memory() / 1024 / 1024, $this->duration()); + } + + /** + * Get array output + * + * @return array + */ + public function toArray() + { + return array( + 'label' => $this->label(), + 'start' => $this->started(), + 'end' => $this->ended(), + 'memory' => $this->memory() + ); + } +} diff --git a/core/libraries/Hubzero/Debug/Profiler.php b/core/libraries/Hubzero/Debug/Profiler.php new file mode 100644 index 00000000000..7fb52ea4e0d --- /dev/null +++ b/core/libraries/Hubzero/Debug/Profiler.php @@ -0,0 +1,195 @@ +reset(); + + $this->prefix = $prefix; + } + + /** + * Reset the profiler + * + * @return void + */ + public function reset() + { + $this->started = $this->now(); + $this->prefix = ''; + $this->marks = array(); + $this->memory = memory_get_usage(true); + } + + /** + * Get the prefix + * + * @return string + */ + public function label() + { + return $this->prefix; + } + + /** + * Output a time mark + * + * The mark is returned as text enclosed in
tags + * with a CSS class of 'profiler'. + * + * @param string $label A label for the time mark + * @return string Mark enclosed in
tags + */ + public function mark($label) + { + $this->marks[] = new Mark($label, $this->ended(), $this->now(), memory_get_usage(true)); + + return $this; + } + + /** + * Get the current time. + * + * @return float The current time + */ + public function now() + { + return microtime(true); + } + + /** + * Gets the relative time of the start of the first period. + * + * @return int The time (in milliseconds) + */ + public function started() + { + return $this->started; + } + + /** + * Gets the relative time of the end of the last period. + * + * @return int The time (in milliseconds) + */ + public function ended() + { + $count = count($this->marks); + + return $count ? $this->marks[$count - 1]->ended() : $this->started; + } + + /** + * Gets the duration of the events (including all periods). + * + * @return int The duration (in milliseconds) + */ + public function duration() + { + $total = 0; + + foreach ($this->marks as $mark) + { + $total += $mark->duration(); + } + + return $total; + } + + /** + * Gets the max memory usage of all periods. + * + * @return int The memory usage (in bytes) + */ + public function memory() + { + $memory = $this->memory; + + foreach ($this->marks as $mark) + { + if ($mark->memory() > $memory) + { + $memory = $mark->memory(); + } + } + + return $memory; + } + + /** + * Get all marks + * + * @return array + */ + public function marks() + { + return $this->marks; + } + + /** + * Returns a summary of all timer activity so far + * + * @return array + */ + public function summary() + { + $summary = array( + 'start' => $this->started(), + 'end' => $this->ended(), + 'total' => $this->duration(), + 'memory' => $this->memory() + ); + + return $summary; + } +} diff --git a/core/libraries/Hubzero/Debug/Tests/MarkTest.php b/core/libraries/Hubzero/Debug/Tests/MarkTest.php new file mode 100644 index 00000000000..6cf26f9cc16 --- /dev/null +++ b/core/libraries/Hubzero/Debug/Tests/MarkTest.php @@ -0,0 +1,150 @@ +assertEquals($mark->label(), 'test1'); + $this->assertEquals($mark->started(), 0.0); + $this->assertEquals($mark->ended(), 0.0); + $this->assertEquals($mark->memory(), 0); + + $mark = new Mark('test2', 1.5, 3.5, 1048576); + + $this->assertEquals($mark->label(), 'test2'); + $this->assertEquals($mark->started(), 1.5); + $this->assertEquals($mark->ended(), 3.5); + $this->assertEquals($mark->memory(), 1048576); + } + + /** + * Tests the label() method. + * + * @return void + * @covers \Hubzero\Debug\Profile\Mark::label + */ + public function testLabel() + { + $mark = new Mark('test', 0, 1.5, 0); + $this->assertEquals($mark->label(), 'test'); + } + + /** + * Tests the started() method. + * + * @return void + * @covers \Hubzero\Debug\Profile\Mark::started + */ + public function testStarted() + { + $mark = new Mark('test', 0, 0, 0); + $this->assertEquals($mark->started(), 0); + + $mark = new Mark('test', 1.5, 3.5, 0); + $this->assertEquals($mark->started(), 1.5); + } + + /** + * Tests the ended() method. + * + * @return void + * @covers \Hubzero\Debug\Profile\Mark::ended + */ + public function testEnded() + { + $mark = new Mark('test', 0, 1.5, 0); + $this->assertEquals($mark->ended(), 1.5); + + $mark = new Mark('test', 1.5, 3.5, 0); + $this->assertEquals($mark->ended(), 3.5); + } + + /** + * Tests the duration() method. + * + * @return void + * @covers \Hubzero\Debug\Profile\Mark::duration + */ + public function testDuration() + { + $mark = new Mark('test', 0, 0, 0); + $this->assertEquals($mark->duration(), 0); + + $mark = new Mark('test', 0, 1.5, 0); + $this->assertEquals($mark->duration(), 1.5); + } + + /** + * Tests the memory() method. + * + * @return void + * @covers \Hubzero\Debug\Profile\Mark::memory + */ + public function testMemory() + { + $mark = new Mark('test', 0, 1.5, 0); + $this->assertEquals($mark->memory(), 0); + + $mark = new Mark('test', 0, 1.5, 1048576); + $this->assertEquals($mark->memory(), 1048576); + } + + /** + * Tests the toString() method. + * + * @return void + * @covers \Hubzero\Debug\Profile\Mark::toString + * @covers \Hubzero\Debug\Profile\Mark::__toString + */ + public function testToString() + { + $mark = new Mark('test', 0, 1.5, 1048576); + + $result = sprintf('%s: %.2F MiB - %d ms', 'test', 1048576 / 1024 / 1024, 1.5); + + $this->assertEquals($mark->toString(), $result); + $this->assertEquals($mark->__toString(), $result); + $this->assertEquals((string)$mark, $result); + } + + /** + * Tests the toArray() method. + * + * @return void + * @covers \Hubzero\Debug\Profile\Mark::toArray + */ + public function testToArray() + { + $mark = new Mark('test', 0, 1.5, 1048576); + + $result = array( + 'label' => 'test', + 'start' => 0.0, + 'end' => 1.5, + 'memory' => 1048576 + ); + + $this->assertEquals($mark->toArray(), $result); + } +} diff --git a/core/libraries/Hubzero/Debug/Tests/ProfilerTest.php b/core/libraries/Hubzero/Debug/Tests/ProfilerTest.php new file mode 100644 index 00000000000..25ce83ccc99 --- /dev/null +++ b/core/libraries/Hubzero/Debug/Tests/ProfilerTest.php @@ -0,0 +1,260 @@ +instance = new Profiler('test'); + } + + /** + * Tests the __constructor. + * + * @covers \Hubzero\Debug\Profiler::__construct + * @return void + **/ + public function testConstructor() + { + $instance = new Profiler(); + + $this->assertGreaterThan(0, $instance->started()); + $this->assertGreaterThan(0, $instance->memory()); + $this->assertEquals(count($instance->marks()), 0); + $this->assertEquals($instance->label(), ''); + + $instance = new Profiler('test'); + + $this->assertEquals($instance->label(), 'test'); + } + + /** + * Tests the marks() method. + * + * @covers \Hubzero\Debug\Profiler::marks + * @return void + **/ + public function testMarks() + { + $this->instance->mark('one'); + $this->instance->mark('two'); + $this->instance->mark('three'); + + // Assert the first point has a time and memory = 0 + $marks = $this->instance->marks(); + + $this->assertTrue(is_array($marks), 'marks() should return an array'); + $this->assertEquals(count($marks), 3); + } + + /** + * Tests the mark() method. + * + * @covers \Hubzero\Debug\Profiler::mark + * @return void + **/ + public function testMark() + { + $started = $this->instance->started(); + + $this->instance->mark('one'); + $this->instance->mark('two'); + $this->instance->mark('three'); + + // Assert the first point has a time and memory = 0 + $marks = $this->instance->marks(); + + $first = $marks[0]; + + $this->assertEquals($first->label(), 'one'); + $this->assertEquals($first->started(), $started); + + // Assert the other points have a time and memory + $second = $marks[1]; + + $this->assertEquals($second->label(), 'two'); + $this->assertGreaterThan(0, $second->duration()); + $this->assertGreaterThan(0, $second->memory()); + + $third = $marks[2]; + + $this->assertEquals($third->label(), 'three'); + $this->assertGreaterThan(0, $third->duration()); + $this->assertGreaterThan(0, $third->memory()); + + // Assert the third point has greater values than the other points + $this->assertGreaterThan($second->ended(), $third->ended()); + $this->assertGreaterThanOrEqual($second->memory(), $third->memory()); + } + + /** + * Tests the duration() method. + * + * @covers \Hubzero\Debug\Profiler::duration + * @return void + **/ + public function testDuration() + { + $this->instance->mark('one'); + $this->instance->mark('two'); + $this->instance->mark('three'); + + $this->assertGreaterThan(0, $this->instance->duration()); + } + + /** + * Tests the label() method. + * + * @covers \Hubzero\Debug\Profiler::label + * @return void + **/ + public function testLabel() + { + $this->assertEquals($this->instance->label(), 'test'); + } + + /** + * Tests the now() method. + * + * @covers \Hubzero\Debug\Profiler::now + * @return void + **/ + public function testNow() + { + $this->assertGreaterThanOrEqual(microtime(true), $this->instance->now()); + } + + /** + * Tests the reset() method. + * + * @covers \Hubzero\Debug\Profiler::reset + * @return void + **/ + public function testReset() + { + $instance = new Profiler('test'); + + $instance->mark('one'); + $instance->mark('two'); + $instance->mark('three'); + + $instance->reset(); + + $marks = $instance->marks(); + + $this->assertTrue(empty($marks)); + $this->assertEquals($instance->label(), ''); + } + + /** + * Tests the started() method. + * + * @covers \Hubzero\Debug\Profiler::started + * @return void + **/ + public function testStarted() + { + $instance = new Profiler('test'); + + $this->assertTrue($instance->started() >= time()); + } + + /** + * Tests the ended() method. + * + * @covers \Hubzero\Debug\Profiler::ended + * @return void + **/ + public function testEnded() + { + $instance = new Profiler('test'); + + $started = $instance->started(); + + sleep(0.1); + + $this->assertEquals($instance->ended(), $started); + + $instance->mark('one'); + $instance->mark('two'); + $instance->mark('three'); + + $this->assertNotEquals($instance->ended(), $started); + $this->assertTrue($instance->ended() > $started); + } + + /** + * Tests the memory() method. + * + * @covers \Hubzero\Debug\Profiler::memory + * @return void + **/ + public function testMemory() + { + $instance = $this->instance; + + $memory1 = $instance->memory(); + + $instance->mark('foo'); + + $data = array(); + for ($i = 0; $i < 900; $i++) + { + $jnk = new \stdClass; + $jnk->bar = array_fill(0, 100, str_repeat('bar', 10)); + + $data[] = $jnk; + } + + $instance->mark('bar'); + + unset($data); + + $memory2 = $instance->memory(); + + $this->assertTrue($memory2 >= $memory1); + } + + /** + * Tests the summary() method. + * + * @covers \Hubzero\Debug\Profiler::summary + * @return void + **/ + public function testSummary() + { + $summary = $this->instance->summary(); + + $this->assertTrue(is_array($summary)); + $this->assertTrue(array_key_exists('start', $summary)); + $this->assertTrue(array_key_exists('end', $summary)); + $this->assertTrue(array_key_exists('total', $summary)); + $this->assertTrue(array_key_exists('memory', $summary)); + } +} diff --git a/core/libraries/Hubzero/Document/Asset/File.php b/core/libraries/Hubzero/Document/Asset/File.php new file mode 100644 index 00000000000..3e615884b91 --- /dev/null +++ b/core/libraries/Hubzero/Document/Asset/File.php @@ -0,0 +1,492 @@ + null, + 'override' => null, + 'target' => null + ); + + /** + * Constructor + * + * @param string $extension CMS Extension to load asset from + * @param string $name Asset name (optional) + * @return void + */ + public function __construct($extension, $name=null) + { + $this->extension = strtolower(trim((string) $extension)); + + switch (substr($this->extension, 0, 4)) + { + case 'com_': + $this->kind = 'components'; + $name = $name ?: substr($this->extension, 4); + break; + case 'mod_': + $this->kind = 'modules'; + $name = $name ?: $this->extension; + break; + case 'plg_': + $this->kind = 'plugins'; + if (!$name) + { + $parts = explode('_', $this->extension); + $name = $parts[2]; + } + break; + case 'tpl_': + $this->kind = 'templates'; + break; + default: + if ($this->extension == 'system') + { + $this->kind = $this->extension; + } + break; + } + + $this->name = $name; + + if (substr($this->name, 0, strlen('http')) == 'http' + || substr($this->name, 0, strlen('://')) == '://' + || substr($this->name, 0, strlen('//')) == '//') + { + $this->kind = 'external'; + $this->paths['source'] = ltrim($this->name, ':'); + $this->paths['target'] = ltrim($this->name, ':'); + } + + if (strstr($this->name, '.')) + { + if (strtolower(substr($name, strrpos($name, '.') + 1)) == $this->type) + { + $this->name = preg_replace('/\.[^.]*$/', '', $this->name); + } + } + + $this->directory = $this->dir($this->name, $this->type()); + } + + /** + * Determine the asset directory + * + * @param string $name Asset path + * @param string $default Default directory + * @return string + */ + protected function dir(&$name, $default='') + { + if (substr($name, 0, 2) == './') + { + $name = substr($name, 2); + + return ''; + } + + if (substr($name, 0, 1) == '/') + { + $name = substr($name, 1); + + return '/'; + } + + return $default; + } + + /** + * Get the asset type + * + * @return string + */ + public function type() + { + return $this->type; + } + + /** + * Get the CMS extension name + * [mod_*, com_*, plg_folder_type, tpl_*, system] + * + * @return string + */ + public function extensionName() + { + return $this->extension; + } + + /** + * Get the CMS extension type + * [modules, plugins, components, templates, system] + * + * @return string + */ + public function extensionType() + { + return $this->kind; + } + + /** + * Get the file name + * + * @return string + */ + public function file() + { + return $this->name . '.' . $this->type; + } + + /** + * Is the asset a declaration? + * + * @return boolean + */ + public function isDeclaration() + { + return $this->declaration; + } + + /** + * Is the asset external to the site? + * + * @return boolean + */ + public function isExternal() + { + return $this->kind == 'external'; + } + + /** + * Adds to the list of paths + * + * @param string $type The type of path to add. + * @param mixed $path The directory or stream, or an array of either, to search. + * @return object + */ + public function setPath($type, $path) + { + if (!array_key_exists($type, $this->paths)) + { + throw new \Exception(\App::get('language')->txt('Unknown asset path type of %s given.', $type)); + } + + $path = trim((string) $path); + + // Add separators as needed + $path = DS . trim($path, DS); + + // Add to list of paths + $this->paths[$type] = $path; + + return $this; + } + + /** + * Get the source path + * + * @return string + */ + public function sourcePath() + { + if (!isset($this->paths['source'])) + { + // If loading from an absolute path + if ($this->directory == '/') + { + $this->paths['source'] = PATH_ROOT . DS . $this->directory . $this->file(); + } + else + { + $paths = array(); + + $basea = PATH_APP . DS; + $basec = PATH_CORE . DS; + + $client = (isset(\App::get('client')->alias) ? \App::get('client')->alias : \App::get('client')->name); + + $path2 = ''; + switch ($this->extensionType()) + { + case 'plugins': + $parts = explode('_', $this->extensionName()); + $path = $this->extensionType() . DS . $parts[1] . DS . $parts[2] . DS; + break; + + case 'components': + $path = $this->extensionType() . DS . $this->extensionName() . DS . $client . DS; + if (substr($this->extensionName(), 0, 4) == 'com_') + { + $path2 = $this->extensionType() . DS . substr($this->extensionName(), 4) . DS . $client . DS; + } + break; + + case 'modules': + $path = $this->extensionType() . DS . $this->extensionName() . DS; + if (substr($this->extensionName(), 0, 4) == 'mod_') + { + $path2 = $this->extensionType() . DS . substr($this->extensionName(), 4) . DS; + } + break; + + case 'system': + case 'core': + default: + $path = ''; + break; + } + + // App + $paths_app[] = $basea . $path . 'assets' . ($this->directory ? DS . $this->directory : '') . DS . $this->file(); + $paths_app[] = $basea . $path . ($this->directory ? $this->directory . DS : '') . $this->file(); + + // Core + $paths_core[] = $basec . $path . 'assets' . ($this->directory ? DS . $this->directory : '') . DS . $this->file(); + $paths_core[] = $basec . $path . ($this->directory ? $this->directory . DS : '') . $this->file(); + + if ($path2) + { + // App + $paths_app[] = $basea . $path2 . 'assets' . ($this->directory ? DS . $this->directory : '') . DS . $this->file(); + $paths_app[] = $basea . $path2 . ($this->directory ? $this->directory . DS : '') . $this->file(); + if ($this->name == $this->extension) + { + $paths_app[] = $basea . $path2 . 'assets' . ($this->directory ? DS . $this->directory : '') . DS . substr($this->name, 4) . '.' . $this->type; + $paths_app[] = $basea . $path2 . ($this->directory ? $this->directory . DS : '') . substr($this->name, 4) . '.' . $this->type; + } + + // Core + $paths_core[] = $basec . $path2 . 'assets' . ($this->directory ? DS . $this->directory : '') . DS . $this->file(); + $paths_core[] = $basec . $path2 . ($this->directory ? $this->directory . DS : '') . $this->file(); + if ($this->name == $this->extension) + { + $paths_core[] = $basec . $path2 . 'assets' . ($this->directory ? DS . $this->directory : '') . DS . substr($this->name, 4) . '.' . $this->type; + $paths_core[] = $basec . $path2 . ($this->directory ? $this->directory . DS : '') . substr($this->name, 4) . '.' . $this->type; + } + } + $paths = array_merge($paths_app, $paths_core); + // Run through each path until we find one that works + foreach ($paths as $path) + { + if (file_exists($path)) + { + $this->paths['source'] = $path; + break; + } + } + } + } + return $this->paths['source']; + } + + /** + * Get the override path + * + * @return string + */ + public function overridePath() + { + if (!isset($this->paths['override'])) + { + $this->paths['override'] = \App::get('template')->path . DS . 'html'; + $this->paths['override'] .= DS . $this->extensionName() . DS . ($this->extensionType() == 'system' ? $this->type() . DS : '') . $this->file(); + } + return $this->paths['override']; + } + + /** + * Get the target path + * + * @return string + */ + public function targetPath() + { + if (!isset($this->paths['target'])) + { + if ($this->declaration) + { + $this->paths['target'] = ''; + } + else + { + $this->paths['target'] = $this->sourcePath(); + + if ($this->overridePath() && file_exists($this->overridePath())) + { + $this->paths['target'] = $this->overridePath(); + } + } + } + + return $this->paths['target']; + } + + /** + * Get the last modified time for a file + * + * @return integer + */ + public function lastModified() + { + $source = $this->targetPath(); + + if ($this->declaration || !is_file($source)) + { + return 0; + } + + return filemtime($source); + } + + /** + * Does asset exist? + * + * @return boolean + */ + public function exists() + { + if ($this->isExternal()) + { + return true; + } + + if ($this->declaration && $this->name) + { + return true; + } + + if ($this->targetPath() && file_exists($this->targetPath())) + { + return true; + } + + return false; + } + + /** + * Get public asset path + * + * @param boolean $timestamp Append timestamp? + * @return string + */ + public function link($timestamp=true) + { + $output = $this->targetPath(); + + if (!$output) + { + return $output; + } + + if ($this->isExternal()) + { + return $output; + } + + if ($this->extensionType() == 'system') + { + $relative = rtrim(str_replace('/administrator', '', \Request::base(true)), '/') . substr($output, strlen(PATH_ROOT)); + } + else + { + if (strpos($output, PATH_ROOT) === 0) + { + $relative = rtrim(\Request::root(true), '/') . rtrim(substr($output, strlen(PATH_ROOT)), '/'); + } + else if (strpos($output, PATH_CORE) === 0) + { + $relative = rtrim(\Request::root(true), '/') . "/core/" . rtrim(substr($output, strlen(PATH_CORE)), '/'); + } + } + + return $relative . ($timestamp ? '?v=' . $this->lastModified() : ''); + } + + /** + * Get target asset's content + * + * @return string + */ + public function contents() + { + if ($this->declaration) + { + return $this->name; + } + + return file_exists($this->targetPath()) ? file_get_contents($this->targetPath()) : ''; + } + + /** + * Convert to string + * + * @return string + */ + public function __toString() + { + return (string) ($this->declaration ? $this->name : $this->link()); + } +} diff --git a/core/libraries/Hubzero/Document/Asset/Image.php b/core/libraries/Hubzero/Document/Asset/Image.php new file mode 100644 index 00000000000..4febfed4ba9 --- /dev/null +++ b/core/libraries/Hubzero/Document/Asset/Image.php @@ -0,0 +1,61 @@ +ext = strtolower(\App::get('filesystem')->extension($name)); + + } + + /** + * Get the file name + * + * @return string + */ + public function file() + { + return $this->name; + } +} diff --git a/core/libraries/Hubzero/Document/Asset/Javascript.php b/core/libraries/Hubzero/Document/Asset/Javascript.php new file mode 100644 index 00000000000..0724f2477c3 --- /dev/null +++ b/core/libraries/Hubzero/Document/Asset/Javascript.php @@ -0,0 +1,43 @@ +declaration = true; + + // Reset the name in case any parsing/modification + // happened in the parent constructor. + $this->name = $name; + } + } +} diff --git a/core/libraries/Hubzero/Document/Asset/Stylesheet.php b/core/libraries/Hubzero/Document/Asset/Stylesheet.php new file mode 100644 index 00000000000..90b51526a5c --- /dev/null +++ b/core/libraries/Hubzero/Document/Asset/Stylesheet.php @@ -0,0 +1,43 @@ +extension || strstr($name, '{') || strstr($name, '@')) + { + $this->declaration = true; + + // Reset the name in case any parsing/modification + // happened in the parent constructor. + $this->name = $name; + } + } +} diff --git a/core/libraries/Hubzero/Document/Assets.php b/core/libraries/Hubzero/Document/Assets.php new file mode 100644 index 00000000000..9522f3bb97c --- /dev/null +++ b/core/libraries/Hubzero/Document/Assets.php @@ -0,0 +1,799 @@ +addStyleSheet(rtrim(Request::base(true), '/') . $stylesheet . '?v=' . filemtime($root . $stylesheet)); + } + } + + /** + * Adds a linked script to the page + * + * @param string $script Script name (optional, uses module name if left blank) + * @return void + */ + public static function addScript($script) + { + if (!$script) + { + return; + } + + if (substr($script, -3) != '.js') + { + $script .= '.js'; + } + + $root = self::base(); + + if ($document = self::app('document')) + { + $document->addScript(rtrim(Request::base(true), '/') . $script . '?v=' . filemtime($root . $script)); + } + } + + /** + * Adds a linked stylesheet from a component to the page + * + * @param string $component Component name + * @param string $stylesheet Stylesheet name (optional, uses component name if left blank) + * @param string $dir Asset directory to look in + * @return void + */ + public static function addComponentStylesheet($component, $stylesheet = '', $dir = 'css') + { + if ($dir != 'css') + { + $stylesheet = $dir . '/' . $stylesheet; + } + + $asset = new Stylesheet($component, $stylesheet); + + if (defined('JPATH_GROUPCOMPONENT')) + { + $base = substr(JPATH_GROUPCOMPONENT, strlen(PATH_ROOT)); + + $asset->setPath('source', $base . DS . 'assets' . DS . 'css' . DS . $asset->file()); + $asset->setPath('override', $base . DS . 'assets' . DS . 'css' . DS . $asset->file()); + } + + if ($asset->exists()) + { + if ($document = self::app('document')) + { + $document->addStyleSheet($asset->link()); + } + } + } + + /** + * Adds a linked script from a component to the page + * + * @param string $component URL to the linked script + * @param string $script Script name (optional, uses module name if left blank) + * @param string $dir Asset directory to look in + * @return void + */ + public static function addComponentScript($component, $script = '', $dir = 'js') + { + if ($dir != 'js') + { + $script = $dir . '/' . $script; + } + + $asset = new Javascript($component, $script); + + if (defined('JPATH_GROUPCOMPONENT')) + { + $base = substr(JPATH_GROUPCOMPONENT, strlen(PATH_ROOT)); + + $asset->setPath('source', $base . DS . 'assets' . DS . ($dir ? DS . $dir : '') . DS . $asset->file()); + $asset->setPath('override', $base . DS . 'assets' . DS . ($dir ? DS . $dir : '') . DS . $asset->file()); + } + + if ($asset->exists()) + { + if ($document = self::app('document')) + { + $document->addScript($asset->link()); + } + } + } + + /** + * Adds a linked stylesheet from the system to the page + * + * @param string $stylesheet Stylesheet name + * @param string $dir Asset directory to look in + * @return void + */ + public static function addSystemStylesheet($stylesheet, $dir = 'css') + { + if ($dir != 'css') + { + $stylesheet = $dir . '/' . $stylesheet; + } + + $asset = new Stylesheet('system', $stylesheet); + + if ($asset->exists()) + { + if ($document = self::app('document')) + { + $document->addStyleSheet($asset->link()); + } + } + } + + /** + * Adds a linked script from the system to the page + * + * @param string $script Script name (optional, uses module name if left blank) + * @param string $dir Asset directory to look in + * @return void + */ + public static function addSystemScript($script, $dir = 'js') + { + if ($dir != 'js') + { + $script = $dir . '/' . $script; + } + + $asset = new Javascript('system', $script); + + if ($asset->exists()) + { + if ($document = self::app('document')) + { + $document->addScript($asset->link()); + } + } + } + + /** + * Gets the path to a component image + * checks template overrides first, then component + * + * @param string $component Component name + * @param string $image Image to look for + * @param string $dir Asset directory to look in + * @return string Path to an image file + */ + public static function getComponentImage($component, $image, $dir = 'img') + { + $image = ltrim($image, DS); + + if (!self::isImage($image)) + { + return $image; + } + + $template = 'system'; + if ($t = self::app('template')) + { + $template = self::app('template')->template; + } + + $paths = array(); + $paths[] = DS . 'templates' . DS . $template . DS . 'html' . DS . $component . DS . 'images' . DS . $image; + $paths[] = DS . 'components' . DS . $component . DS . 'assets' . ($dir ? DS . $dir : '') . DS . $image; + $paths[] = DS . 'components' . DS . $component . DS . 'images' . DS . $image; + + $root = self::base(); + + // Run through each path until we find one that works + foreach ($paths as $path) + { + if (file_exists($root . $path)) + { + // Push script to the document + return rtrim(Request::base(true), '/') . $path; + } + } + } + + /** + * Gets the path to a component stylesheet + * checks template overrides first, then component + * + * @param string $component Component name + * @param string $stylesheet Stylesheet to look for + * @param string $dir Asset directory to look in + * @return string Path to a stylesheet + */ + public static function getComponentStylesheet($component, $stylesheet, $dir = 'css') + { + $template = 'system'; + if ($t = self::app('template')) + { + $template = self::app('template')->template; + } + + $paths = array(); + $paths[] = DS . 'templates' . DS . $template . DS . 'html' . DS . $component . DS . $stylesheet; + $paths[] = DS . 'components' . DS . $component . DS . 'assets' . ($dir ? DS . $dir : '') . DS . $stylesheet; + $paths[] = DS . 'components' . DS . $component . DS . $folder . DS . $stylesheet; + + $root = self::base(); + + // Run through each path until we find one that works + foreach ($paths as $path) + { + if (file_exists($root . $path)) + { + // Push script to the document + return rtrim(Request::base(true), '/') . $path; + } + } + } + + /** + * Gets the path to a module image + * checks template overrides first, then module + * + * @param string $module Module name + * @param string $image Image to look for + * @param string $dir Asset directory to look in + * @return string Path to an image file + */ + public static function getModuleImage($module, $image, $dir = 'img') + { + $image = ltrim($image, DS); + + if (!self::isImage($image)) + { + return $image; + } + + $template = 'system'; + if ($t = self::app('template')) + { + $template = self::app('template')->template; + } + + $paths = array(); + $paths[] = DS . 'templates' . DS . $template . DS . 'html' . DS . $module . DS . 'images' . DS . $image; + $paths[] = DS . 'modules' . DS . $module . DS . 'assets' . ($dir ? DS . $dir : '') . DS . $image; + $paths[] = DS . 'modules' . DS . $module . DS . 'images' . DS . $image; + + $root = self::base(); + + // Run through each path until we find one that works + foreach ($paths as $path) + { + if (file_exists($root . $path)) + { + // Push script to the document + return rtrim(Request::base(true), '/') . $path; + } + } + } + + /** + * Adds a linked stylesheet from a module to the page + * + * @param string $module Module name + * @param string $stylesheet Stylesheet name (optional, uses module name if left blank) + * @param string $dir Asset directory to look in + * @return void + */ + public static function addModuleStyleSheet($module, $stylesheet = '', $dir = 'css') + { + if ($dir != 'css') + { + $stylesheet = $dir . '/' . $stylesheet; + } + + $asset = new Stylesheet($module, $stylesheet); + + if ($asset->exists()) + { + if ($document = self::app('document')) + { + $document->addStyleSheet($asset->link()); + } + } + } + + /** + * Adds a linked script to the page + * + * @param string $module URL to the linked script + * @param string $script Script name (optional, uses module name if left blank) + * @param string $dir Asset directory to look in + * @return void + */ + public static function addModuleScript($module, $script = '', $dir = 'js') + { + if ($dir != 'js') + { + $script = $dir . '/' . $script; + } + + $asset = new Javascript($module, $script); + + if ($asset->exists()) + { + if ($document = self::app('document')) + { + $document->addScript($asset->link()); + } + } + } + + /** + * Gets the path to a plugin image + * checks template overrides first, then plugin folder + * + * @param string $folder Plugin folder name + * @param string $plugin Plugin name + * @param string $image Image to look for + * @param string $dir Asset directory to look in + * @return string Path to an image file + */ + public static function getPluginImage($folder, $plugin, $image, $dir = 'img') + { + $image = ltrim($image, DS); + + if (!self::isImage($image)) + { + return $image; + } + + $template = 'system'; + if ($t = self::app('template')) + { + $template = self::app('template')->template; + } + + $paths = array(); + $paths[] = DS . 'templates' . DS . $template . DS . 'html' . DS . 'plg_' . $folder . '_' . $plugin . DS . 'images' . DS . $image; + $paths[] = DS . 'plugins' . DS . $folder . DS . $plugin . DS . 'assets' . ($dir ? DS . $dir : '') . DS . $image; + $paths[] = DS . 'plugins' . DS . $folder . DS . $plugin . DS . 'images' . DS . $image; + + // Run through each path until we find one that works + foreach ($paths as $i => $path) + { + $root = JPATH_SITE; + if ($i == 0) + { + $root = JPATH_ADMINISTRATOR; + } + + if (file_exists($root . $path)) + { + if ($i == 0) + { + $b = rtrim(Request::base(true), DS); + } + else + { + $b = str_replace('/administrator', '', rtrim(Request::base(true), DS)); + } + // Push script to the document + return $b . $path; + } + } + } + + /** + * Adds a linked stylesheet from a plugin to the page + * + * @param string $folder Plugin folder name + * @param string $plugin Plugin name + * @param string $stylesheet Stylesheet name (optional, uses module name if left blank) + * @param string $dir Asset directory to look in + * @return void + */ + public static function addPluginStyleSheet($folder, $plugin, $stylesheet = '', $dir = 'css') + { + if ($dir != 'css') + { + $stylesheet = $dir . '/' . $stylesheet; + } + + $asset = new Stylesheet('plg_' . $folder . '_' . $plugin, $stylesheet); + + if ($asset->exists()) + { + if ($document = self::app('document')) + { + $document->addStyleSheet($asset->link()); + } + } + } + + /** + * Adds a linked script to the page + * + * @param string $folder Plugin folder name + * @param string $plugin Plugin name + * @param string $script Script name (optional, uses module name if left blank) + * @param string $dir Asset directory to look in + * @return void + */ + public static function addPluginScript($folder, $plugin, $script = '', $dir = 'js') + { + if ($dir != 'js') + { + $script = $dir . '/' . $script; + } + + $asset = new Javascript('plg_' . $folder . '_' . $plugin, $script); + + if ($asset->exists()) + { + if ($document = self::app('document')) + { + $document->addScript($asset->link()); + } + } + } + + /** + * Gets the path to a system image + * + * @param string $image Image to look for + * @param string $dir Asset directory to look in + * @return string Path to an image file + */ + public static function getSystemImage($image, $dir = 'images') + { + $image = ltrim($image, DS); + + if (!self::isImage($image)) + { + return $image; + } + + $template = DS . 'core' . DS . 'templates' . DS . 'system'; + if ($t = self::app('template')) + { + $template = substr($t->path, strlen(PATH_ROOT)); + } + + $paths = array(); + $paths[] = $template . DS . 'html' . DS . 'system' . ($dir ? DS . $dir : '') . DS . $image; + $paths[] = DS . 'core' . DS . 'assets' . DS . $dir . DS . $image; + + // Run through each path until we find one that works + foreach ($paths as $path) + { + if (file_exists(PATH_ROOT . $path)) + { + // Push script to the document + return str_replace('/administrator', '', rtrim(Request::base(true), '/')) . $path; + } + } + } + + /** + * Returns the path to a system stylesheet + * Accepts either an array or string of comma-separated file names + * If more than one stylesheet is called for, it will combine, compress, return path to cached file + * + * @param mixed $elements An array or string of comma-separated file names + * @return string + */ + public static function getSystemStylesheet($elements = null) + { + // Path to system cache + $client = (isset(\App::get('client')->alias) ? \App::get('client')->alias : \App::get('client')->name); + + $cachedir = PATH_APP . DS . 'cache' . DS . $client; + if (!self::app('filesystem')->exists(PATH_APP . DS . 'cache' . DS . $client)) + { + if (!self::app('filesystem')->makeDirectory(PATH_APP . DS . 'cache' . DS . $client)) + { + return ''; + } + } + + // Path to system CSS + $thispath = PATH_CORE . DS . 'assets' . DS . 'css'; + + $env = self::app('config')->get('application_env', 'production'); + + try + { + // Primary build file + $primary = 'site'; + + // Cache vars + $output = $cachedir . DS . $primary . '.css'; + + // If debugging is turned off and a cache file exist + if ($env == 'production' && file_exists($output)) + { + $output = rtrim(Request::root(true), '/') . '/app/cache/' . $client . '/' . $primary . '.css?v=' . filemtime($output); + } + else + { + $lesspath = PATH_CORE . DS . 'assets' . DS . 'less'; + + if (!class_exists('lessc')) + { + throw new Exception('LESS parser not found.'); + } + + // Try to compile LESS files + $less = new lessc; + if ($env != 'development') + { + $less->setFormatter('compressed'); + } + + // Are there any template overrides? + $template = self::app('template')->path . DS . 'less'; // . 'bootstrap.less'; + $input = $lesspath . DS . $primary . '.less'; + + if (file_exists($template . DS . $primary . '.less')) + { + // Reset the path to the primary build file + $input = $template . DS . $primary . '.less'; + } + + // Add the template path to the import list + $less->setImportDir(array( + $template . DS, + $lesspath . DS + )); + + $cacheFile = $cachedir . DS . $primary . '.less.cache'; + $cache = null; + + if (file_exists($cacheFile)) + { + $cache = unserialize(file_get_contents($cacheFile)); + } + + if ($cache && is_array($cache['files'])) + { + foreach ($cache['files'] as $fname => $ftime) + { + $path = explode('/', $fname); + $file = array_pop($path); + + if (file_exists($template . '/' . $file)) + { + $nname = $template . '/' . $file; + } + else + { + $nname = $lesspath . '/' . $file; + } + + if ($fname != $nname or !file_exists($nname) or filemtime($nname) > $ftime) + { + // One of the files we knew about previously has changed + // so we should look at our incoming root again. + $cache = $input; + break; + } + } + } + + // If no cache file or the root build file is different + if (!$cache || (is_array($cache) && isset($cache['root']) && $cache['root'] != $input)) + { + $cache = $input; + } + + // create a new cache object, and compile + /* + array( + 'files' => list of files imported, + 'root' => root file (bootstrap.less) + 'updated' => timestamp, + 'compiled' => compiled LESS + ) + */ + + if (is_string($cache)) + { + $newCache = $less->cachedCompile($cache); + } + else + { + $newCache = $cache; + } + + // Did the cache change? + if (!is_array($cache) || $newCache['updated'] > $cache['updated']) + { + file_put_contents($cacheFile, serialize($newCache)); // Update the compiled LESS timestamp + $newCache['compiled'] = str_replace(array("'/media/system/", "'/core/assets/"), "'" . rtrim(Request::root(true), '/') . '/core/assets/', $newCache['compiled']); + file_put_contents($output, $newCache['compiled']); // Update the compiled LESS + } + $output = rtrim(Request::root(true), '/') . '/app/cache/' . $client . '/' . $primary . '.css?v=' . $newCache['updated']; + } + } + catch (Exception $e) + { + // Anything passed? + if (!$elements) + { + return ''; + } + // Is it a string? + if (is_string($elements)) + { + $elements = explode(',', $elements); + } + if (count($elements) <= 0) + { + return ''; + } + // Trim items + $elements = array_map('trim', $elements); + + // Determine last modification date of the files + $lastmodified = 0; + + foreach ($elements as $k => $element) + { + if (!$element) + { + $elements[$k] = false; + continue; + } + + // Strip file extension to normalize data + $element = basename($element, '.css'); + + $elements[$k] = $element; + + // Check if the file exists + $path = $thispath . DS . $element . '.css'; + + if (!file_exists($path)) + { + $elements[$k] = false; + continue; + } + + // Get the last modified time + // We take the max time so $lastmodified should be different if any of the files have changed. + $lastmodified += filemtime($path); + } + + // Remove any empty items + $elements = array_filter($elements); + + // Build hash + $hash = $lastmodified; // . '-' . md5(implode(',', $elements)); + + // Only one stylesheet called for so return it as is + if (count($elements) == 1) + { + return $thispath . DS . $elements[0] . '.css'; + } + + // Try the cache first to see if the combined files were already generated + $cachefile = 'system-' . $hash . '.css'; + + if (!file_exists($cachedir . DS . $cachefile)) + { + $contents = ''; + reset($elements); + + foreach ($elements as $k => $element) + { + $contents .= "\n\n" . file_get_contents($thispath . DS . $element . '.css'); + } + $patterns = array( + '!/\*[^*]*\*+([^/][^*]*\*+)*/!', /* remove comments */ + '/[\n\r \t]/', /* remove tabs, spaces, newlines, etc. */ + '/ +/' /* collapse multiple spaces to a single space */ + /* '/ ?([,:;{}]) ?/' remove space before and after , : ; { } [!] apparently, IE 7 doesn't like this and won't process the stylesheet */ + ); + $replacements = array( + '', + ' ', + ' '/*, + '$1'*/ + ); + $contents = preg_replace($patterns, $replacements, $contents); + $contents = str_replace(array("url('/media/system/", "url('/core/assets/"), "url('" . rtrim(Request::root(true), '/') . "/core/assets/", $contents); + + if ($fp = fopen($cachedir . DS . $cachefile, 'wb')) + { + fwrite($fp, $contents); + fclose($fp); + } + } + + $output = rtrim(Request::base(true), '/') . '/app/cache/' . $client . '/' . $cachefile; + } + + return $output; + } +} diff --git a/core/libraries/Hubzero/Document/Base.php b/core/libraries/Hubzero/Document/Base.php new file mode 100644 index 00000000000..9817a6a6ce3 --- /dev/null +++ b/core/libraries/Hubzero/Document/Base.php @@ -0,0 +1,743 @@ +setLineEnd($options['lineend']); + } + + if (array_key_exists('charset', $options)) + { + $this->setCharset($options['charset']); + } + + if (array_key_exists('language', $options)) + { + $this->setLanguage($options['language']); + } + + if (array_key_exists('direction', $options)) + { + $this->setDirection($options['direction']); + } + + if (array_key_exists('tab', $options)) + { + $this->setTab($options['tab']); + } + + if (array_key_exists('link', $options)) + { + $this->setLink($options['link']); + } + + if (array_key_exists('base', $options)) + { + $this->setBase($options['base']); + } + } + + /** + * Get the contents of the document buffer + * + * @return string The contents of the document buffer + */ + public function getBuffer() + { + return self::$_buffer; + } + + /** + * Set the contents of the document buffer + * + * @param string $content The content to be set in the buffer. + * @param array $options Array of optional elements. + * @return object Document instance of $this to allow chaining + */ + public function setBuffer($content, $options = array()) + { + self::$_buffer = $content; + + return $this; + } + + /** + * Gets a meta tag. + * + * @param string $name Value of name or http-equiv tag + * @param boolean $httpEquiv META type "http-equiv" defaults to null + * @return string + */ + public function getMetaData($name, $httpEquiv = false) + { + $result = ''; + $name = strtolower($name); + if ($name == 'generator') + { + $result = $this->getGenerator(); + } + elseif ($name == 'description') + { + $result = $this->getDescription(); + } + else + { + if ($httpEquiv == true) + { + $result = @$this->_metaTags['http-equiv'][$name]; + } + else + { + $result = @$this->_metaTags['standard'][$name]; + } + } + + return $result; + } + + /** + * Sets or alters a meta tag. + * + * @param string $name Value of name or http-equiv tag + * @param string $content Value of the content tag + * @param boolean $http_equiv META type "http-equiv" defaults to null + * @param boolean $sync Should http-equiv="content-type" by synced with HTTP-header? + * @return object Document instance of $this to allow chaining + */ + public function setMetaData($name, $content, $http_equiv = false, $sync = true) + { + $name = strtolower($name); + + if ($name == 'generator') + { + $this->setGenerator($content); + } + elseif ($name == 'description') + { + $this->setDescription($content); + } + else + { + if ($http_equiv == true) + { + $this->_metaTags['http-equiv'][$name] = $content; + + // Syncing with HTTP-header + if ($sync && strtolower($name) == 'content-type') + { + $this->setMimeEncoding($content, false); + } + } + else + { + $this->_metaTags['standard'][] = array('name' => $name, 'content' => $content); + } + } + + return $this; + } + + /** + * Adds a linked script to the page + * + * @param string $url URL to the linked script + * @param string $type Type of script. Defaults to 'text/javascript' + * @param boolean $defer Adds the defer attribute. + * @param boolean $async Adds the async attribute. + * @return object Document instance of $this to allow chaining + */ + public function addScript($url, $type = "text/javascript", $defer = false, $async = false) + { + $this->_scripts[$url]['mime'] = $type; + $this->_scripts[$url]['defer'] = $defer; + $this->_scripts[$url]['async'] = $async; + + return $this; + } + + /** + * Adds a script to the page + * + * @param string $content Script + * @param string $type Scripting mime (defaults to 'text/javascript') + * @return object Document instance of $this to allow chaining + */ + public function addScriptDeclaration($content, $type = 'text/javascript') + { + if (!isset($this->_script[strtolower($type)])) + { + $this->_script[strtolower($type)] = array($content); + } + else + { + $this->_script[strtolower($type)][] = chr(13) . $content; + } + + return $this; + } + + /** + * Adds a linked stylesheet to the page + * + * @param string $url URL to the linked style sheet + * @param string $type Mime encoding type + * @param string $media Media type that this stylesheet applies to + * @param array $attribs Array of attributes + * @return object Document instance of $this to allow chaining + */ + public function addStyleSheet($url, $type = 'text/css', $media = null, $attribs = array()) + { + $this->_styleSheets[$url]['mime'] = $type; + $this->_styleSheets[$url]['media'] = $media; + $this->_styleSheets[$url]['attribs'] = $attribs; + + return $this; + } + + /** + * Adds a stylesheet declaration to the page + * + * @param string $content Style declarations + * @param string $type Type of stylesheet (defaults to 'text/css') + * @return object Document instance of $this to allow chaining + */ + public function addStyleDeclaration($content, $type = 'text/css') + { + if (!isset($this->_style[strtolower($type)])) + { + $this->_style[strtolower($type)] = $content; + } + else + { + $this->_style[strtolower($type)] .= chr(13) . $content; + } + + return $this; + } + + /** + * Sets the document charset + * + * @param string $type Charset encoding string + * @return object Document instance of $this to allow chaining + */ + public function setCharset($type = 'utf-8') + { + $this->_charset = $type; + + return $this; + } + + /** + * Returns the document charset encoding. + * + * @return string + */ + public function getCharset() + { + return $this->_charset; + } + + /** + * Sets the global document language declaration. Default is English (en-gb). + * + * @param string $lang The language to be set + * @return object Document instance of $this to allow chaining + */ + public function setLanguage($lang = 'en-gb') + { + $this->language = strtolower($lang); + + return $this; + } + + /** + * Returns the document language. + * + * @return string + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Sets the global document direction declaration. Default is left-to-right (ltr). + * + * @param string $dir The language direction to be set + * @return object Document instance of $this to allow chaining + */ + public function setDirection($dir = 'ltr') + { + $this->direction = strtolower($dir); + + return $this; + } + + /** + * Returns the document direction declaration. + * + * @return string + */ + public function getDirection() + { + return $this->direction; + } + + /** + * Sets the title of the document + * + * @param string $title The title to be set + * @return object Document instance of $this to allow chaining + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * Return the title of the document. + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Sets the base URI of the document + * + * @param string $base The base URI to be set + * @return object Document instance of $this to allow chaining + */ + public function setBase($base) + { + $this->base = $base; + + return $this; + } + + /** + * Return the base URI of the document. + * + * @return string + */ + public function getBase() + { + return $this->base; + } + + /** + * Sets the description of the document + * + * @param string $description The description to set + * @return object Document instance of $this to allow chaining + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Return the title of the page. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Sets the document link + * + * @param string $url A url + * @return object Document instance of $this to allow chaining + */ + public function setLink($url) + { + $this->link = $url; + + return $this; + } + + /** + * Returns the document base url + * + * @return string + */ + public function getLink() + { + return $this->link; + } + + /** + * Sets the document generator + * + * @param string $generator The generator to be set + * @return object Document instance of $this to allow chaining + */ + public function setGenerator($generator) + { + $this->_generator = $generator; + + return $this; + } + + /** + * Returns the document generator + * + * @return string + */ + public function getGenerator() + { + return $this->_generator; + } + + /** + * Sets the document modified date + * + * @param string $date The date to be set + * @return object Document instance of $this to allow chaining + */ + public function setModifiedDate($date) + { + $this->_mdate = $date; + + return $this; + } + + /** + * Returns the document modified date + * + * @return string + */ + public function getModifiedDate() + { + return $this->_mdate; + } + + /** + * Sets the document MIME encoding that is sent to the browser. + * + * @param string $type The document type to be sent + * @param boolean $sync Should the type be synced with HTML? + * @return object Document instance of $this to allow chaining + */ + public function setMimeEncoding($type = 'text/html', $sync = true) + { + $this->_mime = strtolower($type); + + // Syncing with meta-data + if ($sync) + { + $this->setMetaData('content-type', $type, true, false); + } + + return $this; + } + + /** + * Return the document MIME encoding that is sent to the browser. + * + * @return string + */ + public function getMimeEncoding() + { + return $this->_mime; + } + + /** + * Sets the line end style to Windows, Mac, Unix or a custom string. + * + * @param string $style "win", "mac", "unix" or custom string. + * @return object Document instance of $this to allow chaining + */ + public function setLineEnd($style) + { + switch ($style) + { + case 'win': + $this->_lineEnd = "\15\12"; + break; + + case 'unix': + $this->_lineEnd = "\12"; + break; + + case 'mac': + $this->_lineEnd = "\15"; + break; + + default: + $this->_lineEnd = $style; + } + + return $this; + } + + /** + * Returns the lineEnd + * + * @return string + */ + public function _getLineEnd() + { + return $this->_lineEnd; + } + + /** + * Sets the string used to indent HTML + * + * @param string $string String used to indent ("\11", "\t", ' ', etc.). + * @return object Document instance of $this to allow chaining + */ + public function setTab($string) + { + $this->_tab = $string; + + return $this; + } + + /** + * Returns a string containing the unit for indenting HTML + * + * @return string + */ + public function _getTab() + { + return $this->_tab; + } + + /** + * Load a renderer + * + * @param string $type The renderer type + * @return object Object or null if class does not exist + */ + public function loadRenderer($type) + { + $class = __NAMESPACE__ . '\\Type\\' . ucfirst($this->_type) . '\\' . ucfirst($type); + + if (!class_exists($class)) + { + throw new \InvalidArgumentException(\Lang::txt('Unable to load renderer class'), 500); + } + + return new $class($this); + } + + /** + * Parses the document and prepares the buffers + * + * @param array $params The array of parameters + * @return object Document instance of $this to allow chaining + */ + public function parse($params = array()) + { + return $this; + } + + /** + * Outputs the document + * + * @param boolean $cache If true, cache the output + * @param array $params Associative array of attributes + * @return The rendered data + */ + public function render($cache = false, $params = array()) + { + if ($mdate = $this->getModifiedDate()) + { + \App::get('response')->headers->set('Last-Modified', $mdate /* gmdate('D, d M Y H:i:s', time() + 900) . ' GMT' */); + } + + \App::get('response')->headers->set('Content-Type', $this->_mime . ($this->_charset ? '; charset=' . $this->_charset : '')); + } +} diff --git a/core/libraries/Hubzero/Document/Manager.php b/core/libraries/Hubzero/Document/Manager.php new file mode 100644 index 00000000000..5421c019955 --- /dev/null +++ b/core/libraries/Hubzero/Document/Manager.php @@ -0,0 +1,141 @@ +types = array(); + } + + /** + * Get a type instance. + * + * @param string $type + * @param array $options Associative array of options + * @return mixed + */ + public function instance($type = null, $options = array()) + { + $type = $type ?: $this->getType(); + + $signature = serialize(array($type, $options)); + + // If the given type has not been created before, we will create the instances + // here and cache it so we can return it next time very quickly. If there is + // already a type created by this name, we'll just return that instance. + if (!isset($this->types[$signature])) + { + try + { + $document = $this->createType($type, $options); + } + catch (Exception $e) + { + $document = $this->createType('html', $options); + } + + $this->types[$signature] = $document; + } + + return $this->types[$signature]; + } + + /** + * Create a new type instance. + * + * @param string $type + * @param array $options Associative array of options + * @return object + * @throws InvalidArgumentException + */ + protected function createType($type, $options = array()) + { + $type = preg_replace('/[^A-Z0-9_\.-]/i', '', $type); + + $class = __NAMESPACE__ . '\\Type\\' . ucfirst($type); + + if (!class_exists($class)) + { + throw new InvalidArgumentException("Type [$type] not supported."); + } + + return new $class($options); + } + + /** + * Get the current type + * + * @return string + */ + public function setType($type) + { + $this->type = (string) $type; + + return $this; + } + + /** + * Get the current type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Get all of the created "types". + * + * @return array + */ + public function getTypes() + { + return $this->types; + } + + /** + * Dynamically call the default type instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return call_user_func_array(array($this->instance(), $method), $parameters); + } +} diff --git a/core/libraries/Hubzero/Document/Renderer.php b/core/libraries/Hubzero/Document/Renderer.php new file mode 100644 index 00000000000..b0635942bae --- /dev/null +++ b/core/libraries/Hubzero/Document/Renderer.php @@ -0,0 +1,66 @@ +doc = &$doc; + } + + /** + * Renders a script and returns the results as a string + * + * @param string $name The name of the element to render + * @param array $params Array of values + * @param string $content Override the output of the renderer + * @return string The output of the script + */ + public function render($name, $params = null, $content = null) + { + // ... + } + + /** + * Return the content type of the renderer + * + * @return string The contentType + */ + public function getContentType() + { + return $this->mime; + } +} diff --git a/core/libraries/Hubzero/Document/Type/Error.php b/core/libraries/Hubzero/Document/Type/Error.php new file mode 100644 index 00000000000..d29ddd7a6ea --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Error.php @@ -0,0 +1,211 @@ +mime = 'text/html'; + + // Set document type + $this->type = 'error'; + } + + /** + * Set error object + * + * @param object $error Error object to set + * @return boolean True on success + */ + public function setError($error, $key = null) + { + if ($error instanceof Exception) + { + $this->error = $error; + return true; + } + + return false; + } + + /** + * Render the document + * + * @param boolean $cache If true, cache the output + * @param array $params Associative array of attributes + * @return string The rendered data + */ + public function render($cache = false, $params = array()) + { + // If no error object is set return null + if (!isset($this->error)) + { + return; + } + + // Set the status header + //\App::get('response')->headers->set('status', $this->error->getCode() . ' ' . str_replace("\n", ' ', $this->error->getMessage())); + + $file = 'error.php'; + + // Check template + $directory = isset($params['directory']) ? $params['directory'] : PATH_CORE . '/templates'; + $template = isset($params['template']) ? ltrim(preg_replace('/[^A-Z0-9_\.-]/i', '', (string) $params['template']), '.') : 'system'; + + if (!file_exists($directory . DS . $template . DS . $file)) + { + $directory = PATH_CORE . '/templates'; + $template = 'system'; + } + + // Set variables + $this->baseurl = (isset($params['baseurl']) ? $params['baseurl'] : rtrim(\Request::root(true), '/') . rtrim(substr(dirname($directory), strlen(PATH_ROOT)), '/')); + $this->template = $template; + $this->debug = isset($params['debug']) ? $params['debug'] : false; + + // Load + $data = $this->loadTemplate($directory . DS . $template, $file); + + parent::render(); + + return $data; + } + + /** + * Load a template file + * + * @param string $directory The name of the template + * @param string $filename The actual filename + * @return string The contents of the template + */ + protected function loadTemplate($directory, $filename) + { + $contents = ''; + + // Check to see if we have a valid template file + if (file_exists($directory . DS . $filename)) + { + // Store the file path + $this->file = $directory . DS . $filename; + + // Get the file content + ob_start(); + require_once $directory . DS . $filename; + $contents = ob_get_contents(); + ob_end_clean(); + } + + return $contents; + } + + /** + * Render the backtrace + * + * @return string The contents of the backtrace + */ + public function renderBacktrace() + { + $contents = null; + $backtrace = $this->error->getTrace(); + + if (is_array($backtrace)) + { + ob_start(); + + $j = 1; + + $html = array(); + $html[] = ''; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + for ($i = count($backtrace) - 1; $i >= 0; $i--) + { + $html[] = ' '; + $html[] = ' '; + if (isset($backtrace[$i]['class'])) + { + $html[] = ' '; + } + else + { + $html[] = ' '; + } + if (isset($backtrace[$i]['file'])) + { + $html[] = ' '; + } + else + { + $html[] = ' '; + } + $html[] = ' '; + $j++; + } + $html[] = ' '; + $html[] = '
Call stack
#FunctionLocation
0!! ' . $this->error->getMessage() . ' !!' . $this->rooted($this->error->getFile()) . ':' . $this->error->getLine() . '
' . $j . '' . $backtrace[$i]['class'] . '' . $backtrace[$i]['type'] . '' . $backtrace[$i]['function'] . '()' . $backtrace[$i]['function'] . '()' . $this->rooted($backtrace[$i]['file']) . ':' . $backtrace[$i]['line'] . ' 
'; + + echo "\n" . implode("\n", $html) . "\n"; + $contents = ob_get_contents(); + ob_end_clean(); + } + return $contents; + } + + /** + * Strip root path off to shorten lines some + * + * @param string $path + * @return string + */ + private function rooted($path) + { + if (substr($path, 0, strlen(PATH_ROOT)) == PATH_ROOT) + { + $path = 'ROOT' . substr($path, strlen(PATH_ROOT)); + } + return $path; + } +} diff --git a/core/libraries/Hubzero/Document/Type/Feed.php b/core/libraries/Hubzero/Document/Type/Feed.php new file mode 100644 index 00000000000..25f449e0395 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Feed.php @@ -0,0 +1,277 @@ +_type = 'feed'; + } + + /** + * Render the document + * + * @param boolean $cache If true, cache the output + * @param array $params Associative array of attributes + * @return The rendered data + */ + public function render($cache = false, $params = array()) + { + // Get the feed type + $type = \Request::getCmd('type', 'Rss'); + + // Instantiate feed renderer and set the mime encoding + $renderer = $this->loadRenderer(($type) ? $type : 'rss'); + + if (!($renderer instanceof Renderer)) + { + \App::abort(404, \Lang::txt('Resource Not Found')); + } + + $this->setMimeEncoding($renderer->getContentType()); + + // output + // Generate prolog + $data = '_charset . '"?>' . "\n"; + $data .= '' . "\n"; + + // Generate stylesheet links + foreach ($this->_styleSheets as $src => $attr) + { + $data .= '' . "\n"; + } + + // Render the feed + $data .= $renderer->render(); + + parent::render(); + + return $data; + } + + /** + * Adds an Item to the feed. + * + * @param object &$item The feeditem to add to the feed. + * @return object instance of $this to allow chaining + */ + public function addItem(Item $item) + { + $item->source = $this->link; + + $this->items[] = $item; + + return $this; + } +} diff --git a/core/libraries/Hubzero/Document/Type/Feed/Atom.php b/core/libraries/Hubzero/Document/Type/Feed/Atom.php new file mode 100644 index 00000000000..74387912ca9 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Feed/Atom.php @@ -0,0 +1,175 @@ +doc; + + $url = App::get('request')->root(); + + $tz = new \DateTimeZone(App::get('config')->get('offset')); + + $syndicationURL = App::get('router')->url('&format=feed&type=atom'); + + if (App::get('config')->get('sitename_pagetitles', 0) == 1) + { + $data->title = App::get('language')->txt('JPAGETITLE', App::get('config')->get('sitename'), $data->title); + } + elseif (App::get('config')->get('sitename_pagetitles', 0) == 2) + { + $data->title = App::get('language')->txt('JPAGETITLE', $data->title, App::get('config')->get('sitename')); + } + + $feed_title = htmlspecialchars($data->title, ENT_COMPAT, 'UTF-8'); + + $feed = "language != "") + { + $feed .= " xml:lang=\"" . $data->language . "\""; + } + $feed .= ">\n"; + $feed .= " " . $feed_title . "\n"; + $feed .= " " . htmlspecialchars($data->description, ENT_COMPAT, 'UTF-8') . "\n"; + if (empty($data->category) === false) + { + if (is_array($data->category)) + { + foreach ($data->category as $cat) + { + $feed .= " \n"; + } + } + else + { + $feed .= " category, ENT_COMPAT, 'UTF-8') . "\" />\n"; + } + } + $feed .= " \n"; + $feed .= " " . str_replace(' ', '%20', $data->getBase()) . "\n"; + $feed .= " " . htmlspecialchars($now->toISO8601(true), ENT_COMPAT, 'UTF-8') . "\n"; + if ($data->editor != "") + { + $feed .= " \n"; + $feed .= " " . $data->editor . "\n"; + if ($data->editorEmail != "") + { + $feed .= " " . htmlspecialchars($data->editorEmail, ENT_COMPAT, 'UTF-8') . "\n"; + } + $feed .= " \n"; + } + $feed .= " " . $data->getGenerator() . "\n"; + $feed .= ' \n"; + + for ($i = 0, $count = count($data->items); $i < $count; $i++) + { + $feed .= " \n"; + $feed .= " " . htmlspecialchars(strip_tags($data->items[$i]->title), ENT_COMPAT, 'UTF-8') . "\n"; + $feed .= ' \n"; + + if ($data->items[$i]->date == "") + { + $data->items[$i]->date = $now->toUnix(); + } + $itemDate = new Date($data->items[$i]->date); + $itemDate->setTimeZone($tz); + + $feed .= " " . htmlspecialchars($itemDate->toISO8601(true), ENT_COMPAT, 'UTF-8') . "\n"; + $feed .= " " . htmlspecialchars($itemDate->toISO8601(true), ENT_COMPAT, 'UTF-8') . "\n"; + if (empty($data->items[$i]->guid) === true) + { + $feed .= " " . str_replace(' ', '%20', $url . $data->items[$i]->link) . "\n"; + } + else + { + $feed .= " " . htmlspecialchars($data->items[$i]->guid, ENT_COMPAT, 'UTF-8') . "\n"; + } + + if ($data->items[$i]->author != "") + { + $feed .= " \n"; + $feed .= " " . htmlspecialchars($data->items[$i]->author, ENT_COMPAT, 'UTF-8') . "\n"; + if ($data->items[$i]->authorEmail != "") + { + $feed .= " " . htmlspecialchars($data->items[$i]->authorEmail, ENT_COMPAT, 'UTF-8') . "\n"; + } + $feed .= " \n"; + } + if ($data->items[$i]->description != "") + { + $feed .= " " . htmlspecialchars($data->items[$i]->description, ENT_COMPAT, 'UTF-8') . "\n"; + $feed .= " " . htmlspecialchars($data->items[$i]->description, ENT_COMPAT, 'UTF-8') . "\n"; + } + if (empty($data->items[$i]->category) === false) + { + if (is_array($data->items[$i]->category)) + { + foreach ($data->items[$i]->category as $cat) + { + $feed .= " \n"; + } + } + else + { + $feed .= " items[$i]->category, ENT_COMPAT, 'UTF-8') . "\" />\n"; + } + } + if ($data->items[$i]->enclosure != null) + { + $feed .= " items[$i]->enclosure->url . "\" type=\"" . $data->items[$i]->enclosure->type . "\" length=\"" . $data->items[$i]->enclosure->length . "\" />\n"; + } + $feed .= " \n"; + } + $feed .= "\n"; + return $feed; + } + + /** + * Escape text + * + * @param string $text + * @return string + */ + public function escape($text) + { + return htmlspecialchars($text, ENT_COMPAT, 'UTF-8'); + } +} diff --git a/core/libraries/Hubzero/Document/Type/Feed/Enclosure.php b/core/libraries/Hubzero/Document/Type/Feed/Enclosure.php new file mode 100644 index 00000000000..c6e31224889 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Feed/Enclosure.php @@ -0,0 +1,39 @@ +enclosure = $enclosure; + } +} diff --git a/core/libraries/Hubzero/Document/Type/Feed/ItunesItem.php b/core/libraries/Hubzero/Document/Type/Feed/ItunesItem.php new file mode 100644 index 00000000000..3879c2a3739 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Feed/ItunesItem.php @@ -0,0 +1,70 @@ +doc; + + $url = rtrim(\App::get('request')->root(), '/') . '/'; + + if (\App::get('config')->get('sitename_pagetitles', 0) == 1) + { + $data->title = \App::get('language')->txt('JPAGETITLE', \App::get('config')->get('sitename'), $data->title); + } + elseif (\App::get('config')->get('sitename_pagetitles', 0) == 2) + { + $data->title = \App::get('language')->txt('JPAGETITLE', $data->title, \App::get('config')->get('sitename')); + } + + $feed = '' . "\n"; + //$feed = "\n"; + $feed .= ' ' . "\n"; + $feed .= ' ' . $data->title . '' . "\n"; + $feed .= ' description . ']]>' . "\n"; + $feed .= ' ' . str_replace(' ', '%20', $url . ltrim($data->link, '/')) . '' . "\n"; + $feed .= ' ' . $this->escape($now->toRFC822()) . '' . "\n"; + $feed .= ' ' . $data->getGenerator() . '' . "\n"; + + // iTunes specific tags + if ($data->itunes_summary != '') + { + $feed .= ' ' . $this->escape($data->itunes_summary) . '' . "\n"; + } + if ($data->itunes_category != '') + { + $feed .= ' ' . "\n"; + if ($data->itunes_subcategories != null) + { + $cats = $data->itunes_subcategories; + foreach ($cats as $cat) + { + $feed .= ' ' . "\n"; + } + } + $feed .= ' ' . "\n"; + } + if ($data->itunes_owner != null) + { + $feed .= ' ' . "\n"; + $feed .= ' ' . $this->escape($data->itunes_owner->name) . '' . "\n"; + $feed .= ' ' . $data->itunes_owner->email . '' . "\n"; + $feed .= ' ' . "\n"; + } + if ($data->itunes_explicit != '') + { + $feed .= ' ' . $data->itunes_explicit . '' . "\n"; + } + if ($data->itunes_keywords != '') + { + $feed .= ' ' . $this->escape($data->itunes_keywords) . '' . "\n"; + } + if ($data->itunes_author != '') + { + $feed .= ' ' . $this->escape($data->itunes_author) . '' . "\n"; + } + if ($data->itunes_image != null) + { + $feed .= ' ' . "\n"; + } + // end iTunes specific tags + + if ($data->image != null) + { + $feed .= ' ' . "\n"; + $feed .= ' ' . $data->image->url . '' . "\n"; + $feed .= ' ' . $this->escape($data->image->title) . '' . "\n"; + $feed .= ' ' . str_replace(' ', '%20', $data->image->link) . '' . "\n"; + if ($data->image->width != "") + { + $feed .= ' ' . $data->image->width . '' . "\n"; + } + if ($data->image->height != '') + { + $feed .= ' ' . $data->image->height . '' . "\n"; + } + if ($data->image->description != '') + { + $feed .= ' image->description . ']]>' . "\n"; + } + $feed .= ' ' . "\n"; + } + if ($data->language != '') + { + $feed .= " " . $data->language . "\n"; + } + if ($data->copyright != '') + { + $feed .= " " . $this->escape($data->copyright) . "\n"; + } + if ($data->editor != '') + { + $feed .= " " . $this->escape($data->editor) . "\n"; + } + if ($data->webmaster != '') + { + $feed .= " " . $this->escape($data->webmaster) . "\n"; + } + if ($data->pubDate != '') + { + $pubDate = new Date($data->pubDate); + $feed .= " " . $this->escape($pubDate->toRFC822()) . "\n"; + } + if ($data->category) + { + if (!is_array($data->category)) + { + $data->category = array($data->category); + } + + foreach ($data->category as $category) + { + $feed .= " " . $this->escape($category) . "\n"; + } + } + if ($data->docs != '') + { + $feed .= " " . $this->escape($data->docs) . "\n"; + } + if ($data->ttl != '') + { + $feed .= " " . $this->escape($data->ttl) . "\n"; + } + if ($data->rating != '') + { + $feed .= " " . $this->escape($data->rating) . "\n"; + } + if ($data->skipHours != '') + { + $feed .= " " . $this->escape($data->skipHours) . "\n"; + } + if ($data->skipDays != '') + { + $feed .= " " . $this->escape($data->skipDays) . "\n"; + } + + for ($i=0; $iitems); $i++) + { + if ((strpos($data->items[$i]->link, 'http://') === false) and (strpos($data->items[$i]->link, 'https://') === false)) + { + $data->items[$i]->link = str_replace(' ', '%20', $url . ltrim($data->items[$i]->link, '/')); + } + + $feed .= " \n"; + $feed .= " " . $this->escape(strip_tags($data->items[$i]->title)) . "\n"; + $feed .= " " . str_replace(' ', '%20', $data->items[$i]->link) . "\n"; + $feed .= " " . $this->_relToAbs($data->items[$i]->description) . "\n"; + + if (empty($data->items[$i]->guid) === true) + { + $feed .= " " . str_replace(' ', '%20', $data->items[$i]->link) . "\n"; + } + else + { + $feed .= " " . $this->escape($data->items[$i]->guid) . "\n"; + } + + // iTunes specific tags + if ($data->items[$i]->itunes_summary != '') + { + $feed .= " " . $this->escape($data->items[$i]->itunes_summary) . "\n"; + } + if ($data->items[$i]->itunes_duration != '') + { + $feed .= " " . $this->escape($data->items[$i]->itunes_duration) . "\n"; + } + if ($data->items[$i]->itunes_explicit != '') + { + $feed .= " " . $data->items[$i]->itunes_explicit . "\n"; + } + if ($data->items[$i]->itunes_keywords != '') + { + $feed .= " " . $this->escape($data->items[$i]->itunes_keywords) . "\n"; + } + if ($data->items[$i]->itunes_author != '') + { + $feed .= " " . $this->escape($data->items[$i]->itunes_author) . "\n"; + } + if ($data->items[$i]->itunes_category != '') + { + $feed .= " escape($data->items[$i]->itunes_category) . "\">\n"; + if ($data->items[$i]->itunes_subcategories != '') + { + $icats = $data->items[$i]->itunes_subcategories; + foreach ($icats as $icat) + { + $feed .= " escape($icat) . "\">\n"; + } + } + $feed .= " \n"; + } + if ($data->items[$i]->itunes_image != null) + { + $feed .= " \n"; + $feed .= " " . $data->items[$i]->itunes_image->url . "\n"; + $feed .= " " . $this->escape($data->items[$i]->itunes_image->title) . "\n"; + $feed .= " " . $data->items[$i]->itunes_image->link . "\n"; + if ($data->items[$i]->itunes_image->width != '') + { + $feed .= " " . $data->items[$i]->itunes_image->width . "\n"; + } + if ($data->items[$i]->itunes_image->height != '') + { + $feed .= " " . $data->items[$i]->itunes_image->height . "\n"; + } + if ($data->items[$i]->itunes_image->description != '') + { + $feed .= " items[$i]->itunes_image->description . "]]>\n"; + } + $feed .= " \n"; + } + // end iTunes specific tags + + if ($data->items[$i]->author != '') + { + $feed .= " " . $this->escape($data->items[$i]->author) . "\n"; + } + if ($data->items[$i]->category) + { + if (!is_array($data->items[$i]->category)) + { + $data->items[$i]->category = array($data->items[$i]->category); + } + + foreach ($data->items[$i]->category as $category) + { + $feed .= " " . $this->escape($category) . "\n"; + } + } + if ($data->items[$i]->comments != '') + { + $feed .= " " . $this->escape($data->items[$i]->comments) . "\n"; + } + if ($data->items[$i]->date != '') + { + $itemDate = new Date($data->items[$i]->date); + $feed .= " " . $this->escape($itemDate->toRFC822()) . "\n"; + } + if ($data->items[$i]->guid != '') + { + $feed .= " " . $this->escape($data->items[$i]->guid) . "\n"; + } + if ($data->items[$i]->enclosure != null) + { + $feed .= ' ' . "\n"; + } + + $feed .= " \n"; + } + $feed .= " \n"; + $feed .= "\n"; + + return $feed; + } + + /** + * Convert links in a text from relative to absolute + * + * @return string + */ + private function _relToAbs($text) + { + $base = \App::get('request')->base(); + $text = preg_replace("/(href|src)=\"(?!http|ftp|https)([^\"]*)\"/", "$1=\"$base\$2\"", $text); + + return $text; + } + + /** + * Escape text + * + * @param string $text + * @return string + */ + public function escape($text) + { + return htmlspecialchars($text, ENT_COMPAT, 'UTF-8'); + } +} diff --git a/core/libraries/Hubzero/Document/Type/Html.php b/core/libraries/Hubzero/Document/Type/Html.php new file mode 100644 index 00000000000..36c9b1371b9 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Html.php @@ -0,0 +1,603 @@ + tags + * + * @var array + */ + public $_links = array(); + + /** + * Array of custom tags + * + * @var array + */ + public $_custom = array(); + + /** + * Name of the template + * + * @var string + */ + public $template = null; + + /** + * Base url + * + * @var string + */ + public $baseurl = null; + + /** + * Array of template parameters + * + * @var array + */ + public $params = null; + + /** + * File name + * + * @var array + */ + public $_file = null; + + /** + * String holding parsed template + * + * @var string + */ + protected $_template = ''; + + /** + * Array of parsed template tags + * + * @var array + */ + protected $_template_tags = array(); + + /** + * Integer with caching setting + * + * @var integer + */ + protected $_caching = null; + + /** + * Class constructor + * + * @param array $options Associative array of options + * @return void + */ + public function __construct($options = array()) + { + parent::__construct($options); + + // Set document type + $this->_type = 'html'; + + // Set default mime type and document metadata (meta data syncs with mime type by default) + $this->setMimeEncoding('text/html'); + } + + /** + * Get the HTML document head data + * + * @return array The document head data in array form + */ + public function getHeadData() + { + $data = array(); + $data['title'] = $this->title; + $data['description'] = $this->description; + $data['link'] = $this->link; + $data['metaTags'] = $this->_metaTags; + $data['links'] = $this->_links; + $data['styleSheets'] = $this->_styleSheets; + $data['style'] = $this->_style; + $data['scripts'] = $this->_scripts; + $data['script'] = $this->_script; + $data['custom'] = $this->_custom; + return $data; + } + + /** + * Set the HTML document head data + * + * @param array $data The document head data in array form + * @return object instance of $this to allow chaining + */ + public function setHeadData($data) + { + if (empty($data) || !is_array($data)) + { + return; + } + + $this->title = (isset($data['title']) && !empty($data['title'])) ? $data['title'] : $this->title; + $this->description = (isset($data['description']) && !empty($data['description'])) ? $data['description'] : $this->description; + $this->link = (isset($data['link']) && !empty($data['link'])) ? $data['link'] : $this->link; + + $this->_metaTags = (isset($data['metaTags']) && !empty($data['metaTags'])) ? $data['metaTags'] : $this->_metaTags; + $this->_links = (isset($data['links']) && !empty($data['links'])) ? $data['links'] : $this->_links; + $this->_styleSheets = (isset($data['styleSheets']) && !empty($data['styleSheets'])) ? $data['styleSheets'] : $this->_styleSheets; + $this->_style = (isset($data['style']) && !empty($data['style'])) ? $data['style'] : $this->_style; + $this->_scripts = (isset($data['scripts']) && !empty($data['scripts'])) ? $data['scripts'] : $this->_scripts; + $this->_script = (isset($data['script']) && !empty($data['script'])) ? $data['script'] : $this->_script; + $this->_custom = (isset($data['custom']) && !empty($data['custom'])) ? $data['custom'] : $this->_custom; + + return $this; + } + + /** + * Merge the HTML document head data + * + * @param array $data The document head data in array form + * @return object instance of $this to allow chaining + */ + public function mergeHeadData($data) + { + if (empty($data) || !is_array($data)) + { + return; + } + + $this->title = (isset($data['title']) && !empty($data['title']) && !stristr($this->title, $data['title'])) + ? $this->title . $data['title'] + : $this->title; + $this->description = (isset($data['description']) && !empty($data['description']) && !stristr($this->description, $data['description'])) + ? $this->description . $data['description'] + : $this->description; + $this->link = (isset($data['link'])) ? $data['link'] : $this->link; + + if (isset($data['metaTags'])) + { + foreach ($data['metaTags'] as $type1 => $data1) + { + $booldog = $type1 == 'http-equiv' ? true : false; + foreach ($data1 as $name2 => $data2) + { + $this->setMetaData($name2, $data2, $booldog); + } + } + } + + $this->_links = (isset($data['links']) && !empty($data['links']) && is_array($data['links'])) + ? array_unique(array_merge($this->_links, $data['links'])) + : $this->_links; + + $this->_styleSheets = (isset($data['styleSheets']) && !empty($data['styleSheets']) && is_array($data['styleSheets'])) + ? array_merge($this->_styleSheets, $data['styleSheets']) + : $this->_styleSheets; + + if (isset($data['style'])) + { + foreach ($data['style'] as $type => $stdata) + { + if (!isset($this->_style[strtolower($type)]) || !stristr($this->_style[strtolower($type)], $stdata)) + { + $this->addStyleDeclaration($stdata, $type); + } + } + } + + $this->_scripts = (isset($data['scripts']) && !empty($data['scripts']) && is_array($data['scripts'])) + ? array_merge($this->_scripts, $data['scripts']) + : $this->_scripts; + + if (isset($data['script'])) + { + foreach ($data['script'] as $type => $sdata) + { + if (!isset($this->_script[strtolower($type)]) || !stristr($this->_script[strtolower($type)], $sdata)) + { + $this->addScriptDeclaration($sdata, $type); + } + } + } + + $this->_custom = (isset($data['custom']) && !empty($data['custom']) && is_array($data['custom'])) + ? array_unique(array_merge($this->_custom, $data['custom'])) + : $this->_custom; + + return $this; + } + + /** + * Adds tags to the head of the document + * + * $relType defaults to 'rel' as it is the most common relation type used. + * ('rev' refers to reverse relation, 'rel' indicates normal, forward relation.) + * Typical tag: + * + * @param string $href The link that is being related. + * @param string $relation Relation of link. + * @param string $relType Relation type attribute. Either rel or rev (default: 'rel'). + * @param array $attribs Associative array of remaining attributes. + * @return object instance of $this to allow chaining + */ + public function addHeadLink($href, $relation, $relType = 'rel', $attribs = array()) + { + $this->_links[$href]['relation'] = $relation; + $this->_links[$href]['relType'] = $relType; + $this->_links[$href]['attribs'] = $attribs; + + return $this; + } + + /** + * Adds a shortcut icon (favicon) + * + * This adds a link to the icon shown in the favorites list or on + * the left of the url in the address bar. Some browsers display + * it on the tab, as well. + * + * @param string $href The link that is being related. + * @param string $type File type + * @param string $relation Relation of link + * @return object instance of $this to allow chaining + */ + public function addFavicon($href, $type = 'image/vnd.microsoft.icon', $relation = 'shortcut icon') + { + $href = str_replace('\\', '/', $href); + $this->addHeadLink($href, $relation, 'rel', array('type' => $type)); + + return $this; + } + + /** + * Adds a custom HTML string to the head block + * + * @param string $html The HTML to add to the head + * @return object instance of $this to allow chaining + */ + public function addCustomTag($html) + { + $this->_custom[] = trim($html); + + return $this; + } + + /** + * Get the contents of a document include + * + * @param string $type The type of renderer + * @param string $name The name of the element to render + * @param array $attribs Associative array of remaining attributes. + * @return The output of the renderer + */ + public function getBuffer($type = null, $name = null, $attribs = array()) + { + // If no type is specified, return the whole buffer + if ($type === null) + { + return parent::$_buffer; + } + + $result = null; + if (isset(parent::$_buffer[$type][$name])) + { + return parent::$_buffer[$type][$name]; + } + + // If the buffer has been explicitly turned off don't display or attempt to render + if ($result === false) + { + return null; + } + + $renderer = $this->loadRenderer($type); + + $this->setBuffer($renderer->render($name, $attribs, $result), $type, $name); + + return parent::$_buffer[$type][$name]; + } + + /** + * Set the contents a document includes + * + * @param string $content The content to be set in the buffer. + * @param array $options Array of optional elements. + * @return object instance of $this to allow chaining + */ + public function setBuffer($content, $options = array()) + { + // The following code is just for backward compatibility. + if (func_num_args() > 1 && !is_array($options)) + { + $args = func_get_args(); + $options = array(); + $options['type'] = $args[1]; + $options['name'] = (isset($args[2])) ? $args[2] : null; + } + + parent::$_buffer[$options['type']][$options['name']] = $content; + + return $this; + } + + /** + * Parses the template and populates the buffer + * + * @param array $params Parameters for fetching the template + * @return object instance of $this to allow chaining + */ + public function parse($params = array()) + { + return $this->_fetchTemplate($params)->_parseTemplate(); + } + + /** + * Outputs the template to the browser. + * + * @param boolean $caching If true, cache the output + * @param array $params Associative array of attributes + * @return object The rendered data + */ + public function render($caching = false, $params = array()) + { + $this->_caching = $caching; + + if (!empty($this->_template)) + { + $data = $this->_renderTemplate(); + } + else + { + $this->parse($params); + $data = $this->_renderTemplate(); + } + + parent::render(); + return $data; + } + + /** + * Count the modules based on the given condition + * + * @param string $condition The condition to use + * @return integer Number of modules found + */ + public function countModules($condition) + { + $operators = '(\+|\-|\*|\/|==|\!=|\<\>|\<|\>|\<=|\>=|and|or|xor)'; + $words = preg_split('# ' . $operators . ' #', $condition, null, PREG_SPLIT_DELIM_CAPTURE); + for ($i = 0, $n = count($words); $i < $n; $i += 2) + { + // odd parts (modules) + $name = strtolower($words[$i]); + $words[$i] = ((isset(parent::$_buffer['modules'][$name])) && (parent::$_buffer['modules'][$name] === false)) + ? 0 + : count(\Module::byPosition($name)); + } + + $str = 'return ' . implode(' ', $words) . ';'; + + return eval($str); + } + + /** + * Count the number of child menu items + * + * @return integer Number of child menu items + */ + public function countMenuChildren() + { + static $children; + + if (!isset($children)) + { + $menu = \App::get('menu'); + $active = $menu->getActive(); + if ($active) + { + $dbo = \App::get('db'); + + $query = $dbo->getQuery(); + $query + ->select('id', null, true) + ->from('#__menu') + ->whereEquals('parent_id', $active->id) + ->whereEquals('published', '1'); + + $dbo->setQuery($query->toString()); + $children = $dbo->loadResult(); + } + else + { + $children = 0; + } + } + + return $children; + } + + /** + * Load a template file + * + * @param string $directory The name of the template + * @param string $filename The actual filename + * @return string The contents of the template + */ + protected function _loadTemplate($directory, $filename) + { + $contents = ''; + + // Check to see if we have a valid template file + if (file_exists($directory . DS . $filename)) + { + // Store the file path + $this->_file = $directory . DS . $filename; + + //get the file content + ob_start(); + require $directory . DS . $filename; + $contents = ob_get_contents(); + ob_end_clean(); + } + + // Try to find a favicon by checking the template and root folder + $dirs = array( + $directory . DS, + PATH_ROOT . DS + ); + + foreach ($dirs as $dir) + { + $icon = $dir . 'favicon.ico'; + + if (file_exists($icon)) + { + $path = str_replace(PATH_ROOT . '/', '', $dir); + $path = str_replace('\\', '/', $path); + + $this->addFavicon(rtrim(\Request::root(true), '/') . '/' . $path . 'favicon.ico'); + break; + } + } + + return $contents; + } + + /** + * Fetch the template, and initialise the params + * + * @param array $params Parameters to determine the template + * @return object instance of $this to allow chaining + */ + protected function _fetchTemplate($params = array()) + { + // Check + $directory = isset($params['directory']) ? $params['directory'] : PATH_CORE . '/templates'; + + $template = isset($params['template']) ? preg_replace('/[^A-Z0-9_\.-]/i', '', $params['template']) : 'system'; + $file = isset($params['file']) ? preg_replace('/[^A-Z0-9_\.-]/i', '', $params['file']) : 'index.php'; + + if (!file_exists($directory . DS . $template . DS . $file)) + { + $directory = PATH_CORE . '/templates'; + $template = 'system'; + $params['baseurl'] = str_replace('/app', '/core', $params['baseurl']); + } + + // Load the language file for the template + $lang = \App::get('language'); + $lang->load('tpl_' . $template, PATH_APP . DS . 'bootstrap' . DS . \App::get('client')->name, null, false, true) || + $lang->load('tpl_' . $template, $directory . DS . $template, null, false, true); + + // Assign the variables + $this->template = $template; + //$this->path = (isset($params['path']) ? $params['path'] : rtrim(\Request::root(true), '/')) . '/templates/' . $template; + //$this->baseurl = rtrim(\Request::root(true), '/'); + $this->baseurl = isset($params['baseurl']) ? $params['baseurl'] : rtrim(\Request::root(true), '/'); + $this->params = isset($params['params']) ? $params['params'] : new Registry; + + // Load + $this->_template = $this->_loadTemplate($directory . DS . $template, $file); + + return $this; + } + + /** + * Parse a document template + * + * @return object instance of $this to allow chaining + */ + protected function _parseTemplate() + { + $matches = array(); + + if (preg_match_all('##iU', $this->_template, $matches)) + { + $template_tags_first = array(); + $template_tags_last = array(); + + // Step through the jdocs in reverse order. + for ($i = count($matches[0]) - 1; $i >= 0; $i--) + { + $type = $matches[1][$i]; + $attribs = empty($matches[2][$i]) ? array() : $this->parseAttributes($matches[2][$i]); + $name = isset($attribs['name']) ? $attribs['name'] : null; + + // Separate buffers to be executed first and last + if ($type == 'module' || $type == 'modules') + { + $template_tags_first[$matches[0][$i]] = array('type' => $type, 'name' => $name, 'attribs' => $attribs); + } + else + { + $template_tags_last[$matches[0][$i]] = array('type' => $type, 'name' => $name, 'attribs' => $attribs); + } + } + // Reverse the last array so the jdocs are in forward order. + $template_tags_last = array_reverse($template_tags_last); + + $this->_template_tags = $template_tags_first + $template_tags_last; + } + + return $this; + } + + /** + * Method to extract key/value pairs out of a string with XML style attributes + * + * @param string $string String containing XML style attributes + * @return array Key/Value pairs for the attributes + */ + private function parseAttributes($string) + { + // Initialise variables. + $attr = array(); + $ret = array(); + + // Let's grab all the key/value pairs using a regular expression + preg_match_all('/([\w:-]+)[\s]?=[\s]?"([^"]*)"/i', $string, $attr); + + if (is_array($attr)) + { + $numPairs = count($attr[1]); + for ($i = 0; $i < $numPairs; $i++) + { + $ret[$attr[1][$i]] = $attr[2][$i]; + } + } + + return $ret; + } + + /** + * Render pre-parsed template + * + * @return string rendered template + */ + protected function _renderTemplate() + { + $replace = array(); + $with = array(); + + foreach ($this->_template_tags as $jdoc => $args) + { + $replace[] = $jdoc; + $with[] = $this->getBuffer($args['type'], $args['name'], $args['attribs']); + } + + return str_replace($replace, $with, $this->_template); + } +} diff --git a/core/libraries/Hubzero/Document/Type/Html/Component.php b/core/libraries/Hubzero/Document/Type/Html/Component.php new file mode 100644 index 00000000000..9ee765e5f47 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Html/Component.php @@ -0,0 +1,31 @@ +fetchHead($this->doc); + $buffer = ob_get_contents(); + ob_end_clean(); + + return $buffer; + } + + /** + * Generates the head HTML and return the results as a string + * + * @param object &$document The document for which the head will be created + * @return string The head hTML + */ + public function fetchHead(&$document) + { + // Trigger the onBeforeCompileHead event (skip for installation, since it causes an error) + \Event::trigger('onBeforeCompileHead'); + + // Get line endings + $lnEnd = $document->_getLineEnd(); + $tab = $document->_getTab(); + $tagEnd = ' />'; + $buffer = array(); + + // Generate base tag (need to happen first) + $base = $document->getBase(); + if (!empty($base)) + { + $buffer[] = $tab . ''; + } + + // Generate META tags (needs to happen as early as possible in the head) + foreach ($document->_metaTags as $type => $tag) + { + foreach ($tag as $name => $content) + { + if ($type == 'http-equiv') + { + $content .= '; charset=' . $document->getCharset(); + $buffer[] = $tab . ''; + } + elseif ($type == 'standard' && !empty($content) && isset($content['content'])) + { + $buffer[] = $tab . ''; + } + } + } + + // Don't add empty descriptions + if ($description = $document->getDescription()) + { + $buffer[] = $tab . ''; + } + + // Don't add empty generators + if ($generator = $document->getGenerator()) + { + $buffer[] = $tab . ''; + } + + $buffer[] = $tab . '' . htmlspecialchars($document->getTitle(), ENT_COMPAT, 'UTF-8') . ''; + + // Generate link declarations + foreach ($document->_links as $link => $linkAtrr) + { + $line = $tab . '_styleSheets as $strSrc => $strAttr) + { + $line = $tab . '_style as $type => $content) + { + $buffer[] = $tab . ''; + } + + // Generate script file links + foreach ($document->_scripts as $strSrc => $strAttr) + { + $line = $tab . ''; + } + + // Generate script language declarations. + if (count(\Lang::script())) + { + $buffer[] = $tab . ''; + } + + foreach ($document->_custom as $custom) + { + $buffer[] = $tab . $custom; + } + + return implode($lnEnd, $buffer); + } +} diff --git a/core/libraries/Hubzero/Document/Type/Html/Message.php b/core/libraries/Hubzero/Document/Type/Html/Message.php new file mode 100644 index 00000000000..c2eb3c62d87 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Html/Message.php @@ -0,0 +1,80 @@ +messages(); + + // Build the sorted message list + if (is_array($messages) && !empty($messages)) + { + foreach ($messages as $msg) + { + if (isset($msg['type']) && isset($msg['message'])) + { + $lists[$msg['type']][] = $msg['message']; + } + } + } + + $lnEnd = $this->doc->_getLineEnd(); + $tab = $this->doc->_getTab(); + + // Build the return string + $buffer[] = '
'; + + // If messages exist render them + if (!empty($lists)) + { + $buffer[] = $tab . '
'; + foreach ($lists as $type => $msgs) + { + if (count($msgs)) + { + $buffer[] = $tab . $tab . '
' . \App::get('language')->txt($type) . '
'; + $buffer[] = $tab . $tab . '
'; + $buffer[] = $tab . $tab . $tab . '
    '; + foreach ($msgs as $msg) + { + $buffer[] = $tab . $tab . $tab . $tab . '
  • ' . $msg . '
  • '; + } + $buffer[] = $tab . $tab . $tab . '
'; + $buffer[] = $tab . $tab . '
'; + } + } + $buffer[] = $tab . '
'; + } + + $buffer[] = '
'; + + return implode($lnEnd, $buffer); + } +} diff --git a/core/libraries/Hubzero/Document/Type/Html/Module.php b/core/libraries/Hubzero/Document/Type/Html/Module.php new file mode 100644 index 00000000000..2ceff2c583f --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Html/Module.php @@ -0,0 +1,80 @@ +byName($module, $title); + + if (!is_object($module)) + { + if (is_null($content)) + { + return ''; + } + else + { + // If module isn't found in the database but data has been pushed in the buffer + // we want to render it + $tmp = $module; + + $module = new stdClass; + $module->params = null; + $module->module = $tmp; + $module->id = 0; + $module->user = 0; + } + } + } + + // Set the module content + if (!is_null($content)) + { + $module->content = $content; + } + + // Get module parameters + $params = new Registry($module->params); + + // Use parameters from template + if (isset($attribs['params'])) + { + $template_params = new Registry(html_entity_decode($attribs['params'], ENT_COMPAT, 'UTF-8')); + + $params->merge($template_params); + + $module = clone $module; + $module->params = (string) $params; + } + + return \App::get('module')->render($module, $attribs); + } +} diff --git a/core/libraries/Hubzero/Document/Type/Html/Modules.php b/core/libraries/Hubzero/Document/Type/Html/Modules.php new file mode 100644 index 00000000000..dce67b30ae7 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Html/Modules.php @@ -0,0 +1,39 @@ +doc->loadRenderer('module'); + + $buffer = ''; + foreach (\App::get('module')->byPosition($position) as $mod) + { + $buffer .= $renderer->render($mod, $params, $content); + } + + return $buffer; + } +} diff --git a/core/libraries/Hubzero/Document/Type/Json.php b/core/libraries/Hubzero/Document/Type/Json.php new file mode 100644 index 00000000000..e57789bf0af --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Json.php @@ -0,0 +1,83 @@ +mime = 'application/json'; + + // Set document type + $this->type = 'json'; + } + + /** + * Render the document. + * + * @param boolean $cache If true, cache the output + * @param array $params Associative array of attributes + * @return object The rendered data + */ + public function render($cache = false, $params = array()) + { + \App::get('response')->headers->set('Cache-Control', 'no-cache', false); + \App::get('response')->headers->set('Pragma', 'no-cache'); + \App::get('response')->headers->set('Content-disposition', 'attachment; filename="' . $this->getName() . '.json"', true); + + parent::render(); + + return $this->getBuffer(); + } + + /** + * Returns the document name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the document name + * + * @param string $name Document name + * @return object instance of $this to allow chaining + */ + public function setName($name) + { + $this->name = (string) $name; + + return $this; + } +} diff --git a/core/libraries/Hubzero/Document/Type/Opensearch.php b/core/libraries/Hubzero/Document/Type/Opensearch.php new file mode 100644 index 00000000000..69a4babd3de --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Opensearch.php @@ -0,0 +1,197 @@ +type = 'opensearch'; + + // Set mime type + $this->mime = 'application/opensearchdescription+xml'; + + // Add the URL for self updating + $update = new Url; + $update->type = 'application/opensearchdescription+xml'; + $update->rel = 'self'; + $update->template = Route::url(\Request::current()); + $this->addUrl($update); + + // Add the favicon as the default image + // Try to find a favicon by checking the template and root folder + $dirs = array(App::get('template')->path, PATH_ROOT); + + foreach ($dirs as $dir) + { + if (file_exists($dir . DS . 'favicon.ico')) + { + $path = str_replace(PATH_ROOT . DS, '', $dir); + $path = str_replace('\\', '/', $path); + + $favicon = new Image; + $favicon->data = \Request::root() . $path . '/favicon.ico'; + $favicon->height = '16'; + $favicon->width = '16'; + $favicon->type = 'image/vnd.microsoft.icon'; + + $this->addImage($favicon); + + break; + } + } + } + + /** + * Render the document + * + * @param boolean $cache If true, cache the output + * @param array $params Associative array of attributes + * @return The rendered data + */ + public function render($cache = false, $params = array()) + { + $xml = new \DOMDocument('1.0', 'utf-8'); + $xml->formatOutput = true; + + // The OpenSearch Namespace + $osns = 'http://a9.com/-/spec/opensearch/1.1/'; + + // Create the root element + $elOs = $xml->createElementNS($osns, 'OpenSearchDescription'); + + $elShortName = $xml->createElementNS($osns, 'ShortName'); + $elShortName->appendChild($xml->createTextNode(htmlspecialchars($this->shortName))); + $elOs->appendChild($elShortName); + + $elDescription = $xml->createElementNS($osns, 'Description'); + $elDescription->appendChild($xml->createTextNode(htmlspecialchars($this->description))); + $elOs->appendChild($elDescription); + + // Always set the accepted input encoding to UTF-8 + $elInputEncoding = $xml->createElementNS($osns, 'InputEncoding'); + $elInputEncoding->appendChild($xml->createTextNode('UTF-8')); + $elOs->appendChild($elInputEncoding); + + foreach ($this->images as $image) + { + $elImage = $xml->createElementNS($osns, 'Image'); + $elImage->setAttribute('type', $image->type); + $elImage->setAttribute('width', $image->width); + $elImage->setAttribute('height', $image->height); + $elImage->appendChild($xml->createTextNode(htmlspecialchars($image->data))); + + $elOs->appendChild($elImage); + } + + foreach ($this->urls as $url) + { + $elUrl = $xml->createElementNS($osns, 'Url'); + $elUrl->setAttribute('type', $url->type); + // Results is the defualt value so we don't need to add it + if ($url->rel != 'results') + { + $elUrl->setAttribute('rel', $url->rel); + } + $elUrl->setAttribute('template', $url->template); + + $elOs->appendChild($elUrl); + } + + $xml->appendChild($elOs); + + parent::render(); + + return $xml->saveXML(); + } + + /** + * Sets the short name + * + * @param string $name The name. + * @return object Supports chaining + */ + public function setShortName($name) + { + $this->shortName = $name; + + return $this; + } + + /** + * Adds an URL to the OpenSearch description. + * + * @param object $url The url to add to the description. + * @return object Supports chaining + */ + public function addUrl(Url $url) + { + $this->urls[] = $url; + + return $this; + } + + /** + * Adds an image to the OpenSearch description. + * + * @param object $image The image to add to the description. + * @return object Supports chaining + */ + public function addImage(Image $image) + { + $this->images[] = $image; + + return $this; + } +} diff --git a/core/libraries/Hubzero/Document/Type/Opensearch/Image.php b/core/libraries/Hubzero/Document/Type/Opensearch/Image.php new file mode 100644 index 00000000000..eead6f89a30 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Opensearch/Image.php @@ -0,0 +1,46 @@ +_mime = 'text/html'; + + // Set document type + $this->_type = 'raw'; + } + + /** + * Render the document. + * + * @param boolean $cache If true, cache the output + * @param array $params Associative array of attributes + * @return string The rendered data + */ + public function render($cache = false, $params = array()) + { + parent::render(); + + return $this->getBuffer(); + } +} diff --git a/core/libraries/Hubzero/Document/Type/Xml.php b/core/libraries/Hubzero/Document/Type/Xml.php new file mode 100644 index 00000000000..36b82684646 --- /dev/null +++ b/core/libraries/Hubzero/Document/Type/Xml.php @@ -0,0 +1,83 @@ +_mime = 'application/xml'; + + // set document type + $this->_type = 'xml'; + } + + /** + * Render the document. + * + * @param boolean $cache If true, cache the output + * @param array $params Associative array of attributes + * @return The rendered data + */ + public function render($cache = false, $params = array()) + { + $this->setMimeEncoding($this->_mime); + + parent::render(); + + \App::get('response')->headers->set('Content-disposition', 'inline; filename="' . $this->getName() . '.xml"', true); + + return $this->getBuffer(); + } + + /** + * Returns the document name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the document name + * + * @param string $name Document name + * @return object instance of $this to allow chaining + */ + public function setName($name) + { + $this->name = (string) $name; + + return $this; + } +} diff --git a/core/libraries/Hubzero/Encryption/Cipher.php b/core/libraries/Hubzero/Encryption/Cipher.php new file mode 100644 index 00000000000..f628c289e13 --- /dev/null +++ b/core/libraries/Hubzero/Encryption/Cipher.php @@ -0,0 +1,42 @@ +type != 'simple') + { + throw new InvalidArgumentException('Invalid key of type: ' . $key->type . '. Expected simple.'); + } + + // Initialise variables. + $decrypted = ''; + $tmp = $key->public; + + // Convert the HEX input into an array of integers and get the number of characters. + $chars = $this->_hexToIntArray($data); + $charCount = count($chars); + + // Repeat the key as many times as necessary to ensure that the key is at least as long as the input. + for ($i = 0; $i < $charCount; $i = strlen($tmp)) + { + $tmp = $tmp . $tmp; + } + + // Get the XOR values between the ASCII values of the input and key characters for all input offsets. + for ($i = 0; $i < $charCount; $i++) + { + $decrypted .= chr($chars[$i] ^ ord($tmp[$i])); + } + + return $decrypted; + } + + /** + * Method to encrypt a data string. + * + * @param string $data The data string to encrypt. + * @param object $key The key[/pair] object to use for encryption. + * @return string The encrypted data string. + * @throws InvalidArgumentException + */ + public function encrypt($data, Key $key) + { + // Validate key. + if ($key->type != 'simple') + { + throw new InvalidArgumentException('Invalid key of type: ' . $key->type . '. Expected simple.'); + } + + // Initialise variables. + $encrypted = ''; + $tmp = $key->private; + + // Split up the input into a character array and get the number of characters. + $chars = preg_split('//', $data, -1, PREG_SPLIT_NO_EMPTY); + $charCount = count($chars); + + // Repeat the key as many times as necessary to ensure that the key is at least as long as the input. + for ($i = 0; $i < $charCount; $i = strlen($tmp)) + { + $tmp = $tmp . $tmp; + } + + // Get the XOR values between the ASCII values of the input and key characters for all input offsets. + for ($i = 0; $i < $charCount; $i++) + { + $encrypted .= $this->_intToHex(ord($tmp[$i]) ^ ord($chars[$i])); + } + + return $encrypted; + } + + /** + * Method to generate a new encryption key[/pair] object. + * + * @param array $options Key generation options. + * @return object + */ + public function generateKey(array $options = array()) + { + // Create the new encryption key[/pair] object. + $key = new Key('simple'); + + // Just a random key of a given length. + $key->private = $this->_getRandomKey(); + $key->public = $key->private; + + return $key; + } + + /** + * Method to generate a random key of a given length. + * + * @param integer $length The length of the key to generate. + * @return string + */ + private function _getRandomKey($length = 256) + { + // Initialise variables. + $key = ''; + $salt = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + $saltLength = strlen($salt); + + // Build the random key. + for ($i = 0; $i < $length; $i++) + { + $key .= $salt[mt_rand(0, $saltLength - 1)]; + } + + return $key; + } + + /** + * Convert hex to an integer + * + * @param string $s The hex string to convert. + * @param integer $i The offset? + * @return integer + */ + private function _hexToInt($s, $i) + { + // Initialise variables. + $j = (int) $i * 2; + $k = 0; + $s1 = (string) $s; + + // Get the character at position $j. + $c = substr($s1, $j, 1); + + // Get the character at position $j + 1. + $c1 = substr($s1, $j + 1, 1); + + switch ($c) + { + case 'A': + $k += 160; + break; + case 'B': + $k += 176; + break; + case 'C': + $k += 192; + break; + case 'D': + $k += 208; + break; + case 'E': + $k += 224; + break; + case 'F': + $k += 240; + break; + case ' ': + $k += 0; + break; + default: + (int) $k = $k + (16 * (int) $c); + break; + } + + switch ($c1) + { + case 'A': + $k += 10; + break; + case 'B': + $k += 11; + break; + case 'C': + $k += 12; + break; + case 'D': + $k += 13; + break; + case 'E': + $k += 14; + break; + case 'F': + $k += 15; + break; + case ' ': + $k += 0; + break; + default: + $k += (int) $c1; + break; + } + + return $k; + } + + /** + * Convert hex to an array of integers + * + * @param string $hex The hex string to convert to an integer array. + * @return array An array of integers. + */ + private function _hexToIntArray($hex) + { + $array = array(); + + $j = (int) strlen($hex) / 2; + + for ($i = 0; $i < $j; $i++) + { + $array[$i] = (int) $this->_hexToInt($hex, $i); + } + + return $array; + } + + /** + * Convert an integer to a hexadecimal string. + * + * @param integer $i An integer value to convert to a hex string. + * @return string + */ + private function _intToHex($i) + { + // Sanitize the input. + $i = (int) $i; + + // Get the first character of the hexadecimal string if there is one. + $j = (int) ($i / 16); + if ($j === 0) + { + $s = ' '; + } + else + { + $s = strtoupper(dechex($j)); + } + + // Get the second character of the hexadecimal string. + $k = $i - $j * 16; + $s = $s . strtoupper(dechex($k)); + + return $s; + } +} diff --git a/core/libraries/Hubzero/Encryption/Encrypter.php b/core/libraries/Hubzero/Encryption/Encrypter.php new file mode 100644 index 00000000000..12eb02dfe6f --- /dev/null +++ b/core/libraries/Hubzero/Encryption/Encrypter.php @@ -0,0 +1,242 @@ +key = $key; + + // Set the encryption cipher. + $this->cipher = isset($cipher) ? $cipher : new Simple; + } + + /** + * Method to decrypt a data string. + * + * @param string $data The encrypted string to decrypt. + * @return string The decrypted data string. + */ + public function decrypt($data) + { + return $this->cipher->decrypt($data, $this->key); + } + + /** + * Method to encrypt a data string. + * + * @param string $data The data string to encrypt. + * @return string The encrypted data string. + */ + public function encrypt($data) + { + return $this->cipher->encrypt($data, $this->key); + } + + /** + * Method to generate a new encryption key[/pair] object. + * + * @param array $options Key generation options. + * @return object + */ + public function generateKey(array $options = array()) + { + return $this->cipher->generateKey($options); + } + + /** + * Method to set the encryption key[/pair] object. + * + * @param object $key The key object to set. + * @return object + */ + public function setKey(Key $key) + { + $this->key = $key; + + return $this; + } + + /** + * Generate random bytes. + * + * @param integer $length Length of the random data to generate + * @return string Random binary data + */ + public static function genRandomBytes($length = 16) + { + $sslStr = ''; + + // if a secure randomness generator exists and we don't + // have a buggy PHP version use it. + if (function_exists('openssl_random_pseudo_bytes') + && (version_compare(PHP_VERSION, '5.3.4') >= 0 || IS_WIN)) + { + $sslStr = openssl_random_pseudo_bytes($length, $strong); + if ($strong) + { + return $sslStr; + } + } + + // Collect any entropy available in the system along with a number + // of time measurements of operating system randomness. + $bitsPerRound = 2; + $maxTimeMicro = 400; + $shaHashLength = 20; + $randomStr = ''; + $total = $length; + + // Check if we can use /dev/urandom. + $urandom = false; + $handle = null; + + // This is PHP 5.3.3 and up + if (function_exists('stream_set_read_buffer') && @is_readable('/dev/urandom')) + { + $handle = @fopen('/dev/urandom', 'rb'); + if ($handle) + { + $urandom = true; + } + } + + while ($length > strlen($randomStr)) + { + $bytes = ($total > $shaHashLength)? $shaHashLength : $total; + $total -= $bytes; + + // Collect any entropy available from the PHP system and filesystem. + // If we have ssl data that isn't strong, we use it once. + $entropy = rand() . uniqid(mt_rand(), true) . $sslStr; + $entropy .= implode('', @fstat(fopen(__FILE__, 'r'))); + $entropy .= memory_get_usage(); + $sslStr = ''; + + if ($urandom) + { + stream_set_read_buffer($handle, 0); + $entropy .= @fread($handle, $bytes); + } + else + { + // There is no external source of entropy so we repeat calls + // to mt_rand until we are assured there's real randomness in + // the result. + // + // Measure the time that the operations will take on average. + $samples = 3; + $duration = 0; + for ($pass = 0; $pass < $samples; ++$pass) + { + $microStart = microtime(true) * 1000000; + $hash = sha1(mt_rand(), true); + for ($count = 0; $count < 50; ++$count) + { + $hash = sha1($hash, true); + } + $microEnd = microtime(true) * 1000000; + $entropy .= $microStart . $microEnd; + if ($microStart > $microEnd) + { + $microEnd += 1000000; + } + $duration += $microEnd - $microStart; + } + $duration = $duration / $samples; + + // Based on the average time, determine the total rounds so that + // the total running time is bounded to a reasonable number. + $rounds = (int) (($maxTimeMicro / $duration) * 50); + + // Take additional measurements. On average we can expect + // at least $bitsPerRound bits of entropy from each measurement. + $iter = $bytes * (int) ceil(8 / $bitsPerRound); + for ($pass = 0; $pass < $iter; ++$pass) + { + $microStart = microtime(true); + $hash = sha1(mt_rand(), true); + for ($count = 0; $count < $rounds; ++$count) + { + $hash = sha1($hash, true); + } + $entropy .= $microStart . microtime(true); + } + } + + $randomStr .= sha1($entropy, true); + } + + if ($urandom) + { + @fclose($handle); + } + + return substr($randomStr, 0, $length); + } + + /** + * A timing safe comparison method. This defeats hacking + * attempts that use timing based attack vectors. + * + * @param string $known A known string to check against. + * @param string $unknown An unknown string to check. + * @return boolean True if the two strings are exactly the same. + */ + public static function timingSafeCompare($known, $unknown) + { + // Prevent issues if string length is 0 + $known .= chr(0); + $unknown .= chr(0); + + $knownLength = strlen($known); + $unknownLength = strlen($unknown); + + // Set the result to the difference between the lengths + $result = $knownLength - $unknownLength; + + // Note that we ALWAYS iterate over the user-supplied length to prevent leaking length info. + for ($i = 0; $i < $unknownLength; $i++) + { + // Using % here is a trick to prevent notices. It's safe, since if the lengths are different, $result is already non-0 + $result |= (ord($known[$i % $knownLength]) ^ ord($unknown[$i])); + } + + // They are only identical strings if $result is exactly 0... + return $result === 0; + } +} diff --git a/core/libraries/Hubzero/Encryption/Key.php b/core/libraries/Hubzero/Encryption/Key.php new file mode 100644 index 00000000000..8b7ff92d288 --- /dev/null +++ b/core/libraries/Hubzero/Encryption/Key.php @@ -0,0 +1,55 @@ +type = (string) $type; + + // Set the optional public/private key strings. + $this->private = isset($private) ? (string) $private : null; + $this->public = isset($public) ? (string) $public : null; + } +} diff --git a/core/libraries/Hubzero/Error/Exception/BadFunctionCallException.php b/core/libraries/Hubzero/Error/Exception/BadFunctionCallException.php new file mode 100644 index 00000000000..55f14b6fb7f --- /dev/null +++ b/core/libraries/Hubzero/Error/Exception/BadFunctionCallException.php @@ -0,0 +1,12 @@ +level = $level; + $this->code = $code; + $this->message = $msg; + + if ($info != null) + { + $this->info = $info; + } + + if ($backtrace && is_array($backtrace)) + { + $this->backtrace = $backtrace; + } + + parent::__construct($msg, (int) $code); + } + + /** + * Returns a property of the object or the default value if the property is not set. + * + * @param string $property The name of the property + * @param mixed $default The default value + * @return mixed The value of the property or null + */ + public function get($property, $default = null) + { + if (isset($this->$property)) + { + return $this->$property; + } + return $default; + } + + /** + * Modifies a property of the object, creating it if it does not already exist. + * + * @param string $property The name of the property + * @param mixed $value The value of the property to set + * @return mixed Previous value of the property + */ + public function set($property, $value = null) + { + $this->$property = $value; + return $this; + } +} diff --git a/core/libraries/Hubzero/Error/Exception/LengthException.php b/core/libraries/Hubzero/Error/Exception/LengthException.php new file mode 100644 index 00000000000..c5f21b35f46 --- /dev/null +++ b/core/libraries/Hubzero/Error/Exception/LengthException.php @@ -0,0 +1,12 @@ +logger = $logger; + $this->renderer = $renderer; + } + + /** + * Set the logger for the handler. + * + * @param object $logger + * @return object Chainable. + */ + public function setLogger($logger) + { + $this->logger = $logger; + + return $this; + } + + /** + * Set the render, + * + * @param object $exception + * @return object Chainable. + */ + public function setRenderer(RendererInterface $renderer) + { + $this->renderer = $renderer; + + return $this; + } + + /** + * Determine if we are running in the console. + * + * @return bool + */ + public function runningInConsole() + { + return php_sapi_name() == 'cli'; + } + + /** + * Register the exception / error handlers for the application. + * + * @param string $client + * @return void + */ + public function register($client) + { + $this->registerErrorHandler(); + + $this->registerExceptionHandler(); + + if ($client != 'testing') + { + $this->registerShutdownHandler(); + } + } + + /** + * Register the PHP error handler. + * + * @return void + */ + protected function registerErrorHandler() + { + set_error_handler(array($this, 'handleError')); + } + + /** + * Register the PHP exception handler. + * + * @return void + */ + protected function registerExceptionHandler() + { + set_exception_handler(array($this, 'handleException')); + } + + /** + * Register the PHP shutdown handler. + * + * @return void + */ + protected function registerShutdownHandler() + { + register_shutdown_function(array($this, 'handleShutdown')); + } + + /** + * Handle a PHP error for the application. + * + * @param int $level + * @param string $message + * @param string $file + * @param int $line + * @param array $context + * @throws \ErrorException + */ + public function handleError($level, $message, $file = '', $line = 0, $context = array()) + { + if (error_reporting() & $level) + { + throw new ErrorException($message, 0, $level, $file, $line); + } + } + + /** + * Handle an uncaught exception. + * + * @param object $exception + * @return void + */ + public function handleException($exception) + { + if (is_object($this->logger) && !in_array($exception->getCode(), [403, 404])) + { + $this->logger->log( + 'error', + $exception->getMessage(), + array( + 'code' => $exception->getCode(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine() + ) + ); + } + + return $this->renderer->render($exception); + } + + /** + * Handle the PHP shutdown event. + * + * @return void + */ + public function handleShutdown() + { + $error = error_get_last(); + + // If an error has occurred that has not been displayed, we will create a fatal + // error exception instance and pass it into the regular exception handling + // code so it can be displayed back out to the developer for information. + if (!is_null($error)) + { + extract($error); + + if (!in_array($type, array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) + { + return; + } + + $this->handleException(new FatalError($message, $type, 0, $file, $line)); + } + } +} diff --git a/core/libraries/Hubzero/Error/Renderer/Api.php b/core/libraries/Hubzero/Error/Renderer/Api.php new file mode 100644 index 00000000000..9b4ff6ab0ac --- /dev/null +++ b/core/libraries/Hubzero/Error/Renderer/Api.php @@ -0,0 +1,98 @@ +response = $response; + $this->debug = $debug; + } + + /** + * Display the given exception to the user. + * + * @param object $exception + * @return void + */ + public function render($error) + { + $status = $error->getCode() ? $error->getCode() : 500; + $status = ($status < 100 || $status >= 600) ? 500 : $status; + + $content = new \StdClass; + $content->message = $error->getMessage(); + $content->code = $status; + + if ($this->debug) + { + $content->trace = array(); + + $backtrace = $error->getTrace(); + + if (is_array($backtrace)) + { + $backtrace = array_reverse($backtrace); + + for ($i = count($backtrace) - 1; $i >= 0; $i--) + { + if (isset($backtrace[$i]['class'])) + { + $line = "[$i] " . sprintf("%s %s %s()", $backtrace[$i]['class'], $backtrace[$i]['type'], $backtrace[$i]['function']); + } + else + { + $line = "[$i] " . sprintf("%s()", $backtrace[$i]['function']); + } + + if (isset($backtrace[$i]['file'])) + { + $line .= sprintf(' @ %s:%d', str_replace(PATH_ROOT, '', $backtrace[$i]['file']), $backtrace[$i]['line']); + } + + $content->trace[] = $line; + } + } + } + + $this->response->setStatusCode($content->code); + $this->response->setContent($content); + $this->response->send(); + + exit(); + } +} diff --git a/core/libraries/Hubzero/Error/Renderer/Notification.php b/core/libraries/Hubzero/Error/Renderer/Notification.php new file mode 100644 index 00000000000..244b6d413de --- /dev/null +++ b/core/libraries/Hubzero/Error/Renderer/Notification.php @@ -0,0 +1,47 @@ +notifier = $notifier; + } + + /** + * Render the error page based on an exception. + * + * @param object $error The exception for which to render the error page. + * @return void + */ + public function render($error) + { + $this->notifier->message($error->getMessage(), ($error->getCode() == 500 ? 'error' : 'warning')); + } +} diff --git a/core/libraries/Hubzero/Error/Renderer/Page.php b/core/libraries/Hubzero/Error/Renderer/Page.php new file mode 100644 index 00000000000..96673e3a2a3 --- /dev/null +++ b/core/libraries/Hubzero/Error/Renderer/Page.php @@ -0,0 +1,117 @@ +document = $document; + $this->template = $template; + $this->debug = $debug; + } + + /** + * Render the error page based on an exception. + * + * @param object $error The exception for which to render the error page. + * @return void + */ + public function render($error) + { + try + { + if (!$this->document) + { + // We're probably in an CLI environment + exit($error->getMessage()); + } + + $this->document->setType('error'); + + // Push the error object into the document + $this->document->setError($error); + + if (ob_get_contents()) + { + ob_end_clean(); + } + + $this->document->setTitle(\Lang::txt('Error') . ': ' . $error->getCode()); + + $template = $this->template->load(); + + $data = $this->document->render( + false, + array( + 'template' => $template->template, + 'directory' => dirname($template->path), + 'debug' => $this->debug + ) + ); + + // Failsafe to get the error displayed. + if (empty($data)) + { + exit($error->getMessage() . ' in ' . $error->getFile() . ':' . $error->getLine()); + } + else + { + $status = $error->getCode() ? $error->getCode() : 500; + $status = ($status < 100 || $status >= 600) ? 500 : $status; + + $response = new Response($data, $status); + $response->send(); + + exit(); + } + } + catch (Exception $e) + { + $plain = new Plain($this->debug); + $plain->render($e); + } + } +} diff --git a/core/libraries/Hubzero/Error/Renderer/Plain.php b/core/libraries/Hubzero/Error/Renderer/Plain.php new file mode 100644 index 00000000000..36c52ca5330 --- /dev/null +++ b/core/libraries/Hubzero/Error/Renderer/Plain.php @@ -0,0 +1,84 @@ +debug = $debug; + } + + /** + * Display the given exception to the user. + * + * @param object $exception + * @return void + */ + public function render($error) + { + if (!headers_sent()) + { + header('HTTP/1.1 500 Internal Server Error'); + } + + $response = 'Error: ' . $error->getCode() . ' - ' . $error->getMessage() . "\n\n"; + + if ($this->debug) + { + $backtrace = $error->getTrace(); + + if (is_array($backtrace)) + { + $backtrace = array_reverse($backtrace); + + for ($i = count($backtrace) - 1; $i >= 0; $i--) + { + if (isset($backtrace[$i]['class'])) + { + $response .= "\n[$i] " . sprintf("%s %s %s()", $backtrace[$i]['class'], $backtrace[$i]['type'], $backtrace[$i]['function']); + } + else + { + $response .= "\n[$i] " . sprintf("%s()", $backtrace[$i]['function']); + } + + if (isset($backtrace[$i]['file'])) + { + $response .= sprintf(' @ %s:%d', str_replace(PATH_ROOT, '', $backtrace[$i]['file']), $backtrace[$i]['line']); + } + } + } + } + + echo (php_sapi_name() == 'cli') ? $response : nl2br($response); + + exit(); + } +} diff --git a/core/libraries/Hubzero/Error/RendererInterface.php b/core/libraries/Hubzero/Error/RendererInterface.php new file mode 100644 index 00000000000..1869b5ea44f --- /dev/null +++ b/core/libraries/Hubzero/Error/RendererInterface.php @@ -0,0 +1,23 @@ +dispatcher = $dispatcher; + $this->stopwatch = $stopwatch; + $this->logger = $logger; + $this->called = array(); + } + + /** + * {@inheritdoc} + */ + public function addListener($listener, $events = array()) + { + $this->dispatcher->addListener($listener, $events); + } + + /** + * {@inheritdoc} + */ + public function removeListener($listener, $event = null) + { + return $this->dispatcher->removeListener($listener, $event); + } + + /** + * {@inheritdoc} + */ + public function getListeners($event) + { + return $this->dispatcher->getListeners($event); + } + + /** + * {@inheritdoc} + */ + public function hasListener($listener, $event) + { + return $this->dispatcher->hasListener($listener, $event); + } + + /** + * {@inheritdoc} + */ + public function trigger($event, $args = array()) + { + if (!($event instanceof Event)) + { + $event = new Event($event); + } + + // If a listener group was specified + // lazy load the listeners. + if ($group = $event->getGroup()) + { + $this->dispatcher->addListeners($group); + } + + $this->preProcess($event); + $this->preTrigger($event); + + /*$e = $this->stopwatch->start($event->getName(), 'section'); + + $responses = $this->dispatcher->trigger($event, $args); + + if ($e->isStarted()) + { + $e->stop(); + }*/ + + // Attach any incoming aruments + foreach ((array) $args as $name => $arg) + { + $event->addArgument($name, $arg); + } + + foreach ($this->dispatcher->getListeners($event->getName()) as $listener) + { + // Call the event listener + $response = $listener->trigger($event); + + // Attach response + if (!is_null($response)) + { + $event->addResponse($response); + } + + // Is propagation stopped? + if ($event->isStopped()) + { + break; + } + } + + $this->postTrigger($event); + $this->postProcess($event); + + return $event->getResponse(); + } + + /** + * {@inheritdoc} + */ + public function getCalledListeners() + { + $called = array(); + + foreach ($this->called as $eventName => $listeners) + { + foreach ($listeners as $listener) + { + $info = $this->getListenerInfo($listener->getWrappedListener(), $eventName); + $called[$eventName . '.' . $info['pretty']] = $info; + } + } + + return $called; + } + + /** + * {@inheritdoc} + */ + public function getNotCalledListeners() + { + try + { + $allListeners = $this->getListeners(); + } + catch (\Exception $e) + { + if (null !== $this->logger) + { + $this->logger->info(sprintf('An exception was thrown while getting the uncalled listeners (%s)', $e->getMessage()), array('exception' => $e)); + } + + // unable to retrieve the uncalled listeners + return array(); + } + + $notCalled = array(); + foreach ($allListeners as $eventName => $listeners) + { + foreach ($listeners as $listener) + { + $called = false; + if (isset($this->called[$eventName])) + { + foreach ($this->called[$eventName] as $l) + { + if ($l->getWrappedListener() === $listener) + { + $called = true; + + break; + } + } + } + + if (!$called) + { + $info = $this->getListenerInfo($listener, $eventName); + $notCalled[$eventName . '.' . $info['pretty']] = $info; + } + } + } + + return $notCalled; + } + + /** + * Proxies all method calls to the original event dispatcher. + * + * @param string $method The method name + * @param array $arguments The method arguments + * @return mixed + */ + public function __call($method, $arguments) + { + return call_user_func_array(array($this->dispatcher, $method), $arguments); + } + + /** + * Called before dispatching the event. + * + * @param Event $event The event + */ + protected function preTrigger(Event $event) + { + } + + /** + * Called after dispatching the event. + * + * @param object $event The event + * @return void + */ + protected function postTrigger(Event $event) + { + } + + /** + * Called before preTrigger + * + * @param object $event The event + * @return void + */ + private function preProcess(Event $event) + { + foreach ($this->dispatcher->getListeners($event) as $listener) + { + if ($listener instanceof TraceableListener) + { + continue; + } + + $priority = $this->dispatcher->getListenerPriority($listener, $event); + + //$this->dispatcher->removeListener($listener, $event); + + $info = $this->getListenerInfo($listener, $event->getName()); + $name = isset($info['class']) ? $info['class'] : $info['type']; + + $this->dispatcher->getListeners()[$event->getName()]->remove($listener); + $this->dispatcher->getListeners()[$event->getName()]->add(new TraceableListener($listener, $name, $this->stopwatch), $priority); + + //$this->dispatcher->addListener(new TraceableListener($listener, $name, $this->stopwatch), array($event->getName() => $priority)); + } + } + + /** + * Called after postTrigger + * + * @param object $event The event + * @return void + */ + private function postProcess(Event $event) + { + $skipped = false; + + foreach ($this->dispatcher->getListeners($event) as $listener) + { + if (!($listener instanceof TraceableListener)) + { + // A new listener was added during dispatch. + continue; + } + + // Unwrap listener + //$this->dispatcher->removeListener($listener, $event); + //$this->dispatcher->addListener($listener->getWrappedListener(), $event); + $priority = $this->dispatcher->getListenerPriority($listener, $event); + + $this->dispatcher->getListeners()[$event->getName()]->remove($listener); + $this->dispatcher->getListeners()[$event->getName()]->add($listener->getWrappedListener(), $priority); + + $info = $this->getListenerInfo($listener->getWrappedListener(), $event->getName()); + + $eventName = $event->getName(); + + if ($listener->wasCalled()) + { + if (null !== $this->logger) + { + $this->logger->debug(sprintf('Notified event "%s" to listener "%s".', $eventName, $info['pretty'])); + } + + if (!isset($this->called[$eventName])) + { + $this->called[$eventName] = new \SplObjectStorage(); + } + + $this->called[$eventName]->attach($listener); + } + + if (null !== $this->logger && $skipped) + { + $this->logger->debug(sprintf('Listener "%s" was not called for event "%s".', $info['pretty'], $eventName)); + } + + if ($listener->stoppedPropagation()) + { + if (null !== $this->logger) + { + $this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName)); + } + + $skipped = true; + } + } + } + + /** + * Returns information about the listener + * + * @param object $listener The listener + * @param string $eventName The event name + * @return array Information about the listener + */ + private function getListenerInfo($listener, $event) + { + $info = array( + 'event' => $event, + ); + + if ($listener instanceof \Closure) + { + $info += array( + 'type' => 'Closure', + 'pretty' => 'closure', + ); + } + elseif (is_string($listener)) + { + try + { + $r = new \ReflectionFunction($listener); + $file = $r->getFileName(); + $line = $r->getStartLine(); + } + catch (\ReflectionException $e) + { + $file = null; + $line = null; + } + $info += array( + 'type' => 'Function', + 'function' => $listener, + 'file' => $file, + 'line' => $line, + 'pretty' => $listener, + ); + } + elseif (is_array($listener) || (is_object($listener))) // && is_callable($listener))) + { + if ($listener instanceof WrappedListener) + { + $listener = $listener->getWrappedListener(); + } + + if (!is_array($listener)) + { + $listener = array($listener, $event); + } + + $class = is_object($listener[0]) ? get_class($listener[0]) : $listener[0]; + try + { + $r = new \ReflectionMethod($class, $listener[1]); + $file = $r->getFileName(); + $line = $r->getStartLine(); + } + catch (\ReflectionException $e) + { + $file = null; + $line = null; + } + $info += array( + 'type' => 'Method', + 'class' => $class, + 'method' => $listener[1], + 'file' => $file, + 'line' => $line, + 'pretty' => $class . '::' . $listener[1], + ); + } + + return $info; + } +} diff --git a/core/libraries/Hubzero/Events/Debug/TraceableListener.php b/core/libraries/Hubzero/Events/Debug/TraceableListener.php new file mode 100644 index 00000000000..c1a87bcd929 --- /dev/null +++ b/core/libraries/Hubzero/Events/Debug/TraceableListener.php @@ -0,0 +1,135 @@ +listener = $listener; + $this->name = $name; + $this->stopwatch = $stopwatch; + $this->called = false; + $this->stoppedPropagation = false; + } + + /** + * Return the underlying lsitener + * + * @return object + */ + public function getWrappedListener() + { + return $this->listener; + } + + /** + * Was this listener called? + * + * @return boolean + */ + public function wasCalled() + { + return $this->called; + } + + /** + * Did this listener stop propagation? + * + * @return boolean + */ + public function stoppedPropagation() + { + return $this->stoppedPropagation; + } + + /** + * Did this listener stop propagation? + * + * @return mixed + */ + public function trigger(Event $event) + { + $this->called = true; + + //$e = $this->stopwatch->start($this->name, 'event_listener'); + + if ($this->listener instanceof Closure) + { + $response = call_user_func($this->listener, $event); + } + else + { + $response = call_user_func(array($this->listener, $event->getName()), $event); + } + + /*if ($e->isStarted()) + { + $e->stop(); + }*/ + + if ($event->isStopped()) + { + $this->stoppedPropagation = true; + } + + return $response; + } +} diff --git a/core/libraries/Hubzero/Events/Dispatcher.php b/core/libraries/Hubzero/Events/Dispatcher.php new file mode 100644 index 00000000000..76487f03317 --- /dev/null +++ b/core/libraries/Hubzero/Events/Dispatcher.php @@ -0,0 +1,450 @@ +loaders[$loader->getName()])) + { + //$loader->setDispatcher($this); + + $this->loaders[$loader->getName()] = $loader; + } + + return $this; + } + + /** + * Remove a listener laoder from this dispatcher. + * + * @param LoaderInterface|string $event The event object or name. + * @return Dispatcher This method is chainable. + */ + public function removeListenerLoader($loader) + { + if ($loader instanceof LoaderInterface) + { + $loader = $loader->getName(); + } + + if (isset($this->loaders[$loader])) + { + unset($this->loaders[$loader]); + } + + return $this; + } + + /** + * Remove a listener laoder from this dispatcher. + * + * @return array + */ + public function getListenerLoaders() + { + return $this->loaders; + } + + /** + * Add multiple listeners to this dispatcher. If a string is passed, it will loop + * through the list of listener loaders. + * + * @param array|string $listeners The listener + * @param array $events An associative array of event names as keys + * and the corresponding listener priority as values. + * @return Dispatcher This method is chainable. + */ + public function addListeners($listeners, array $events = array()) + { + if (is_string($listeners)) + { + $loaded = array(); + foreach ($this->getListenerLoaders() as $loader) + { + $loaded += $loader->loadListeners($listeners); + } + $listeners = $loaded; + } + + foreach ($listeners as $listener) + { + if (!$this->hasListener($listener)) + { + $this->addListener($listener, $events); + } + } + + return $this; + } + + /** + * Add a listener to this dispatcher, only if not already registered to these events. + * If no events are specified, it will be registered to all events matching it's methods name. + * In the case of a closure, you must specify at least one event name. + * + * @param object|Closure $listener The listener + * @param array $events An associative array of event names as keys + * and the corresponding listener priority as values. + * @return Dispatcher This method is chainable. + * @throws InvalidArgumentException + */ + public function addListener($listener, $events = array()) + { + if (!is_object($listener)) + { + throw new InvalidArgumentException('The given listener is not an object.'); + } + + if (is_string($events)) + { + $events = array($events => Priority::NORMAL); + } + + // We deal with a closure. + if ($listener instanceof Closure) + { + if (empty($events)) + { + throw new InvalidArgumentException('No event name(s) and priority specified for the Closure listener.'); + } + + foreach ($events as $name => $priority) + { + $name = (strstr($name, '.') ? ltrim(strstr($name, '.'), '.') : $name); + + if (!isset($this->listeners[$name])) + { + $this->listeners[$name] = new ListenersPriorityQueue; + } + + $this->listeners[$name]->add($listener, $priority); + } + + return $this; + } + + // We deal with a "normal" object. + $methods = get_class_methods($listener); + + if (!empty($events)) + { + $methods = array_intersect($methods, array_keys($events)); + } + + // Backwards compatibility + if (!($listener instanceof ListenerInterface)) + { + $listener = new WrappedListener($listener); + } + + foreach ($methods as $event) + { + if (!isset($this->listeners[$event])) + { + $this->listeners[$event] = new ListenersPriorityQueue; + } + + $priority = isset($events[$event]) ? $events[$event] : Priority::NORMAL; + + $this->listeners[$event]->add($listener, $priority); + } + + return $this; + } + + /** + * Thsi is an alias for addListener() + * + * @param mixed $listener + * @param array $events + * @throws InvalidArgumentException + */ + public function listen($listener, $events = array()) + { + return $this->addListener($listener, $events); + } + + /** + * [!] Compatibility + * + * @param object $listener The listener. + * @return void + */ + public function attach($listener) + { + return $this->addListener($listener); + } + + /** + * Get the priority of the given listener for the given event. + * + * @param object $listener The listener. + * @param mixed $event The event object or name. + * @return mixed The listener priority or null if the listener doesn't exist. + */ + public function getListenerPriority($listener, $event) + { + $event = $this->resolveEventName($event); + + if (isset($this->listeners[$event])) + { + return $this->listeners[$event]->getPriority($listener); + } + + return null; + } + + /** + * Get the listeners registered to the given event. + * + * @param mixed $event The event object or name. + * @return array An array of registered listeners sorted according to their priorities. + */ + public function getListeners($event = null) + { + $event = $this->resolveEventName($event); + + if ($event) + { + if (isset($this->listeners[$event])) + { + return $this->listeners[$event]->getAll(); + } + return array(); + } + + return $this->listeners; + } + + /** + * Tell if the given listener has been added. + * If an event is specified, it will tell if the listener is registered for that event. + * + * @param object $listener The listener. + * @param mixed $event The event object or name. + * @return boolean True if the listener is registered, false otherwise. + */ + public function hasListener($listener, $event = null) + { + if ($event) + { + $event = $this->resolveEventName($event); + + if (isset($this->listeners[$event])) + { + return $this->listeners[$event]->has($listener); + } + } + else + { + foreach ($this->listeners as $queue) + { + if ($queue->has($listener)) + { + return true; + } + } + } + + return false; + } + + /** + * Remove the given listener from this dispatcher. + * If no event is specified, it will be removed from all events it is listening to. + * + * @param object $listener The listener to remove. + * @param mixed $event The event object or name. + * @return object This method is chainable. + */ + public function removeListener($listener, $event = null) + { + if ($event) + { + $event = $this->resolveEventName($event); + + if (isset($this->listeners[$event])) + { + $this->listeners[$event]->remove($listener); + } + } + else + { + foreach ($this->listeners as $queue) + { + $queue->remove($listener); + } + } + + return $this; + } + + /** + * This is an alias for removeListener() + * + * @param object $listener The listener to remove. + * @param mixed $event The event object or name. + * @return object This method is chainable. + */ + public function forget($listener, $event = null) + { + return $this->removeListener($listener, $event); + } + + /** + * Clear the listeners in this dispatcher. + * If an event is specified, the listeners will be cleared only for that event. + * + * @param mixed $event The event object or name. + * @return object This method is chainable. + */ + public function clearListeners($event = null) + { + if ($event) + { + $event = $this->resolveEventName($event); + + if (isset($this->listeners[$event])) + { + unset($this->listeners[$event]); + } + } + else + { + $this->listeners = array(); + } + + return $this; + } + + /** + * Count the number of registered listeners for the given event. + * + * @param EventInterface|string $event The event object or name. + * @return integer The number of registered listeners for the given event. + */ + public function countListeners($event) + { + $event = $this->resolveEventName($event); + + return isset($this->listeners[$event]) ? count($this->listeners[$event]) : 0; + } + + /** + * Trigger an event. + * + * @param mixed $event The event object or name. + * @return array The event after being passed through all listeners. + */ + public function trigger($event, $args = array()) + { + if (!($event instanceof Event)) + { + if (isset($this->events[$event])) + { + $event = $this->events[$event]; + } + else + { + $event = new Event($event); + } + } + + // If a listener group was specified + // lazy load the listeners. + if ($group = $event->getGroup()) + { + $this->addListeners($group); + } + + // Attach any incoming aruments + foreach ((array) $args as $name => $arg) + { + $event->addArgument($name, $arg); + } + + // Are there any listeners for this event? + if (isset($this->listeners[$event->getName()])) + { + foreach ($this->listeners[$event->getName()] as $listener) + { + // Call the event listener + if ($listener instanceof Closure) + { + $response = call_user_func($listener, $event); + } + else + { + $response = call_user_func(array($listener, $event->getName()), $event); + } + + // Attach response + if (!is_null($response)) + { + $event->addResponse($response); + } + + // Is propagation stopped? + if ($event->isStopped()) + { + break; + } + } + } + + return $event->getResponse(); + } + + /** + * Trigger an event. + * + * @param mixed $event The event object or name. + * @return array The event after being passed through all listeners. + */ + protected function resolveEventName($event) + { + if ($event instanceof Event) + { + $event = $event->getName(); + } + return $event; + } +} diff --git a/core/libraries/Hubzero/Events/DispatcherInterface.php b/core/libraries/Hubzero/Events/DispatcherInterface.php new file mode 100644 index 00000000000..68981710696 --- /dev/null +++ b/core/libraries/Hubzero/Events/DispatcherInterface.php @@ -0,0 +1,22 @@ +group = strstr($name, '.', true); + $name = ltrim(strstr($name, '.'), '.'); + } + $this->name = $name; + $this->arguments = $arguments; + } + + /** + * Get the event group name. + * + * @return string The event group name. + */ + public function getGroup() + { + return $this->group; + } + + /** + * Get the event name. + * + * @return string The event name. + */ + public function getName() + { + return $this->name; + } + + /** + * Add an event argument, only if it is not existing. + * + * @param string $name The argument name. + * @param mixed $value The argument value. + * @return object This method is chainable. + */ + public function addArgument($name, $value) + { + if (!isset($this->arguments[$name])) + { + $this->arguments[$name] = $value; + } + + return $this; + } + + /** + * Set the value of an event argument. + * If the argument already exists, it will be overridden. + * + * @param string $name The argument name. + * @param mixed $value The argument value. + * @return object This method is chainable. + */ + public function setArgument($name, $value) + { + $this->arguments[$name] = $value; + + return $this; + } + + /** + * Remove an event argument. + * + * @param string $name The argument name. + * @return object This method is chainable. + */ + public function removeArgument($name) + { + if (isset($this->arguments[$name])) + { + unset($this->arguments[$name]); + } + + return $this; + } + + /** + * Clear all event arguments. + * + * @return array The old arguments. + */ + public function clearArguments() + { + $this->arguments = array(); + + return $this; + } + + /** + * Get an event argument value. + * + * @param string $name The argument name. + * @param mixed $default The default value if not found. + * @return mixed The argument value or the default value. + */ + public function getArgument($name, $default = null) + { + if (isset($this->arguments[$name])) + { + return $this->arguments[$name]; + } + + return $default; + } + + /** + * Tell if the given event argument exists. + * + * @param string $name The argument name. + * @return boolean True if it exists, false otherwise. + */ + public function hasArgument($name) + { + return isset($this->arguments[$name]); + } + + /** + * Get all event arguments. + * + * @return array An associative array of argument names as keys + * and their values as values. + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Count the number of arguments. + * + * @return integer The number of arguments. + */ + public function count() + { + return count($this->arguments); + } + + /** + * Serialize the event. + * + * @return string The serialized event. + */ + public function serialize() + { + return serialize(array($this->name, $this->arguments, $this->stopped)); + } + + /** + * Unserialize the event. + * + * @param string $serialized The serialized event. + * @return void + */ + public function unserialize($serialized) + { + list($this->name, $this->arguments, $this->stopped) = unserialize($serialized); + } + + /** + * Add an error message. + * + * @param string $error Error message. + * @param string $key Specific key to set the value to + * @return object This method is chainable. + */ + public function addResponse($data) + { + array_push($this->response, $data); + + return $this; + } + + /** + * Get the list of responses from triggered listeners. + * + * @return array + */ + public function getResponse() + { + return $this->response; + } + + /** + * Stop the event propagation. + * + * @return void + */ + public function stop() + { + $this->stopped = true; + } + + /** + * Resume the event propagation. + * + * @return void + */ + public function resume() + { + $this->stopped = false; + } + + /** + * Tell if the event propagation is stopped. + * + * @return boolean True if stopped, false otherwise. + */ + public function isStopped() + { + return true === $this->stopped; + } + + /** + * Set the value of an event argument. + * + * @param string $name The argument name. + * @param mixed $value The argument value. + * @return void + * @throws InvalidArgumentException If the argument name is null. + */ + public function offsetSet($name, $value) + { + if (is_null($name)) + { + throw new InvalidArgumentException('The argument name cannot be null.'); + } + + $this->setArgument($name, $value); + } + + /** + * Remove an event argument. + * + * @param string $name The argument name. + * @return void + */ + public function offsetUnset($name) + { + $this->removeArgument($name); + } + + /** + * Tell if the given event argument exists. + * + * @param string $name The argument name. + * @return boolean True if it exists, false otherwise. + */ + public function offsetExists($name) + { + return $this->hasArgument($name); + } + + /** + * Get an event argument value. + * + * @param string $name The argument name. + * @return mixed The argument value or null if not existing. + */ + public function offsetGet($name) + { + return $this->getArgument($name); + } +} diff --git a/core/libraries/Hubzero/Events/ListenerInterface.php b/core/libraries/Hubzero/Events/ListenerInterface.php new file mode 100644 index 00000000000..d9f466b3025 --- /dev/null +++ b/core/libraries/Hubzero/Events/ListenerInterface.php @@ -0,0 +1,15 @@ +queue = new SplPriorityQueue; + $this->storage = new SplObjectStorage; + } + + /** + * Add a listener with the given priority only if not already present. + * + * @param object $listener The listener. + * @param integer $priority The listener priority. + * @return object This method is chainable. + */ + public function add($listener, $priority) + { + if (!$this->storage->contains($listener)) + { + // Compute the internal priority as an array. + $priority = array($priority, $this->counter--); + + $this->storage->attach($listener, $priority); + $this->queue->insert($listener, $priority); + } + + return $this; + } + + /** + * Remove a listener from the queue. + * + * @param object $listener The listener. + * @return object This method is chainable. + */ + public function remove($listener) + { + if ($this->storage->contains($listener)) + { + $this->storage->detach($listener); + $this->storage->rewind(); + + $this->queue = new SplPriorityQueue; + + foreach ($this->storage as $listener) + { + $priority = $this->storage->getInfo(); + $this->queue->insert($listener, $priority); + } + } + + return $this; + } + + /** + * Tell if the listener exists in the queue. + * + * @param object $listener The listener. + * @return boolean True if it exists, false otherwise. + */ + public function has($listener) + { + return $this->storage->contains($listener); + } + + /** + * Get the priority of the given listener. + * + * @param object $listener The listener. + * @param mixed $default The default value to return if the listener doesn't exist. + * @return mixed The listener priority if it exists, null otherwise. + */ + public function getPriority($listener, $default = null) + { + if ($this->storage->contains($listener)) + { + return $this->storage[$listener][0]; + } + + return $default; + } + + /** + * Get all listeners contained in this queue, sorted according to their priority. + * + * @return array An array of listeners. + */ + public function getAll() + { + $listeners = array(); + + // Get a clone of the queue. + $queue = $this->getIterator(); + + foreach ($queue as $listener) + { + $listeners[] = $listener; + } + + return $listeners; + } + + /** + * Get the inner queue with its cursor on top of the heap. + * + * @return object The inner queue. + */ + public function getIterator() + { + // SplPriorityQueue queue is a heap. + $queue = clone $this->queue; + + if (!$queue->isEmpty()) + { + $queue->top(); + } + + return $queue; + } + + /** + * Count the number of listeners in the queue. + * + * @return integer The number of listeners in the queue. + */ + public function count() + { + return count($this->queue); + } +} diff --git a/core/libraries/Hubzero/Events/LoaderInterface.php b/core/libraries/Hubzero/Events/LoaderInterface.php new file mode 100644 index 00000000000..73fe7a5713c --- /dev/null +++ b/core/libraries/Hubzero/Events/LoaderInterface.php @@ -0,0 +1,29 @@ +listener = $listener; + $this->called = false; + } + + /** + * Get the listener + * + * @return object + */ + public function getWrappedListener() + { + return $this->listener; + } + + /** + * Load the given listener group. + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call($method, $arguments) + { + $this->called = true; + + $event = isset($arguments[0]) ? $arguments[0] : new Event($method); + + $this->listener->event = $event; + $args = $event->getArguments(); + + // A bit ugly but we need to take into account cases where + // arguments have named indices (i.e., associative array) + if (count($args) && !isset($args[0])) + { + $args = array_values($args); + } + + switch (count($args)) + { + case 0: + return $this->listener->$method(); + + case 1: + return $this->listener->$method($args[0]); + + case 2: + return $this->listener->$method($args[0], $args[1]); + + case 3: + return $this->listener->$method($args[0], $args[1], $args[2]); + + case 4: + return $this->listener->$method($args[0], $args[1], $args[2], $args[3]); + + default: + return call_user_func_array(array($this->listener, $method), $args); + } + } +} diff --git a/core/libraries/Hubzero/Facades/App.php b/core/libraries/Hubzero/Facades/App.php new file mode 100644 index 00000000000..4237513a08d --- /dev/null +++ b/core/libraries/Hubzero/Facades/App.php @@ -0,0 +1,26 @@ +get(static::getAccessor()); + } + + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getAccessor() + { + throw new \RuntimeException('Facade does not implement getAccessor method.'); + } + + /** + * Create aliases + * + * @param array $aliases + * @return void + */ + public static function createAliases(array $aliases) + { + static::$aliases = $aliases; + + // Create autoloader that creates class aliases during runtime + spl_autoload_register(__NAMESPACE__ . '\Facade::loadAliases'); + } + + /** + * Load aliases + * + * @param string $class The class being loaded + * @return void + */ + public static function loadAliases($class) + { + $aliases = static::$aliases; + + if (array_key_exists($class, $aliases)) + { + return class_alias($aliases[$class], $class); + } + + // Allow calling facade in namespaced class + // without resetting to the root namespace + $classPieces = explode('\\', $class); + $classAlt = array_pop($classPieces); + if (array_key_exists($classAlt, $aliases)) + { + return class_alias($aliases[$classAlt], $class); + } + } + + /** + * Handle dynamic, static calls to the object. + * + * @param string $method + * @param array $args + * @return mixed + */ + public static function __callStatic($method, $args) + { + $instance = static::getRoot(); + + // Seems counter-intuitive but the switch here can + // actually be faster than calling call_user_func_array + // every time. + switch (count($args)) + { + case 0: + return $instance->$method(); + + case 1: + return $instance->$method($args[0]); + + case 2: + return $instance->$method($args[0], $args[1]); + + case 3: + return $instance->$method($args[0], $args[1], $args[2]); + + case 4: + return $instance->$method($args[0], $args[1], $args[2], $args[3]); + + default: + return call_user_func_array(array($instance, $method), $args); + } + } +} diff --git a/core/libraries/Hubzero/Facades/Filesystem.php b/core/libraries/Hubzero/Facades/Filesystem.php new file mode 100644 index 00000000000..e20e3eb36dd --- /dev/null +++ b/core/libraries/Hubzero/Facades/Filesystem.php @@ -0,0 +1,26 @@ +has('auth')) + { + $logger = $instance->logger('auth'); + } + else + { + $logger = $instance->logger(); + } + + return $logger->info($message); + } + + /** + * Log an entry to the spam logger + * + * @param string $message + * @return boolean + */ + public static function spam($message) + { + $instance = static::getRoot(); + + if ($instance->has('spam')) + { + $logger = $instance->logger('spam'); + } + else + { + $logger = $instance->logger(); + } + + return $logger->info($message); + } +} diff --git a/core/libraries/Hubzero/Facades/Module.php b/core/libraries/Hubzero/Facades/Module.php new file mode 100644 index 00000000000..8d3adc224ba --- /dev/null +++ b/core/libraries/Hubzero/Facades/Module.php @@ -0,0 +1,24 @@ +app = Facade::getApplication(); + } + + public function tearDown() + { + Facade::setApplication($this->app); + } + + /** + * Test setting and getting underlying application + * + * @covers \Hubzero\Facades\Facade::setApplication + * @covers \Hubzero\Facades\Facade::getApplication + * @return void + **/ + public function testSetAndGetApplication() + { + $foo = new Foo; + + $app = new Application; + $app['foo'] = $foo; + + FooFacade::setApplication($app); + + $this->assertEquals($app, FooFacade::getApplication()); + } + + /** + * Test getRoot method + * + * @covers \Hubzero\Facades\Facade::getRoot + * @return void + **/ + public function testGetRoot() + { + $foo = new Foo; + + $app = new Application; + $app['foo'] = $foo; + + FooFacade::setApplication($app); + + $this->assertEquals($foo, FooFacade::getRoot()); + } + + /** + * Tests getAccessor() method + * + * @covers \Hubzero\Facades\Facade::getAccessor + * @return void + **/ + public function testGetAccessor() + { + $foo = new Foo; + + $app = new Application; + $app['foo'] = $foo; + + BadFacade::setApplication($app); + + $this->setExpectedException('RuntimeException'); + + BadFacade::bar(); + } + + /** + * Test Facade calls the underlying application + * + * @covers \Hubzero\Facades\Facade::getAccessor + * @covers \Hubzero\Facades\Facade::__callStatic + * @return void + **/ + public function testFacadeCallsUnderlyingApplication() + { + $foo = new Foo; + + $app = new Application; + $app['foo'] = $foo; + + FooFacade::setApplication($app); + + $this->assertEquals('baz', FooFacade::bar()); + + $argsCount = FooFacade::multiArg('one'); + $this->assertEquals($argsCount, 1); + + $argsCount = FooFacade::multiArg('one', 'two'); + $this->assertEquals($argsCount, 2); + + $argsCount = FooFacade::multiArg('one', 'two', 'three'); + $this->assertEquals($argsCount, 3); + + $argsCount = FooFacade::multiArg('one', 'two', 'three', 'four'); + $this->assertEquals($argsCount, 4); + + $argsCount = FooFacade::multiArg('one', 'two', 'three', 'four', 'five'); + $this->assertEquals($argsCount, 5); + } + + /** + * Tests swap() method + * + * @covers \Hubzero\Facades\Facade::swap + * @return void + **/ + public function testSwap() + { + $foo = new Foo; + + $app = new Application; + $app['foo'] = $foo; + + FooFacade::setApplication($app); + + $bar = new Bar; + + FooFacade::swap($bar); + + $this->assertEquals('zab', FooFacade::bar()); + $this->assertEquals($bar, FooFacade::getRoot()); + } +} diff --git a/core/libraries/Hubzero/Facades/Tests/Mock/Application.php b/core/libraries/Hubzero/Facades/Tests/Mock/Application.php new file mode 100644 index 00000000000..9308e511f8e --- /dev/null +++ b/core/libraries/Hubzero/Facades/Tests/Mock/Application.php @@ -0,0 +1,89 @@ +offsetSet($key, $val); + } + + /** + * Get a value + * + * @return mixed + */ + public function get($key) + { + return $this->offsetGet($key); + } + + /** + * Check a value exists + * + * @param string $key + * @return bool + */ + public function offsetExists($key) + { + return isset($this->attributes[$key]); + } + + /** + * Get a value + * + * @return mixed + */ + public function offsetGet($key) + { + return $this->attributes[$key]; + } + + /** + * Set a value + * + * @param string $key + * @param mixed $val + * @return void + */ + public function offsetSet($key, $val) + { + $this->attributes[$key] = $val; + } + + /** + * Unsert a value + * + * @param string $key + * @return void + */ + public function offsetUnset($key) + { + unset($this->attributes[$key]); + } +} diff --git a/core/libraries/Hubzero/Facades/Tests/Mock/BadFacade.php b/core/libraries/Hubzero/Facades/Tests/Mock/BadFacade.php new file mode 100644 index 00000000000..893173befef --- /dev/null +++ b/core/libraries/Hubzero/Facades/Tests/Mock/BadFacade.php @@ -0,0 +1,19 @@ +

' . $title . '

'; + + $app = static::getApplication(); + + $app->set('ComponentTitle', $html); + + if ($app->has('document')) + { + $app->get('document')->setTitle($app->get('config')->get('sitename') . ' - ' . $app->get('language')->txt('JADMINISTRATION') . ' - ' . $title); + } + } + + /** + * Writes a spacer cell. + * + * @param string $width The width for the cell + * @return void + */ + public static function spacer($width = '') + { + static::getRoot()->appendButton('Separator', 'spacer', $width); + } + + /** + * Writes a divider between menu buttons + * + * @return void + */ + public static function divider() + { + static::getRoot()->appendButton('Separator', 'divider'); + } + + /** + * Writes a custom option and task button for the button bar. + * + * @param string $task The task to perform (picked up by the switch($task) blocks. + * @param string $icon The image to display. + * @param string $iconOver The image to display when moused over. + * @param string $alt The alt text for the icon image. + * @param bool $listSelect True if required to check that a standard list item is checked. + * @return void + */ + public static function custom($task = '', $icon = '', $iconOver = '', $alt = '', $listSelect = true) + { + // Strip extension. + $icon = preg_replace('#\.[^.]*$#', '', $icon); + + // Add a standard button. + static::getRoot()->appendButton('Standard', $icon, $alt, $task, $listSelect); + } + + /** + * Writes a preview button for a given option (opens a popup window). + * + * @param string $url The name of the popup file (excluding the file extension) + * @param bool $updateEditors + * @return void + */ + public static function preview($url = '', $updateEditors = false) + { + static::getRoot()->appendButton('Popup', 'preview', 'Preview', $url . '&task=preview'); + } + + /** + * Writes a preview button for a given option (opens a popup window). + * + * @param string $ref The name of the popup file (excluding the file extension for an xml file). + * @param bool $com Use the help file in the component directory. + * @param string $override Use this URL instead of any other + * @param string $component Name of component to get Help (null for current component) + * @return void + */ + public static function help($url, $width = 700, $height = 500) + { + static::getRoot()->appendButton('Help', $url, $width, $height); + } + + /** + * Writes a cancel button that will go back to the previous page without doing + * any other operation. + * + * @param string $alt Alternative text. + * @param string $href URL of the href attribute. + * @return void + */ + public static function back($alt = 'JTOOLBAR_BACK', $href = 'javascript:history.back();') + { + static::getRoot()->appendButton('Link', 'back', $alt, $href); + } + + /** + * Writes a media_manager button. + * + * @param string $directory The sub-drectory to upload the media to. + * @param string $alt An override for the alt text. + * @return void + */ + public static function media_manager($directory = '', $alt = 'JTOOLBAR_UPLOAD') + { + static::getRoot()->appendButton('Popup', 'upload', $alt, \Route::url('index.php?option=com_media&tmpl=component&task=popupUpload&folder=' . $directory), 800, 520); + } + + /** + * Writes a common 'default' button for a record. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function makeDefault($task = 'default', $alt = 'JTOOLBAR_DEFAULT') + { + static::getRoot()->appendButton('Standard', 'default', $alt, $task, true); + } + + /** + * Writes a common 'assign' button for a record. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function assign($task = 'assign', $alt = 'JTOOLBAR_ASSIGN') + { + static::getRoot()->appendButton('Standard', 'assign', $alt, $task, true); + } + + /** + * Writes the common 'new' icon for the button bar. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @param boolean $check True if required to check that a standard list item is checked. + * @return void + */ + public static function addNew($task = 'add', $alt = 'JTOOLBAR_NEW', $check = false) + { + static::getRoot()->appendButton('Standard', 'new', $alt, $task, $check); + } + + /** + * Writes a common 'publish' button. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @param boolean $check True if required to check that a standard list item is checked. + * @return void + */ + public static function publish($task = 'publish', $alt = 'JTOOLBAR_PUBLISH', $check = false) + { + static::getRoot()->appendButton('Standard', 'publish', $alt, $task, $check); + } + + /** + * Writes a common 'publish' button for a list of records. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function publishList($task = 'publish', $alt = 'JTOOLBAR_PUBLISH') + { + static::getRoot()->appendButton('Standard', 'publish', $alt, $task, true); + } + + /** + * Writes a common 'unpublish' button. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @param boolean $check True if required to check that a standard list item is checked. + * @return void + */ + public static function unpublish($task = 'unpublish', $alt = 'JTOOLBAR_UNPUBLISH', $check = false) + { + static::getRoot()->appendButton('Standard', 'unpublish', $alt, $task, $check); + } + + /** + * Writes a common 'unpublish' button for a list of records. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function unpublishList($task = 'unpublish', $alt = 'JTOOLBAR_UNPUBLISH') + { + static::getRoot()->appendButton('Standard', 'unpublish', $alt, $task, true); + } + + /** + * Writes a common 'archive' button for a list of records. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function archiveList($task = 'archive', $alt = 'JTOOLBAR_ARCHIVE') + { + static::getRoot()->appendButton('Standard', 'archive', $alt, $task, true); + } + + /** + * Writes an unarchive button for a list of records. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function unarchiveList($task = 'unarchive', $alt = 'JTOOLBAR_UNARCHIVE') + { + static::getRoot()->appendButton('Standard', 'unarchive', $alt, $task, true); + } + + /** + * Writes a common 'edit' button for a list of records. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function editList($task = 'edit', $alt = 'JTOOLBAR_EDIT') + { + static::getRoot()->appendButton('Standard', 'edit', $alt, $task, true); + } + + /** + * Writes a common 'edit' button for a template html. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function editHtml($task = 'edit_source', $alt = 'JTOOLBAR_EDIT_HTML') + { + static::getRoot()->appendButton('Standard', 'edithtml', $alt, $task, true); + } + + /** + * Writes a common 'edit' button for a template css. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function editCss($task = 'edit_css', $alt = 'JTOOLBAR_EDIT_CSS') + { + static::getRoot()->appendButton('Standard', 'editcss', $alt, $task, true); + } + + /** + * Writes a common 'delete' button for a list of records. + * + * @param string $msg Postscript for the 'are you sure' message. + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function deleteList($msg = '', $task = 'remove', $alt = 'JTOOLBAR_DELETE') + { + $bar = static::getRoot(); + // Add a delete button. + if ($msg) + { + $bar->appendButton('Confirm', $msg, 'delete', $alt, $task, true); + } + else + { + $bar->appendButton('Standard', 'delete', $alt, $task, true); + } + } + + /** + * Write a trash button that will move items to Trash Manager. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @param bool $check + * @return void + */ + public static function trash($task = 'remove', $alt = 'JTOOLBAR_TRASH', $check = true) + { + static::getRoot()->appendButton('Standard', 'trash', $alt, $task, $check, false); + } + + /** + * Writes a save button for a given option. + * Apply operation leads to a save action only (does not leave edit mode). + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function apply($task = 'apply', $alt = 'JTOOLBAR_APPLY') + { + static::getRoot()->appendButton('Standard', 'apply', $alt, $task, false); + } + + /** + * Writes a save button for a given option. + * Save operation leads to a save and then close action. + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function save($task = 'save', $alt = 'JTOOLBAR_SAVE') + { + static::getRoot()->appendButton('Standard', 'save', $alt, $task, false); + } + + /** + * Writes a save and create new button for a given option. + * Save and create operation leads to a save and then add action. + * + * @param string $task + * @param string $alt + * @return void + */ + public static function save2new($task = 'save2new', $alt = 'JTOOLBAR_SAVE_AND_NEW') + { + static::getRoot()->appendButton('Standard', 'save-new', $alt, $task, false); + } + + /** + * Writes a save as copy button for a given option. + * Save as copy operation leads to a save after clearing the key, + * then returns user to edit mode with new key. + * + * @param string $task + * @param string $alt + * @return void + */ + public static function save2copy($task = 'save2copy', $alt = 'JTOOLBAR_SAVE_AS_COPY') + { + static::getRoot()->appendButton('Standard', 'save-copy', $alt, $task, false); + } + + /** + * Writes a checkin button for a given option. + * + * @param string $task + * @param string $alt + * @param boolean $check True if required to check that a standard list item is checked. + * @return void + */ + public static function checkin($task = 'checkin', $alt = 'JTOOLBAR_CHECKIN', $check = true) + { + static::getRoot()->appendButton('Standard', 'checkin', $alt, $task, $check); + } + + /** + * Writes a cancel button and invokes a cancel operation (eg a checkin). + * + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @return void + */ + public static function cancel($task = 'cancel', $alt = 'JTOOLBAR_CANCEL') + { + static::getRoot()->appendButton('Standard', 'cancel', $alt, $task, false); + } + + /** + * Writes a configuration button and invokes a cancel operation (eg a checkin). + * + * @param string $component The name of the component, eg, com_content. + * @param int $height The height of the popup. + * @param int $width The width of the popup. + * @param string $alt The name of the button. + * @param string $path An alternative path for the configuation xml relative to PATH_ROOT. + * @param string $onClose Called on close + * @return void + */ + public static function preferences($component, $height = '550', $width = '875', $alt = 'JToolbar_Options', $path = '', $onClose = '') + { + $component = urlencode($component); + $path = urlencode($path); + $top = 0; + $left = 0; + + static::getRoot()->appendButton('Popup', 'options', $alt, \Route::url('index.php?option=com_config&view=component&component=' . $component . '&path=' . $path . '&tmpl=component'), $width, $height, $top, $left, $onClose); + } + + /** + * Writes a button that prompts for confirmation before executing a task + * + * @param string $msg Postscript for the 'are you sure' message. + * @param string $name Name to be used as apart of the id + * @param string $task An override for the task. + * @param string $alt An override for the alt text. + * @param boolean $list True to allow use of lists + * @return void + */ + public static function confirm($msg = '', $name='delete', $task = 'remove', $alt = 'JTOOLBAR_DELETE', $list = true) + { + static::getRoot()->appendButton('Confirm', $msg, $name, $alt, $task, $list); + } +} diff --git a/core/libraries/Hubzero/Facades/User.php b/core/libraries/Hubzero/Facades/User.php new file mode 100644 index 00000000000..086366c65e0 --- /dev/null +++ b/core/libraries/Hubzero/Facades/User.php @@ -0,0 +1,66 @@ +get('session'); + $registry = $session->get('registry'); + + if (!is_null($registry)) + { + return $registry->get($key, $default); + } + + return $default; + } + + /** + * Sets the value of a user state variable. + * + * @param string $key The path of the state. + * @param string $value The value of the variable. + * @return mixed The previous state, if one existed. + */ + public static function setState($key, $value) + { + $session = self::$app->get('session'); + $registry = $session->get('registry'); + + if (!is_null($registry)) + { + return $registry->set($key, $value); + } + + return null; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Adapter/Ftp.php b/core/libraries/Hubzero/Filesystem/Adapter/Ftp.php new file mode 100644 index 00000000000..1bf547c0f0d --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Adapter/Ftp.php @@ -0,0 +1,891 @@ +setConfig($config); + } + + /** + * Set the config. + * + * @param array $config + * @return object $this + */ + public function setConfig(array $config) + { + foreach ($this->configurable as $setting) + { + if (!isset($config[$setting])) + { + continue; + } + + $method = 'set' . ucfirst($setting); + + if (method_exists($this, $method)) + { + $this->$method($config[$setting]); + } + } + + return $this; + } + + /** + * Returns the host. + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Set the host. + * + * @param string $host + * @return object $this + */ + public function setHost($host) + { + $this->host = $host; + + return $this; + } + + /** + * Returns the ftp port. + * + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * Set the ftp port. + * + * @param mixed $port + * @return object $this + */ + public function setPort($port) + { + $this->port = (int) $port; + + return $this; + } + + /** + * Returns the root folder to work from. + * + * @return string + */ + public function getRoot() + { + return $this->root; + } + + /** + * Set the root folder to work from. + * + * @param string $root + * @return object $this + */ + public function setRoot($root) + { + $this->root = rtrim($root, '\\/') . $this->separator; + + return $this; + } + + /** + * Returns the ftp username. + * + * @return string username + */ + public function getUsername() + { + return empty($this->username) ? 'anonymous' : $this->username; + } + + /** + * Set ftp username. + * + * @param string $username + * @return object $this + */ + public function setUsername($username) + { + $this->username = $username; + + return $this; + } + + /** + * Returns the password. + * + * @return string password + */ + public function getPassword() + { + return $this->password; + } + + /** + * Set the ftp password. + * + * @param string $password + * @return object $this + */ + public function setPassword($password) + { + $this->password = $password; + + return $this; + } + + /** + * Returns the amount of seconds before the connection will timeout. + * + * @return int + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Set the amount of seconds before the connection should timeout. + * + * @param int $timeout + * @return object $this + */ + public function setTimeout($timeout) + { + $this->timeout = (int) $timeout; + + return $this; + } + + /** + * Get the transfer mode. + * + * @return int + */ + public function getTransferMode() + { + return $this->transferMode; + } + + /** + * Set the transfer mode. + * + * @param int $mode + * @return object $this + */ + public function setTransferMode($mode) + { + $this->transferMode = $mode; + + return $this; + } + + /** + * Get if Ssl is enabled. + * + * @return bool + */ + public function getSsl() + { + return $this->ssl; + } + + /** + * Set if Ssl is enabled. + * + * @param bool $ssl + * @return object $this + */ + public function setSsl($ssl) + { + $this->ssl = (bool) $ssl; + + return $this; + } + + /** + * Set if passive mode should be used. + * + * @param bool $passive + * @return object $this + */ + public function setPassive($passive = true) + { + $this->passive = $passive; + + return $this; + } + + /** + * Get the connection. + * + * @return resource|Net_SFTP + */ + public function getConnection() + { + if (!$this->connection) + { + $this->connect(); + } + + return $this->connection; + } + + /** + * Connect to the FTP server. + * + * @return void + */ + public function connect() + { + if ($this->ssl) + { + $this->connection = ftp_ssl_connect($this->getHost(), $this->getPort(), $this->getTimeout()); + } + else + { + $this->connection = ftp_connect($this->getHost(), $this->getPort(), $this->getTimeout()); + } + + if (!$this->connection) + { + throw new RuntimeException(sprintf('Could not connect to host: %s, port: %s', $this->getHost(), $this->getPort())); + } + + $this->login(); + $this->setConnectionPassiveMode(); + $this->setConnectionRoot(); + } + + /** + * Set the connections to passive mode. + * + * @return void + * @throws RuntimeException + */ + protected function setConnectionPassiveMode() + { + if (!ftp_pasv($this->getConnection(), $this->passive)) + { + throw new RuntimeException(sprintf('Could not set passive mode for connection: %s::%s', $this->getHost(), $this->getPort())); + } + } + + /** + * Set the connection root. + * + * @return void + */ + protected function setConnectionRoot() + { + $root = $this->getRoot(); + $connection = $this->getConnection(); + + if ($root && !ftp_chdir($connection, $root)) + { + throw new RuntimeException('Root is invalid or does not exist: ' . $this->getRoot()); + } + + // Store absolute path for further reference. + // This is needed when creating directories and + // initial root was a relative path, else the root + // would be relative to the chdir'd path. + $this->root = ftp_pwd($connection); + } + + /** + * Login. + * + * @return void + * @throws RuntimeException + */ + protected function login() + { + set_error_handler(function () {}); + $isLoggedIn = ftp_login($this->getConnection(), $this->getUsername(), $this->getPassword()); + restore_error_handler(); + + if (!$isLoggedIn) + { + $this->disconnect(); + + throw new RuntimeException(sprintf('Could not login with connection: %s::%s, username: %s', $this->getHost(), $this->getPort(), $this->getUsername())); + } + } + + /** + * Disconnect from the FTP server. + * + * @return void + */ + public function disconnect() + { + if ($this->connection) + { + ftp_close($this->connection); + } + + $this->connection = null; + } + + /** + * {@inheritdoc} + */ + public function write($path, $contents) + { + $mimetype = Util::guessMimeType($path, $contents); + + $stream = tmpfile(); + fwrite($stream, $contents); + rewind($stream); + + $result = $this->writeStream($path, $stream); + $result = fclose($stream) && $result; + + if ($result === false) + { + return false; + } + + $this->setPermissions($path, $visibility); + + return true; + } + + /** + * {@inheritdoc} + */ + public function writeStream($path, $resource) + { + $this->ensureDirectory(Util::dirname($path)); + + if (!ftp_fput($this->getConnection(), $path, $resource, $this->transferMode)) + { + return false; + } + + $this->setPermissions($path, $visibility); + + return true; + } + + /** + * {@inheritdoc} + */ + public function prepend($path, $contents) + { + if ($this->exists($path)) + { + return $this->write($path, $contents . $this->read($path)); + } + + return $this->write($path, $contents); + } + + /** + * {@inheritdoc} + */ + public function append($path, $contents) + { + if ($this->exists($path)) + { + return $this->write($path, $this->read($path) . $contents); + } + + return $this->write($path, $contents); + } + + /** + * {@inheritdoc} + */ + public function move($path, $target) + { + return $this->rename($path, $target); + } + + /** + * {@inheritdoc} + */ + public function rename($path, $target) + { + return ftp_rename($this->getConnection(), $path, $target); + } + + /** + * {@inheritdoc} + */ + public function delete($path) + { + return ftp_delete($this->getConnection(), $path); + } + + /** + * {@inheritdoc} + */ + public function find($paths, $file) + { + $paths = is_array($path) ? $path : array($path); + + foreach ($paths as $path) + { + $fullname = $path . DS . $file; + + if ($this->isFile($fullname) && substr($fullname, 0, strlen($path)) == $path) + { + return $fullname; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function makeDirectory($path, $mode = 0755, $recursive = true, $force = false) + { + $result = false; + + $connection = $this->getConnection(); + $directories = explode('/', $path); + + foreach ($directories as $directory) + { + $result = $this->createActualDirectory($directory, $connection); + + if (!$result) + { + break; + } + + ftp_chdir($connection, $directory); + } + + $this->setConnectionRoot(); + + return $result; + } + + /** + * Create a directory. + * + * @param string $directory + * @param resource $connection + * @return bool + */ + protected function createActualDirectory($directory, $connection) + { + // List the current directory + $listing = ftp_nlist($connection, '.'); + + foreach ($listing as $key => $item) + { + if (preg_match('~^\./.*~', $item)) + { + $listing[$key] = substr($item, 2); + } + } + + if (in_array($directory, $listing)) + { + return true; + } + + return (boolean) ftp_mkdir($connection, $directory); + } + + /** + * {@inheritdoc} + */ + public function copyDirectory($path, $target, $options = null) + { + $response = $this->readStream($path); + + if ($response === false || ! is_resource($response['stream'])) + { + return false; + } + + $result = $this->writeStream($target, $response['stream']); + + if ($result !== false && is_resource($response['stream'])) + { + fclose($response['stream']); + } + + return $result !== false; + } + + /** + * {@inheritdoc} + */ + public function deleteDirectory($dirname) + { + $connection = $this->getConnection(); + + $contents = array_reverse($this->listDirectoryContents($dirname)); + + foreach ($contents as $object) + { + if ($object['type'] === 'file') + { + if (!ftp_delete($connection, $object['path'])) + { + return false; + } + } + elseif (!ftp_rmdir($connection, $object['path'])) + { + return false; + } + } + + return ftp_rmdir($connection, $dirname); + } + + /** + * {@inheritdoc} + */ + public function mimetype($path) + { + if (! $contents = $this->read($path)) + { + return false; + } + + return Util::guessMimeType($path, $contents); + } + + /** + * {@inheritdoc} + */ + public function isDirectory($directory) + { + $result = @ftp_chdir($this->connection(), $directory); + $result = $result ? true : false; + + return $result; + } + + /** + * {@inheritdoc} + */ + public function isWritable($path) + { + return is_writable($path); + } + + /** + * {@inheritdoc} + */ + public function isFile($file) + { + $result = @ftp_chdir($this->connection(), $directory); + $result = $result ? false : true; + + return $result; + } + + /** + * {@inheritdoc} + */ + public function isSafe($file) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function read($path) + { + if (! $object = $this->readStream($path)) + { + return false; + } + + $contents = stream_get_contents($object['stream']); + fclose($object['stream']); + unset($object['stream']); + + return $contents; + } + + /** + * {@inheritdoc} + */ + public function readStream($path) + { + $stream = fopen('php://temp', 'w+'); + $result = ftp_fget($this->getConnection(), $stream, $path, $this->transferMode); + rewind($stream); + + if (!$result) + { + fclose($stream); + + return false; + } + + return compact('stream'); + } + + /** + * {@inheritdoc} + */ + public function setPermissions($path, $filemode = '0644', $foldermode = '0755') + { + if (!ftp_chmod($this->getConnection(), $foldermode, $path)) + { + return false; + } + + return true; + } + + /** + * Ensure a directory exists. + * + * @param string $dirname + */ + public function ensureDirectory($dirname) + { + if (!empty($dirname) && !$this->exists($dirname)) + { + $this->makeDirectory($dirname); + } + } + + /** + * {@inheritdoc} + */ + public function listContents($path, $filter = '.', $recursive = false, $full = false, $exclude = array('.svn', '.git', 'CVS', '.DS_Store', '__MACOSX')) + { + $listing = ftp_rawlist($this->getConnection(), '-lna ' . $path, $recursive); + + return $listing ? $this->normalizeListing($listing, ($full ? '' : $path), $filter, $exclude) : array(); + } + + /** + * Normalize a directory listing. + * + * @param array $listing + * @param string $prefix + * @return array Directory listing + */ + protected function normalizeListing(array $listing, $prefix = '', $filter = '.', $exclude = array('.svn', '.git', 'CVS', '.DS_Store', '__MACOSX')) + { + $base = $prefix; + + $result = array(); + $listing = $this->removeDotDirectories($listing); + + while ($item = array_shift($listing)) + { + if (preg_match('#^.*:$#', $item)) + { + $base = trim($item, ':'); + continue; + } + + $file = $this->normalizeObject($item, $base); + + $name = basename($file['path']); + + if (preg_match("/$filter/", $name) && !in_array($name, $exclude)) + { + $result[] = $file; + } + } + + return $this->sortListing($result); + } + + /** + * Sort a directory listing. + * + * @param array $result + * @return array Sorted listing + */ + protected function sortListing(array $result) + { + $compare = function ($one, $two) + { + return strnatcmp($one['path'], $two['path']); + }; + + usort($result, $compare); + + return $result; + } + + /** + * Normalize a file entry. + * + * @param string $item + * @param string $base + * @return array Normalized file array + */ + protected function normalizeObject($item, $base) + { + $item = preg_replace('#\s+#', ' ', trim($item), 7); + list($permissions, /* $number */, /* $owner */, /* $group */, $size, /* $month */, /* $day */, /* $time*/, $name) = explode(' ', $item, 9); + + $type = $this->detectType($permissions); + $path = empty($base) ? $name : $base . $this->separator . $name; + + if ($type === 'dir') + { + return compact('type', 'path'); + } + + $permissions = $this->normalizePermissions($permissions); + $size = (int) $size; + + return compact('type', 'path', 'permissions', 'size'); + } + + /** + * Get the file type from the permissions. + * + * @param string $permissions + * @return string File type + */ + protected function detectType($permissions) + { + return substr($permissions, 0, 1) === 'd' ? 'dir' : 'file'; + } + + /** + * Normalize a permissions string. + * + * @param string $permissions + * @return int + */ + protected function normalizePermissions($permissions) + { + // remove the type identifier + $permissions = substr($permissions, 1); + + // map the string rights to the numeric counterparts + $map = array( + '-' => '0', + 'r' => '4', + 'w' => '2', + 'x' => '1' + ); + $permissions = strtr($permissions, $map); + + // split up the permission groups + $parts = str_split($permissions, 3); + + // convert the groups + $mapper = function ($part) + { + return array_sum(str_split($part)); + }; + + // get the sum of the groups + return array_sum(array_map($mapper, $parts)); + } + + /** + * Filter out dot-directories. + * + * @param array $list + * @return array + */ + public function removeDotDirectories(array $list) + { + $filter = function ($line) + { + if (!empty($line) && !preg_match('#.* \.(\.)?$|^total#', $line)) + { + return true; + } + + return false; + }; + + return array_filter($list, $filter); + } + + /** + * Disconnect on destruction. + * + * @return void + */ + public function __destruct() + { + $this->disconnect(); + } +} diff --git a/core/libraries/Hubzero/Filesystem/Adapter/Local.php b/core/libraries/Hubzero/Filesystem/Adapter/Local.php new file mode 100644 index 00000000000..aec05899bf8 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Adapter/Local.php @@ -0,0 +1,593 @@ +command = $command; + } + + /** + * {@inheritdoc} + */ + public function exists($path) + { + return file_exists($path); + } + + /** + * {@inheritdoc} + */ + public function read($path) + { + if ($this->isFile($path)) + { + return file_get_contents($path); + } + + throw new FileNotFoundException(\Lang::txt('File does not exist at path %s', $path)); + } + + /** + * {@inheritdoc} + */ + public function write($path, $contents) + { + return file_put_contents($path, $contents); + } + + /** + * {@inheritdoc} + */ + public function prepend($path, $contents) + { + if ($this->exists($path)) + { + return $this->write($path, $contents . $this->read($path)); + } + + return $this->write($path, $contents); + } + + /** + * {@inheritdoc} + */ + public function append($path, $contents) + { + return file_put_contents($path, $contents, FILE_APPEND); + } + + /** + * {@inheritdoc} + */ + public function delete($path) + { + $paths = is_array($path) ? $path : array($path); + + $success = true; + + foreach ($paths as $path) + { + if (!is_file($path)) + { + continue; + } + + // Try making the file writable first. If it's read-only, it can't be deleted + // on Windows, even if the parent folder is writable + @chmod($path, 0777); + + if (!@unlink($path)) + { + $success = false; + } + } + + return $success; + } + + /** + * Upload a file + * + * @param string $path + * @param string $target + * @return bool + */ + public function upload($path, $target) + { + $success = false; + + $dir = dirname($target); + + if (!is_dir($dir)) + { + if (!$this->makeDirectory($dir)) + { + return $success; + } + } + + if (is_writeable($dir) && move_uploaded_file($path, $target)) + { + if ($this->setPermissions($target)) + { + $success = true; + } + } + + return $success; + } + + /** + * {@inheritdoc} + */ + public function move($path, $target) + { + return $this->rename($path, $target); + } + + /** + * {@inheritdoc} + */ + public function rename($path, $target) + { + return rename($path, $target); + } + + /** + * {@inheritdoc} + */ + public function copy($path, $target) + { + return copy($path, $target); + } + + /** + * {@inheritdoc} + */ + public function find($paths, $file) + { + $paths = is_array($paths) ? $paths : array($paths); + + foreach ($paths as $path) + { + $fullname = $path . DS . $file; + + // Is the path based on a stream? + if (strpos($path, '://') === false) + { + // Not a stream, so do a realpath() to avoid directory + // traversal attempts on the local file system. + $path = realpath($path); + $fullname = realpath($fullname); + } + + // The substr() check added to make sure that the realpath() + // results in a directory registered so that + // non-registered directories are not accessible via directory + // traversal attempts. + if (file_exists($fullname) && substr($fullname, 0, strlen($path)) == $path) + { + return $fullname; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function name($path) + { + return preg_replace('#\.[^.]*$#', '', $path); + //return pathinfo($path, PATHINFO_FILENAME); + } + + /** + * {@inheritdoc} + */ + public function extension($path) + { + $dot = strrpos($path, '.') + 1; + + return substr($path, $dot); + //return pathinfo($path, PATHINFO_EXTENSION); + } + + /** + * {@inheritdoc} + */ + public function type($path) + { + return filetype($path); + } + + /** + * {@inheritdoc} + */ + public function size($path) + { + if ($this->isFile($path)) + { + return filesize($path); + } + + $ret = 0; + foreach (glob($path . DIRECTORY_SEPARATOR . "*") as $fn) + { + $ret += $this->size($fn); + } + return $ret; + } + + /** + * {@inheritdoc} + */ + public function lastModified($path) + { + return filemtime($path); + } + + /** + * {@inheritdoc} + */ + public function mimetype($path) + { + $mimeType = null; + + if (class_exists('Finfo') && $this->exists($path)) + { + $finfo = new Finfo(FILEINFO_MIME_TYPE); + try + { + $mimeType = $finfo->file($path); + } + catch (\Exception $e) + { + // Gracefully handle non-standard filetypes + } + } + + if (empty($mimeType) || $mimeType === 'text/plain') + { + $extension = $this->extension($path); + + if ($extension) + { + $mimeType = MimeType::detectByFileExtension($extension) ?: 'text/plain'; + } + } + + return $mimeType; + } + + /** + * {@inheritdoc} + */ + public function isDirectory($directory) + { + return is_dir($directory); + } + + /** + * {@inheritdoc} + */ + public function isWritable($path) + { + return is_writable($path); + } + + /** + * {@inheritdoc} + */ + public function isFile($file) + { + return is_file($file); + } + + /** + * {@inheritdoc} + */ + public function isSafe($file) + { + if ($this->command) + { + $command = trim($this->command); + if (strstr($command, '%s')) + { + $command = sprintf($command, $file); + } + else + { + $command .= ' ' . str_replace(' ', '\ ', $file); + } + + exec($command, $output, $status); + + if ($status == 1) + { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function listContents($path, $filter = '.', $recursive = false, $full = false, $exclude = array('.svn', '.git', 'CVS', '.DS_Store', '__MACOSX')) + { + $result = array(); + + if (!is_dir($path)) + { + return $result; + } + + $iterator = $recursive ? $this->getRecursiveDirectoryIterator($path) : $this->getDirectoryIterator($path); + + foreach ($iterator as $file) + { + if ($file->isLink()) + { + continue; + } + + if (preg_match('#(^|/|\\\\)\.{1,2}$#', $file->getPathname())) + { + continue; + } + + $name = $file->getFilename(); + + if (preg_match("/$filter/", $name) && !in_array($name, $exclude)) + { + $result[] = $this->normalizeFileInfo($file, ($full ? null : $path)); + } + } + + return $result; + } + + /** + * Normalize the file info. + * + * @param object $file SplFileInfo + * @return array + */ + protected function normalizeFileInfo(SplFileInfo $file, $base = null) + { + $normalized = array( + 'type' => $file->getType(), + 'path' => ($base ? substr($file->getPathname(), strlen($base)) : $file->getPathname()), + 'timestamp' => $file->getMTime() + ); + + if ($normalized['type'] === 'file') + { + $normalized['size'] = $file->getSize(); + } + + return $normalized; + } + + /** + * @param string $path + * @return object RecursiveIteratorIterator + */ + protected function getRecursiveDirectoryIterator($path) + { + $directory = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS); + $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + + return $iterator; + } + + /** + * @param string $path + * @return object DirectoryIterator + */ + protected function getDirectoryIterator($path) + { + return new DirectoryIterator($path); + } + + /** + * {@inheritdoc} + */ + public function makeDirectory($path, $mode = 0755, $recursive = true, $force = false) + { + if ($force) + { + return @mkdir($path, $mode, $recursive); + } + + return mkdir($path, $mode, $recursive); + } + + /** + * {@inheritdoc} + */ + public function copyDirectory($directory, $destination, $options = null) + { + if (!$this->isDirectory($directory)) + { + return false; + } + + $options = $options ?: FilesystemIterator::SKIP_DOTS; + + // If the destination directory does not actually exist, we will go ahead and + // create it recursively, which just gets the destination prepared to copy + // the files over. Once we make the directory we'll proceed the copying. + if (!$this->isDirectory($destination)) + { + $this->makeDirectory($destination, 0777, true); + } + + $items = new FilesystemIterator($directory, $options); + + foreach ($items as $item) + { + // As we spin through items, we will check to see if the current file is actually + // a directory or a file. When it is actually a directory we will need to call + // back into this function recursively to keep copying these nested folders. + $target = $destination . DS . $item->getBasename(); + + if ($item->isDir()) + { + $path = $item->getPathname(); + + if (!$this->copyDirectory($path, $target, $options)) + { + return false; + } + } + + // If the current items is just a regular file, we will just copy this to the new + // location and keep looping. If for some reason the copy fails we'll bail out + // and return false, so the developer is aware that the copy process failed. + else + { + if (!$this->copy($item->getPathname(), $target)) + { + return false; + } + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function deleteDirectory($directory, $preserve = false) + { + if (!$this->isDirectory($directory)) + { + return false; + } + + $items = new FilesystemIterator($directory); + + foreach ($items as $item) + { + // If the item is a directory, we can just recurse into the function and + // delete that sub-director, otherwise we'll just delete the file and + // keep iterating through each file until the directory is cleaned. + if ($item->isDir()) + { + $this->deleteDirectory($item->getPathname()); + } + + // If the item is just a file, we can go ahead and delete it since we're + // just looping through and waxing all of the files in this directory + // and calling directories recursively, so we delete the real path. + else + { + $this->delete($item->getPathname()); + } + } + + if (!$preserve) + { + @rmdir($directory); + } + + return true; + } + + /** + * Chmods files and directories recursively to given permissions. + * + * @param string $path Root path to begin changing mode [without trailing slash]. + * @param string $filemode Octal representation of the value to change file mode to [null = no change]. + * @param string $foldermode Octal representation of the value to change folder mode to [null = no change]. + * @return boolean True if successful [one fail means the whole operation failed]. + */ + public function setPermissions($path, $filemode = '0644', $foldermode = '0755') + { + // Initialise return value + $success = true; + + if (is_dir($path)) + { + $dh = opendir($path); + + $items = new FilesystemIterator($path, FilesystemIterator::SKIP_DOTS); + + foreach ($items as $item) + { + if ($item->isDir()) + { + if ($this->setPermissions($item->getPathname(), $filemode, $foldermode)) + { + $success = false; + } + + continue; + } + + if (isset($filemode)) + { + if (!@chmod($item->getPathname(), octdec($filemode))) + { + $success = false; + } + } + } + + if (isset($foldermode)) + { + if (!@chmod($path, octdec($foldermode))) + { + $success = false; + } + } + } + else + { + if (isset($filemode)) + { + $success = @chmod($path, octdec($filemode)); + } + } + + return $success; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Adapter/None.php b/core/libraries/Hubzero/Filesystem/Adapter/None.php new file mode 100644 index 00000000000..02805177ee5 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Adapter/None.php @@ -0,0 +1,244 @@ +sortByCallback(function($a, $b) use ($key, $asc) + { + if (!isset($a->$key) || !isset($b->$key)) + { + if (!isset($a->$key)) + { + return ($asc) ? -1 : 1; + } + + if (!isset($b->$key)) + { + return ($asc) ? 1 : -1; + } + + return 0; + } + + if ($asc) + { + return strnatcmp($a->$key, $b->$key); + } + else + { + return strnatcmp($b->$key, $a->$key); + } + }); + } + + /** + * Sorts items using the provided callback function + * + * @param closure $callback The sorting function to use + * @return static + **/ + public function sortByCallback($callback) + { + $cache = $this->_data; + + usort($cache, $callback); + + return new static($cache); + } + + /** + * Finds the first entity with a given name, optionally returning it + * + * @param string $name The name of the entity to find + * @param bool $return Whether or not to return the found entity + * @return \Hubzero\Filesystem\File|\Hubzero\Filesystem\Directory|bool + **/ + public function find($name, $return = true) + { + foreach ($this as $entity) + { + if ($entity->isFile()) + { + if ($entity->getName() == $name) + { + return ($return) ? $entity : true; + } + } + else + { + // The result of this is already flat, so no need for recursion + foreach ($entity->listContents(true) as $sub) + { + if ($sub->getName() == $name) + { + return ($return) ? $sub : true; + } + } + } + } + + return false; + } + + /** + * Checks to see if the collection contains a named item + * + * @param string $name The named item to look for + * @return bool + **/ + public function has($name) + { + return $this->find($name, false); + } + + /** + * Checks to see whether or not the required extensions are in the current collection + * + * @param array $requirements The extension requirements to locate + * @return bool + **/ + public function hasExtensions($requirements) + { + $files = $this->getFlatListOfFiles(); + $extensions = $this->getFlatListOfExtensions(); + foreach ($requirements as $type => $constraint) + { + if (is_numeric($constraint)) + { + if (!array_key_exists($type, $extensions) || $extensions[$type] < $constraint) + { + return false; + } + } + else if (is_callable($constraint)) + { + if (call_user_func_array($constraint, [$extensions, $files]) === false) + { + return false; + } + } + } + + return true; + } + + /** + * Finds the first item with the given extension + * + * @param string $extension The extension to look for + * @return \Hubzero\Filesystem\File|bool + **/ + public function findFirstWithExtension($extension) + { + foreach ($this->getFlatListOfFiles() as $file) + { + if ($file->hasExtension($extension)) + { + return $file; + } + } + + return false; + } + + /** + * Finds all items with the given extension + * + * @param string|array $extension The extension(s) to look for + * @return \Hubzero\Filesystem\File + **/ + public function findAllWithExtension($extensions) + { + $found = []; + + if (!is_array($extensions)) + { + $extensions = [$extensions]; + } + + foreach ($this->getFlatListOfFiles() as $file) + { + $ext = $file->getExtension(); + if (in_array($ext, $extensions)) + { + $found[] = $file; + } + } + + return $found; + } + + /** + * Builds a flat list of files by diving down recursively + * + * @return array + **/ + public function getFlatListOfFiles() + { + if (!isset($this->flatData)) + { + $this->flatData = []; + + foreach ($this as $entity) + { + if ($entity->isFile()) + { + $this->flatData[] = $entity; + } + else + { + // The result of this is already flat, so no need for recursion + foreach ($entity->listContents(true) as $sub) + { + if ($sub->isfile()) + { + $this->flatData[] = $sub; + } + } + } + } + } + + return $this->flatData; + } + + /** + * Builds a flat list of extensions based on files list + * + * @return array + **/ + public function getFlatListOfExtensions() + { + if (!isset($this->flatExtentions)) + { + $this->flatExtentions = []; + + foreach ($this->getFlatListOfFiles() as $file) + { + $this->flatExtentions[] = $file->getExtension(); + } + + $this->flatExtentions = array_count_values($this->flatExtentions); + } + + return $this->flatExtentions; + } + + /** + * Compresses/archives entities in collection + * + * @param bool $structure Whether or not to retain directory location of files being zipped + * @param bool $upload Whether or not to reupload compressed to filesystem location + * @return string|bool + */ + public function compress($structure = false, $upload = false) + { + if (!extension_loaded('zip')) + { + return false; + } + + // Get temp directory + $adapter = null; + $temp = sys_get_temp_dir(); + $uniqueId = uniqid(); + $tarname = uniqid() . '.zip'; + $zip = new \ZipArchive; + + if ($zip->open($temp . DS . $tarname, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) + { + foreach ($this->_data as $entity) + { + if ($entity->isFile()) + { + $fileSrc = $entity->readStream(); + $parent = $entity->getParent(); + if (!empty($parent)) + { + $parent = $temp . DS . $uniqueId . DS . $parent; + if (!is_dir($parent)) + { + mkdir($parent, 0755, true); + } + } + $tmpFilePath = $temp . DS . $uniqueId . DS . $entity->getPath(); + $bufferSize = 1024 * 8; + $tmpSrc = fopen($tmpFilePath, 'w'); + while (!feof($fileSrc)) + { + $buffer = fread($fileSrc, $bufferSize); + fwrite($tmpSrc, $buffer); + } + fclose($tmpSrc); + fclose($fileSrc); + $zip->addFile($tmpFilePath, $structure ? $entity->getPath() : $entity->getFileName()); + } + else if ($entity->isDir() && $structure) + { + $zip->addEmptyDir($entity->getPath()); + } + + // Set some vars in case we need them later + $adapter = $adapter ?: $entity->getAdapter(); + } + + $zip->close(); + + $local = Manager::getTempPath($tarname); + + if ($upload) + { + // @FIXME: use manager copy? + $entity = Entity::fromPath($tarname, $adapter); + $entity->put($local->readAndDelete()); + + return $entity; + } + else + { + return $local; + } + } + else + { + return false; + } + } +} diff --git a/core/libraries/Hubzero/Filesystem/Directory.php b/core/libraries/Hubzero/Filesystem/Directory.php new file mode 100644 index 00000000000..2a0cfefd06e --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Directory.php @@ -0,0 +1,89 @@ +hasAdapterOrFail()->adapter->listContents($this->getPath(), $recursive); + } + + /** + * Create the directory + * + * @return bool + **/ + public function create() + { + return $this->hasAdapterOrFail()->adapter->createDir($this->getPath()); + } + + /** + * Deletes the directory + * + * @return bool + **/ + public function delete() + { + return $this->hasAdapterOrFail()->adapter->deleteDir($this->getPath()); + } + + public function hasSubDirs() + { + return count($this->subDirectories()) > 0; + } + + /** + * Gets a list of directory objects with a depth for walking a directory structure + * + * @param int $depth How deep you currently are since beginning to walk + * @return array + **/ + public function getSubDirs($depth = 0) + { + $dirs = []; + + $contents = $this->hasAdapterOrFail()->adapter->listContents($this->getPath(), false); + foreach ($contents as $item) + { + if ($item->isDir()) + { + $thisDir = new stdClass(); + $thisDir->depth = $depth; + $thisDir->subdirs = $item->getSubDirs($depth+1); + $thisDir->name = $item->getDisplayName(); + $thisDir->path = $item->getPath(); + $dirs[] = $thisDir; + } + } + return $dirs; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Entity.php b/core/libraries/Hubzero/Filesystem/Entity.php new file mode 100644 index 00000000000..961c2ae5d98 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Entity.php @@ -0,0 +1,530 @@ +setAdapter(func_get_arg(1)); + } + } + + /** + * Creates a new object using a path as our starting point + * + * Don't forget, paths are relative to the adapter root + * + * @param string $path The path to use to build a new entity from + * @param object $adapter The filesystem adapter to use for interaction with the entity + * @return static + **/ + public static function fromPath($path, $adapter = null) + { + // If the entity exists, grab its metadata + if (isset($adapter) && $adapter->has($path)) + { + $metadata = $adapter->getMetadata($path); + } + else + { + // Otherwise, we'll make our best guess at the appropriate data + $path = trim($path, '/'); + $bits = explode('/', $path); + $end = end($bits); + + // The minimum required data is a path and a type + $metadata = [ + 'type' => (strpos($end, '.') !== false) ? 'file' : 'dir', + 'path' => $path + ]; + } + + return self::getSpecialized($metadata, $adapter); + } + + /** + * Creates a new object from metadata + * + * This is very similar to just creating a new object, except you could + * instantiate an entity and not even worry about whether its a dir + * or file at the time of creation. + * + * @param array $properties The properties to use to build a new entity from + * @param object $adapter The filesystem adapter to use for interaction with the entity + * @return static + **/ + public static function fromMetadata($properties, $adapter = null) + { + return self::getSpecialized($properties, $adapter); + } + + /** + * Gets the most specialized object for a given entity type + * + * @param array $properties The properties to use to build a new entity from + * @param object $adapter The filesystem adapter to use for interaction with the entity + * @return static + **/ + protected static function getSpecialized($properties, $adapter) + { + // If it's a directory, stop there + if ($properties['type'] == 'dir') + { + return new Directory($properties, $adapter); + } + + if (!isset($properties['extension'])) + { + $bits = explode('.', $properties['path']); + if (count($bits) > 1) + { + $properties['extension'] = end($bits); + } + else + { + $properties['extension'] = ''; + } + } + + // If it's a file, do we have a more specialized class? + $file = __DIR__ . '/Type/' . ucfirst($properties['extension']) . '.php'; + if (file_exists($file)) + { + $class = __NAMESPACE__ . '\\Type\\' . ucfirst($properties['extension']); + return new $class($properties, $adapter); + } + + return new File($properties, $adapter); + } + + /** + * Calls undefined functions + * + * This is a compatibility helper. Basically, we're trying to map + * unnamed functions to their get* equivalent. + * + * @param string $name The function name being called + * @param array $arguments The arguments to be passed to the function + * @return mixed + **/ + public function __call($name, $arguments) + { + static $methods = []; + + if (empty($methods)) + { + $reflection = with(new \ReflectionClass($this))->getMethods(\ReflectionMethod::IS_PUBLIC); + + foreach ($reflection as $method) + { + $methods[] = $method->name; + } + } + + $alt = 'get' . ucfirst($name); + + if (in_array($alt, $methods)) + { + return call_user_func_array([$this, $alt], $arguments); + } + + throw new BadMethodCallException("'{$name}' method does not exist.", 500); + } + + /** + * Sets the adapter on the entity + * + * @return $this + **/ + public function setAdapter($adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * Gets the filesystem adapter + * + * @return object + **/ + public function getAdapter() + { + return $this->adapter; + } + + /** + * Checks to see if entity is directory + * + * @return bool + **/ + public function isDir() + { + $directory = __NAMESPACE__ . '\\Directory'; + return ($this instanceof $directory); + } + + /** + * Checks to see if entity is a directory + * + * @return bool + **/ + public function isDirectory() + { + return $this->isDir(); + } + + /** + * Checks to see if entity is a file + * + * @return bool + **/ + public function isFile() + { + $file = __NAMESPACE__ . '\\File'; + return ($this instanceof $file); + } + + /** + * Checks to see if entity is already on local filesystem + * + * @return bool + **/ + public function isLocal() + { + $local = 'League\\Flysystem\\Adapter\\Local'; + return ($this->getAdapter()->getAdapter() instanceof $local); + } + + /** + * Checks to see if the entity is virus free + * + * @param bool $remote Whether or not to scan remote files + * @return bool + **/ + public function isSafe($remote = false) + { + // Get the command + // -i says we only want to hear about infected files + // -r says scan recursively, this allows scanning a directory, rather than + // making multiple calls to scan individual files + $command = App::get('config')->get('virus_scanner', 'clamscan -i -r --no-summary --block-encrypted'); + + // Always scan local, and only scan remote if explicitly requested + if ($this->isLocal() || (!$this->isLocal() && $remote)) + { + if (!$this->isLocal()) + { + // Copy to tmp + $temp = Manager::getTempPath(uniqid(true) . '.tmp'); + $this->copy($temp); + $path = $temp->getAbsolutePath(); + } + else + { + $path = $this->getAbsolutePath(); + } + + // Build the command + if (strstr($command, '%s')) + { + $command = sprintf($command, escapeshellarg($path)); + } + else + { + $command .= ' ' . escapeshellarg($path); + } + + // Execute the scan + exec($command, $output, $status); + + // Get rid of the local copy if needed + if (!$this->isLocal()) + { + $temp->delete(); + } + + // Check the status, 1 means virus found, 2 means there was an error + // (these definitions are for clamscan specifically) + if ($status >= 1) + { + return false; + } + } + + return true; + } + + /** + * Grabs the item name + * + * @return string + **/ + public function getName() + { + if (isset($this->basename)) + { + return $this->basename; + } + + if (isset($this->filename)) + { + return $this->filename; + } + + if (isset($this->path)) + { + $bits = explode('/', $this->path); + return end($bits); + } + + return ''; + } + + /** + * Grabs the item name + * + * @return string + **/ + public function getDisplayName() + { + if (isset($this->filename)) + { + // If it is a dotfile the filename is '' and the extension will be the rest of the filename + if ($this->filename == '' && isset($this->extension)) + { + return '.' . $this->extension; + } + return $this->filename; + } + + if (isset($this->basename)) + { + return $this->basename; + } + + if (isset($this->path)) + { + $bits = explode('/', $this->path); + return end($bits); + } + + return ''; + } + + /** + * Grabs the parent element, if applicable + * + * @param bool $raw Whether or not to return raw string or applicable object + * @return string|object + **/ + public function getParent($raw = true) + { + $return = ''; + + if (isset($this->dirname)) + { + $return = $this->dirname; + } + else if ($path = $this->getPath()) + { + $bits = explode('/', $path); + array_pop($bits); + + if (count($bits) > 0) + { + $return = implode('/', $bits); + } + } + + return $raw ? $return : self::fromPath($return, $this->getAdapter()); + } + + /** + * Grabs the timestamp + * + * @return string + **/ + public function getTimestamp() + { + if (!isset($this->timestamp)) + { + $this->timestamp = $this->hasAdapterOrFail()->adapter->getTimestamp($this->getPath()); + } + + return $this->timestamp; + } + + /** + * Grabs the item ownership + * + * @return int + **/ + public function getOwner() + { + return (isset($this->owner)) ? $this->owner : 0; + } + + /** + * Grabs the full path to the entity + * + * @return string + **/ + public function getPath() + { + return $this->path; + } + + /** + * Computes the depth of the entity + * + * @return int + */ + public function getDirLevel() + { + if (!trim($this->getPath())) + { + return 0; + } + + $dirParts = explode('/', $this->getPath()); + + return count($dirParts); + } + + /** + * Grabs the absolute path to the entity (not relative to instance root) + * + * @return string + **/ + public function getAbsolutePath() + { + // Don't let this confuse you...we're getting the actual filesystem adapter from + // our adapter variable, which is really the filesystem class itself...words can be confusing. + return $this->hasAdapterOrFail()->adapter->getAdapter()->applyPathPrefix($this->getPath()); + } + + /** + * Checks for a proper filesystem adapter being set + * + * @return $this + **/ + public function hasAdapterOrFail() + { + if (!isset($this->adapter)) + { + throw new \Exception('No adapter set', 500); + } + + return $this; + } + + /** + * Checks to see if entity exists on filesystem + * + * @return bool + **/ + public function exists() + { + return $this->hasAdapterOrFail()->adapter->has($this->getPath()); + } + + /** + * Checks to see if entity exists on filesystem + * + * @return bool + **/ + public function has() + { + return $this->exists(); + } + + /** + * Moves the entity + * + * @param string $to Where to move the entity + * @return bool + **/ + public function move($to) + { + $dest = $to . '/' . $this->getDisplayName() . '.' . $this->getExtension(); + $return = $this->hasAdapterOrFail()->adapter->rename($this->getPath(), $dest); + + // Update the internal path + if ($return) + { + $this->path = $dest; + } + + return $return; + } + + /** + * Renames the entity + * + * @param string $to What to rename the entity to + * @return bool + **/ + public function rename($to) + { + $dest = $this->getParent() . '/' . $to; + $return = $this->hasAdapterOrFail()->adapter->rename($this->getPath(), $dest); + + // Update the internal path + if ($return) + { + $this->path = $dest; + } + + return $return; + } + + /** + * Copies the entity + * + * @param string|object $to What/where to copy the entity to + * @return bool + **/ + public function copy($to) + { + if (is_string($to)) + { + return $this->hasAdapterOrFail()->adapter->copy($this->getPath(), $to); + } + else + { + return Manager::copy($this, $to); + } + } +} diff --git a/core/libraries/Hubzero/Filesystem/Exception/FileExistsException.php b/core/libraries/Hubzero/Filesystem/Exception/FileExistsException.php new file mode 100644 index 00000000000..833a1440f5a --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Exception/FileExistsException.php @@ -0,0 +1,43 @@ +path = $path; + + parent::__construct('File already exists at path: ' . $this->getPath(), $code, $previous); + } + + /** + * Get the path which was not found. + * + * @return string + */ + public function getPath() + { + return $this->path; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Exception/FileNotFoundException.php b/core/libraries/Hubzero/Filesystem/Exception/FileNotFoundException.php new file mode 100644 index 00000000000..fbf7e25e128 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Exception/FileNotFoundException.php @@ -0,0 +1,43 @@ +path = $path; + + parent::__construct('File not found at path: ' . $this->getPath(), $code, $previous); + } + + /** + * Get the path which was not found. + * + * @return string + */ + public function getPath() + { + return $this->path; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Exception/PathViolationException.php b/core/libraries/Hubzero/Filesystem/Exception/PathViolationException.php new file mode 100644 index 00000000000..4ed659b3618 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Exception/PathViolationException.php @@ -0,0 +1,15 @@ +has($path)) + { + $metadata = $adapter->getMetadata($path); + } + else + { + // Otherwise, we'll make our best guess at the appropriate data + $path = trim($path, '/'); + + // The minimum required data is a path and a type + $metadata = [ + 'type' => 'file', + 'path' => $path + ]; + } + + return self::getSpecialized($metadata, $adapter); + } + + /** + * Checks to see if file is an image + * + * @return bool + */ + public function isImage() + { + return strpos($this->getMimetype(), 'image/') !== false ? true : false; + } + + /** + * Checks if file is binary + * + * @return bool + */ + public function isBinary() + { + return substr($this->getMimetype(), 0, 4) == 'text' ? false : true; + } + + /** + * Checks to see if entity can be expanded + * + * @return bool + **/ + public function isExpandable() + { + $expandable = __NAMESPACE__ . '\\Type\\Expandable'; + return ($this instanceof $expandable); + } + + /** + * Grabs the item name, without extension + * + * @return string + **/ + public function getDisplayName() + { + return str_replace('.' . $this->getExtension(), '', parent::getDisplayName()); + } + + /** + * Gets the file mimetype + * + * @return string + */ + public function getMimetype() + { + if (!isset($this->mimetype)) + { + $this->mimetype = $this->hasAdapterOrFail()->adapter->getMimetype($this->getPath()); + } + + return $this->mimetype; + } + + /** + * Grabs the item size + * + * @param bool $raw Whether or not to return raw size (vs formatted size) + * @return string|int + **/ + public function getSize($raw = false) + { + if (!isset($this->size)) + { + $this->size = $this->hasAdapterOrFail()->adapter->getSize($this->getPath()); + } + + return ($raw) ? $this->size : \Hubzero\Utility\Number::formatBytes($this->size); + } + + /** + * Grabs the entity extension + * + * @return string + **/ + public function getExtension() + { + if (!isset($this->extension)) + { + $bits = explode('.', $this->getName()); + if (count($bits) > 1) + { + $this->extension = end($bits); + } + else + { + $this->extension = ''; + } + } + + return $this->extension; + } + + /** + * Grabs the name of a file with its extension + * + * @return string + **/ + public function getFilename() + { + $filename = $this->getDisplayName(); + if ($this->getExtension() != '') + { + $filename .= '.' . $this->getExtension(); + } + return $filename; + } + + /** + * Checks if current file has the given extension + * + * @param string $extension The extension to compare against + * @return bool + **/ + public function hasExtension($extension) + { + return $this->getExtension() == $extension; + } + + /** + * Reads the file + * + * @return string + **/ + public function read() + { + if (!isset($this->contents)) + { + $this->contents = $this->hasAdapterOrFail()->adapter->read($this->getPath()); + } + + return (string) $this->contents; + } + + /** + * Creates the file pointer resource to this file. + * + * @return handle + **/ + public function readStream() + { + return $this->hasAdapterOrFail()->adapter->readStream($this->getPath()); + } + + /** + * Writes contents to the file + * + * @param string $contents The contents to write to the file + * @return bool + **/ + public function write($contents) + { + return $this->hasAdapterOrFail()->adapter->write($this->getPath(), $contents); + } + + /** + * Updates the file + * + * @param string $contents The contents to write to the file + * @return bool + **/ + public function update($contents) + { + return $this->hasAdapterOrFail()->adapter->update($this->getPath(), $contents); + } + + /** + * Updates or creates the file + * + * @param string $contents The contents to write to the file + * @return bool + **/ + public function put($contents) + { + return $this->hasAdapterOrFail()->adapter->put($this->getPath(), $contents); + } + + /** + * Updates or creates the file using stream input + * + * @param resource $contents The contents to write to the file + * @return bool + **/ + public function putStream($contents) + { + return $this->hasAdapterOrFail()->adapter->putStream($this->getPath(), $contents); + } + + /** + * Saves the file contents that are already set on the object + * + * @param bool $scan Whether or not to scan for viruses + * @return bool + **/ + public function save($scan = true) + { + if (!isset($this->contents)) + { + return false; + } + + // We can save a stream or string...so see which it is + $method = (is_resource($this->contents) && get_resource_type($this->contents) == 'stream') ? 'putStream' : 'put'; + + if (!$this->$method($this->contents)) + { + return false; + } + + if ($scan && !$this->isSafe()) + { + $this->delete(); + return false; + } + + return true; + } + + /** + * Updates or creates the file + * + * @param string $contents The contents to write to the file + * @return bool + **/ + public function createOrUpdate($contents) + { + return $this->put($contents); + } + + /** + * Reads the file + * + * @return string + **/ + public function readAndDelete() + { + if (!isset($this->contents)) + { + $this->contents = $this->hasAdapterOrFail()->adapter->readAndDelete($this->getPath()); + } + else + { + // We've already read it...so just go ahead and delete rather than reading again + $this->delete(); + } + + return $this->contents; + } + + /** + * Deletes the file + * + * @return bool + **/ + public function delete() + { + return $this->hasAdapterOrFail()->adapter->delete($this->getPath()); + } + + /** + * Serves up the file to the web + * + * @param string $as What to serve the file as + * @return bool + **/ + public function serve($as = null) + { + // Initiate a new content server + $server = new \Hubzero\Content\Server(); + $server->disposition('attachment'); + $server->acceptranges(false); + + if (!$this->isLocal()) + { + // Create a temp file and write to it + $temp = tmpfile(); + fwrite($temp, $this->read()); + $server->filename(stream_get_meta_data($temp)['uri']); + } + else + { + $server->filename($this->getAbsolutePath()); + } + + $server->saveas($as ?: $this->getFileName()); + + // Serve up the file + $result = $server->serve(); + + // Clean up after serving + if (isset($temp) && is_resource($temp)) + { + fclose($temp); + } + + return $result; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Filesystem.php b/core/libraries/Hubzero/Filesystem/Filesystem.php new file mode 100644 index 00000000000..209de2f8dac --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Filesystem.php @@ -0,0 +1,567 @@ +adapter = $adapter; + } + + /** + * Get the Adapter. + * + * @return object AdapterInterface + */ + public function getAdapter() + { + return $this->adapter; + } + + /** + * Set the Adapter. + * + * @param object $adapter AdapterInterface + * @return object + */ + public function setAdapter(AdapterInterface $adapter) + { + $this->adapter = $adapter; + + return $this; + } + + /** + * Determine if a file exists. + * + * @param string $path + * @return bool + */ + public function exists($path) + { + $path = Util::normalizePath($path); + + return (bool) $this->adapter->exists($path); + } + + /** + * Get the contents of a file. + * + * @param string $path + * @return string + */ + public function read($path) + { + $path = Util::normalizePath($path); + + $this->assertPresent($path); + + return (string) $this->adapter->read($path); + } + + /** + * Write the contents of a file. + * + * @param string $path + * @param string $contents + * @return int + */ + public function write($path, $contents) + { + $path = Util::normalizePath($path); + + //$this->assertAbsent($path); + + return (bool) $this->adapter->write($path, $contents); + } + + /** + * Prepend to a file. + * + * @param string $path + * @param string $data + * @return int + */ + public function prepend($path, $data) + { + if ($this->exists($path)) + { + return $this->write($path, $data . $this->read($path)); + } + + return $this->write($path, $data); + } + + /** + * Append to a file. + * + * @param string $path + * @param string $data + * @return int + */ + public function append($path, $data) + { + if ($this->exists($path)) + { + return $this->write($path, $this->read($path) . $data); + } + + return $this->write($path, $data); + } + + /** + * Delete the file at a given path. + * + * @param mixed $path string + * @return bool + */ + public function delete($path) + { + $path = Util::normalizePath($path); + + $this->assertPresent($path); + + return $this->adapter->delete($path); + } + + /** + * Upload a file + * + * @param string $path + * @param string $target + * @return bool + */ + public function upload($path, $target) + { + $path = Util::normalizePath($path); + $target = Util::normalizePath($target); + + return $this->adapter->upload($path, $target); + } + + /** + * Move a file to a new location. + * + * @param string $path + * @param string $target + * @return bool + */ + public function move($path, $target) + { + return $this->rename($path, $target); + } + + /** + * Rename a file. + * + * @param string $path + * @param string $target + * @return bool + */ + public function rename($path, $target) + { + $path = Util::normalizePath($path); + $target = Util::normalizePath($target); + + $this->assertPresent($path); + //$this->assertAbsent($target); + + return (bool) $this->adapter->rename($path, $target); + } + + /** + * Copy a file to a new location. + * + * @param string $path + * @param string $target + * @return bool + */ + public function copy($path, $target) + { + $path = Util::normalizePath($path); + $target = Util::normalizePath($target); + + $this->assertPresent($path); + //$this->assertAbsent($target); + + return (bool) $this->adapter->copy($path, $target); + } + + /** + * Searches the directory paths for a given file. + * + * @param mixed $paths An path string or array of path strings to search in + * @param string $file The file name to look for. + * @return mixed Full path and name for the target file, or false if file not found. + */ + public function find($paths, $file) + { + if (!$file) + { + return false; + } + + return $this->adapter->find((array) $paths, $file); + } + + /** + * Extract the file name from a file path. + * + * @param string $path + * @return string + */ + public function name($path) + { + $path = Util::normalizePath($path); + + return $this->adapter->name($path); + } + + /** + * Extract the file extension from a file path. + * + * @param string $path + * @return string + */ + public function extension($path) + { + $path = Util::normalizePath($path); + + return $this->adapter->extension($path); + } + + /** + * Get the file type of a given file. + * + * @param string $path + * @return string + */ + public function type($path) + { + $path = Util::normalizePath($path); + + return $this->adapter->type($path); + } + + /** + * Get the file size of a given file. + * + * @param string $path + * @return int + */ + public function size($path) + { + $path = Util::normalizePath($path); + + return $this->adapter->size($path); + } + + /** + * Get a file's mime-type. + * + * @param string $path path to file + * @return string + * @throws FileNotFoundException + */ + public function mimetype($path) + { + $path = Util::normalizePath($path); + + return $this->adapter->mimetype($path); + } + + /** + * Get the file's last modification time. + * + * @param string $path + * @return int + */ + public function lastModified($path) + { + $path = Util::normalizePath($path); + + return (int) $this->adapter->lastModified($path); + } + + /** + * Determine if the given path is a directory. + * + * @param string $directory + * @return bool + */ + public function isDirectory($directory) + { + $directory = Util::normalizePath($directory); + + return (bool) $this->adapter->isDirectory($directory); + } + + /** + * Determine if the given path is writable. + * + * @param string $path + * @return bool + */ + public function isWritable($path) + { + $path = Util::normalizePath($path); + + return (bool) $this->adapter->isWritable($path); + } + + /** + * Determine if the given path is a file. + * + * @param string $file + * @return bool + */ + public function isFile($file) + { + $file = Util::normalizePath($file); + + return (bool) $this->adapter->isFile($file); + } + + /** + * Run a virus scan against a file + * + * @param string $file The name of the file [not full path] + * @return boolean + */ + public function isSafe($file) + { + $file = Util::normalizePath($file); + + return (bool) $this->adapter->isSafe($file); + } + + /** + * Get all contents within a given directory. + * + * @param string $path The path of the folder to read. + * @param string $filter A filter for file names. + * @param mixed $recurse True to recursively search into sub-folders, or an integer to specify the maximum depth. + * @param boolean $full True to return the full path to the file. + * @param array $exclude Array with names of files which should not be shown in the result. + * @return array + */ + public function listContents($path, $filter = '.', $recursive = false, $full = false, $exclude = array('.svn', '.git', 'CVS', '.DS_Store', '__MACOSX')) + { + $path = Util::normalizePath($path); + + return (array) $this->adapter->listContents($path, $filter, $recursive, $full, $exclude); + } + + /** + * Create a directory. + * + * @param string $path + * @param int $mode + * @param bool $recursive + * @param bool $force + * @return bool + */ + public function makeDirectory($path, $mode = 0755, $recursive = true, $force = false) + { + $path = Util::normalizePath($path); + + return (bool) $this->adapter->makeDirectory($path, $mode, $recursive, $force); + } + + /** + * Copy a directory from one location to another. + * + * @param string $path + * @param string $target + * @param int $options + * @return bool + */ + public function copyDirectory($path, $target, $options = null) + { + $path = Util::normalizePath($path); + $target = Util::normalizePath($target); + + $this->assertPresent($path); + //$this->assertAbsent($target); + + return (bool) $this->adapter->copyDirectory($path, $target, $options); + } + + /** + * Recursively delete a directory. + * + * The directory itself may be optionally preserved. + * + * @param string $path + * @param bool $preserve + * @return bool + */ + public function deleteDirectory($path, $preserve = false) + { + $path = Util::normalizePath($path); + + $this->assertPresent($path); + + return (bool) $this->adapter->deleteDirectory($path, $preserve); + } + + /** + * Chmods files and directories recursively to given permissions. + * + * @param string $path Root path to begin changing mode [without trailing slash]. + * @param string $filemode Octal representation of the value to change file mode to [null = no change]. + * @param string $foldermode Octal representation of the value to change folder mode to [null = no change]. + * @return boolean True if successful [one fail means the whole operation failed]. + */ + public function setPermissions($path, $filemode = '0644', $foldermode = '0755') + { + $path = Util::normalizePath($path); + + return (bool) $this->adapter->setPermissions($path, $filemode, $foldermode); + } + + /** + * Makes file name safe to use + * + * @param string $file The name of the file [not full path] + * @return string The sanitised string + */ + public function clean($file) + { + return Util::normalizeFile($file); + } + + /** + * Makes path safe to use + * + * @param string $path The full path to sanitise. + * @return string The sanitised string + */ + public function cleanPath($path) + { + return Util::normalizePath($path); + } + + /** + * Makes directory name safe to use. + * + * @param string $directory The directory to sanitise. + * @return string The sanitised string. + */ + public function cleanDirectory($directory) + { + return Util::normalizeDirectory($directory); + } + + /** + * Assert a file is present. + * + * @param string $path Path to file + * @throws FileNotFoundException + */ + public function assertPresent($path) + { + if (!$this->exists($path)) + { + throw new FileNotFoundException($path); + } + } + + /** + * Assert a file is absent. + * + * @param string $path Path to file + * @throws FileExistsException + */ + public function assertAbsent($path) + { + if ($this->exists($path)) + { + throw new FileExistsException($path); + } + } + + /** + * Register a macro. + * + * @param object $plugin MacroInterface + * @return $this + */ + public function addMacro(MacroInterface $macro) + { + if (!method_exists($macro, 'handle')) + { + throw new \LogicException(sprintf('%s does not have a handle method.', get_class($macro))); + } + + $this->macros[$macro->getMethod()] = $macro; + + return $this; + } + + /** + * Checks if macro is registered. + * + * @param string $name + * @return bool + */ + public function hasMacro($method) + { + return isset($this->macros[$method]); + } + + /** + * Call a macro. + * + * @param string $method + * @param array $arguments + * @return mixed + * @throws BadMethodCallException + */ + public function __call($method, array $arguments) + { + if ($this->hasMacro($method)) + { + $macro = $this->macros[$method]; + $macro->setFilesystem($this); + + return call_user_func_array(array($macro, 'handle'), $arguments); + } + + throw new \BadMethodCallException('Call to undefined method ' . __CLASS__ . '::' . $method); + } +} diff --git a/core/libraries/Hubzero/Filesystem/Flysystem.php b/core/libraries/Hubzero/Filesystem/Flysystem.php new file mode 100644 index 00000000000..fe59eb909c8 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Flysystem.php @@ -0,0 +1,46 @@ +encapsulate($contents); + } + + /** + * Encapsulates the entities list in their appropriate classes and returns as part of a collection + * + * @param array $entities The filesystem contents + * @return \Hubzero\Filesystem\Collection + */ + private function encapsulate($entities) + { + $items = []; + + foreach ($entities as $entity) + { + $items[] = Entity::fromMetadata($entity, $this); + } + + return new Collection($items); + } +} diff --git a/core/libraries/Hubzero/Filesystem/Macro/Base.php b/core/libraries/Hubzero/Filesystem/Macro/Base.php new file mode 100644 index 00000000000..a8446e0f0c0 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Macro/Base.php @@ -0,0 +1,35 @@ +filesystem = $filesystem; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Macro/Directories.php b/core/libraries/Hubzero/Filesystem/Macro/Directories.php new file mode 100644 index 00000000000..8a235558970 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Macro/Directories.php @@ -0,0 +1,51 @@ +filesystem->listContents($path, $filter, $recursive, $full, $exclude); + + foreach ($contents as $object) + { + if ($object['type'] === 'dir') + { + $result[] = $object['path']; + } + } + + return $result; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Macro/DirectoryTree.php b/core/libraries/Hubzero/Filesystem/Macro/DirectoryTree.php new file mode 100644 index 00000000000..1dc15be461d --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Macro/DirectoryTree.php @@ -0,0 +1,83 @@ +index = 0; + } + + if ($level < $maxLevel) + { + $folders = $this->filesystem->listContents($path, $filter); + + // First path, index foldernames + foreach ($folders as $name) + { + if ($name['type'] != 'dir') + { + continue; + } + + $this->index++; + + $fullName = $this->filesystem->cleanPath($path . DS . $name['path']); + + $dirs[] = array( + 'id' => $this->index, + 'parent' => $parent, + 'name' => ltrim($name['path'], '\\/'), + 'fullname' => $fullName, + 'relname' => str_replace(PATH_ROOT, '', $fullName) + ); + + $dirs2 = $this->handle($fullName, $filter, $maxLevel, $level + 1, $this->index); + + $dirs = array_merge($dirs, $dirs2); + } + } + + return $dirs; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Macro/EmptyDirectory.php b/core/libraries/Hubzero/Filesystem/Macro/EmptyDirectory.php new file mode 100644 index 00000000000..cc440237741 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Macro/EmptyDirectory.php @@ -0,0 +1,47 @@ +filesystem->listContents($dirname, false); + + foreach ($listing as $item) + { + if ($item['type'] === 'dir') + { + $this->filesystem->deleteDirectory($dirname . $item['path']); + } + else + { + $this->filesystem->delete($dirname . $item['path']); + } + } + } +} diff --git a/core/libraries/Hubzero/Filesystem/Macro/Files.php b/core/libraries/Hubzero/Filesystem/Macro/Files.php new file mode 100644 index 00000000000..5ad7316dbd1 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Macro/Files.php @@ -0,0 +1,51 @@ +filesystem->listContents($path, $filter, $recursive, $full, $exclude); + + foreach ($contents as $object) + { + if ($object['type'] === 'file') + { + $result[] = $object['path']; + } + } + + return $result; + } +} diff --git a/core/libraries/Hubzero/Filesystem/MacroInterface.php b/core/libraries/Hubzero/Filesystem/MacroInterface.php new file mode 100644 index 00000000000..673ce7db20a --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/MacroInterface.php @@ -0,0 +1,28 @@ +getAdapter(); + $location = $locationName . '://' . $location->getPath(); + } + + return [$location, $locationName, $locationMount]; + } + + /** + * Finds a mount by name, irrelevant of params + * + * @param string $name The mount name to look for + * @return static|bool + **/ + protected static function findMountByName($name) + { + if (array_key_exists($name, self::$adapters)) + { + return self::$adapters[$name]; + } + + foreach (self::$adapters as $key => $adapter) + { + if (strpos($key, $name) === 0) + { + return self::$adapters[$key]; + } + } + + return false; + } + + /** + * Grabs the mount name from a given path + * + * @param string $path The path to parse for mount names + * @return void + **/ + protected static function getMountNameFromPath($path) + { + preg_match('/([[:alpha:]]*):\/\//', $path, $name); + + if (!isset($name[1]) || !$name[1]) + { + throw new \Exception('Could not determine source mount type', 500); + } + + return $name[1]; + } + + /** + * Grabs the relative path from the given path, removing any mount prefix + * + * @param string $path The path to un-prefix + * @return string + **/ + protected static function getRelativePath($path) + { + preg_match('/([[:alpha:]]*):\/\/(.*)/', $path, $name); + + if (isset($name[2]) && $name[2]) + { + return $name[2]; + } + + return $path; + } + + /** + * Returns the appropriate adapter + * + * @param string $name The adapter name to instantiate + * @param array $params Any initialization parameters + * @param string $key A custom key under which to store the adapter + * @return object + **/ + public static function adapter($name, $params = [], $key = null) + { + $key = $key ?: $name . '.' . md5(serialize($params)); + + if (!isset(self::$adapters[$key])) + { + // Import filesystem plugins + Plugin::import('filesystem'); + + // Get the adapter + $plugin = 'plgFilesystem' . ucfirst($name); + $adapter = $plugin::init($params); + + self::$adapters[$key] = new Flysystem($adapter); + } + + // Return the filesystem connection + return self::$adapters[$key]; + } + + /** + * Copys file from one location to another, between mounts + * + * @param mixed $source The source path or object + * @param mixed $dest The destination path or object + * @param bool $overwrite Whether or not to overwrite any existing files by the same name + * @return bool + **/ + public static function copy($source, $dest, $overwrite = false) + { + list($source, $sourceName, $sourceMount) = self::parseLocation($source); + list($dest, $destName, $destMount) = self::parseLocation($dest); + + // Make sure we got the mounts we need + if (!$sourceMount) + { + throw new \Exception("'{$sourceMount}' has not been mounted", 500); + } + + if (!$destMount) + { + throw new \Exception("'{$destMount}' has not been mounted", 500); + } + + // Check to see if destination already exists + if ($destMount->has(self::getRelativePath($dest))) + { + if ($overwrite) + { + $destMount->delete(self::getRelativePath($dest)); + } + else + { + return true; + } + } + + // Create mount manager + $manager = new \League\Flysystem\MountManager([ + $sourceName => $sourceMount, + $destName => $destMount + ]); + + // Do copy + return $manager->copy($source, $dest); + } + + /** + * Creates a filesystem handle to the PHP temp directory + * + * @param string $path The relative path within the temp dir to use + * @return object + **/ + public static function getTempPath($path = '') + { + return Entity::fromPath($path, self::adapter('local', ['path' => sys_get_temp_dir()], 'temp')); + } +} diff --git a/core/libraries/Hubzero/Filesystem/Type/Expandable.php b/core/libraries/Hubzero/Filesystem/Type/Expandable.php new file mode 100644 index 00000000000..c07453bfb56 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Type/Expandable.php @@ -0,0 +1,55 @@ +cleanup(); + } + + return true; + } + + /** + * Cleans the archive of OS-specific files + * + * @return bool + **/ + protected function cleanup() + { + $items = $this->getParent(false)->listContents(); + + foreach ($items as $item) + { + if (in_array($item->getName(), ['.svn', 'CVS', '.DS_Store', '__MACOSX'])) + { + if (!$item->delete()) + { + return false; + } + } + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Type/Gz.php b/core/libraries/Hubzero/Filesystem/Type/Gz.php new file mode 100644 index 00000000000..c622d32fdba --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Type/Gz.php @@ -0,0 +1,15 @@ +getName()); + $this->copy($temp); + + $archive = new \PharData($temp->getAbsolutePath()); + + foreach ($archive as $file) + { + // Add 7 to the length for the 'phar://' prefix to the file + $path = substr($file, strlen($temp->getAbsolutePath()) + 7); + $entity = Entity::fromPath($this->getParent() . $path, $this->getAdapter()); + + if ($entity->isFile()) + { + // Open + $item = fopen($file, 'r'); + + // Write stream + $entity->putStream($item); + + // Close + fclose($item); + } + else + { + // Create the directory + $entity->create(); + } + } + + // Clean up + $temp->delete(); + + return parent::expand($cleanup); + } +} diff --git a/core/libraries/Hubzero/Filesystem/Type/Zip.php b/core/libraries/Hubzero/Filesystem/Type/Zip.php new file mode 100644 index 00000000000..814df8bf44e --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Type/Zip.php @@ -0,0 +1,68 @@ +getName()); + $this->copy($temp); + + $zip = new \ZipArchive; + + // Open the temp archive (we use the absolute path because we're on the local filesystem) + if ($zip->open($temp->getAbsolutePath()) === true) + { + // We don't actually have to extract the archive, we can just read out of it and copy over to the original location + for ($i = 0; $i < $zip->numFiles; $i++) + { + $filename = $zip->getNameIndex($i); + $entity = Entity::fromPath($this->getParent() . '/' . $filename, $this->getAdapter()); + + if ($entity->isFile()) + { + // Open + $item = fopen('zip://' . $temp->getAbsolutePath() . '#' . $filename, 'r'); + + // Write stream + $entity->putStream($item); + + // Close + fclose($item); + } + else + { + // Create the directory + $entity->create(); + } + } + + // Clean up + $zip->close(); + $temp->delete(); + + return parent::expand($cleanup); + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Filesystem/Util.php b/core/libraries/Hubzero/Filesystem/Util.php new file mode 100644 index 00000000000..9726d5c65a0 --- /dev/null +++ b/core/libraries/Hubzero/Filesystem/Util.php @@ -0,0 +1,197 @@ +buffer($content); + + return $mimeType ?: null; + } + + /** + * Detects MIME Type based on file extension. + * + * @param string $extension + * @return mixed string|null MIME Type or NULL if no extension detected + */ + public static function detectByFileExtension($extension) + { + static $extensionToMimeTypeMap; + + if (!$extensionToMimeTypeMap) + { + $extensionToMimeTypeMap = static::getExtensionToMimeTypeMap(); + } + + if (isset($extensionToMimeTypeMap[$extension])) + { + return $extensionToMimeTypeMap[$extension]; + } + } + + /** + * Map of file extension to MIME Type + * + * @return array + */ + public static function getExtensionToMimeTypeMap() + { + return array( + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'csv' => 'text/x-comma-separated-values', + 'bin' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'exe' => 'application/octet-stream', + 'class' => 'application/octet-stream', + 'psd' => 'application/x-photoshop', + 'so' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'ai' => 'application/pdf', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'wbxml' => 'application/wbxml', + 'wmlc' => 'application/wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'gzip' => 'application/x-gzip', + 'php' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'php3' => 'application/x-httpd-php', + 'phtml' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'js' => 'application/javascript', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-tar', + 'z' => 'application/x-compress', + 'xhtml' => 'application/xhtml+xml', + 'xht' => 'application/xhtml+xml', + 'zip' => 'application/x-zip', + 'rar' => 'application/x-rar', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mpga' => 'audio/mpeg', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'aif' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'rv' => 'video/vnd.rn-realvideo', + 'wav' => 'audio/x-wav', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'shtml' => 'text/html', + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'log' => 'text/plain', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'xml' => 'application/xml', + 'xsl' => 'application/xml', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'word' => 'application/msword', + 'xl' => 'application/excel', + 'eml' => 'message/rfc822', + 'json' => 'application/json', + 'pem' => 'application/x-x509-user-cert', + 'p10' => 'application/x-pkcs10', + 'p12' => 'application/x-pkcs12', + 'p7a' => 'application/x-pkcs7-signature', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'crt' => 'application/x-x509-ca-cert', + 'crl' => 'application/pkix-crl', + 'der' => 'application/x-x509-ca-cert', + 'kdb' => 'application/octet-stream', + 'pgp' => 'application/pgp', + 'gpg' => 'application/gpg-keys', + 'sst' => 'application/octet-stream', + 'csr' => 'application/octet-stream', + 'rsa' => 'application/x-pkcs7', + 'cer' => 'application/pkix-cert', + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gp', + 'mp4' => 'video/mp4', + 'm4a' => 'audio/x-m4a', + 'f4v' => 'video/mp4', + 'webm' => 'video/webm', + 'aac' => 'audio/x-acc', + 'm4u' => 'application/vnd.mpegurl', + 'm3u' => 'text/plain', + 'xspf' => 'application/xspf+xml', + 'vlc' => 'application/videolan', + 'wmv' => 'video/x-ms-wmv', + 'au' => 'audio/x-au', + 'ac3' => 'audio/ac3', + 'flac' => 'audio/x-flac', + 'ogg' => 'audio/ogg', + 'kmz' => 'application/vnd.google-earth.kmz', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'ics' => 'text/calendar', + 'zsh' => 'text/x-scriptzsh', + '7zip' => 'application/x-7z-compressed', + 'cdr' => 'application/cdr', + 'wma' => 'audio/x-ms-wma', + 'jar' => 'application/java-archive', + ); + } +} diff --git a/core/libraries/Hubzero/Form/Exception/InvalidData.php b/core/libraries/Hubzero/Form/Exception/InvalidData.php new file mode 100644 index 00000000000..f7bd414f461 --- /dev/null +++ b/core/libraries/Hubzero/Form/Exception/InvalidData.php @@ -0,0 +1,31 @@ +getMessage(); + } + + /** + * Returns to error message + * + * @return string Error message + */ + public function toString() + { + return $this->__toString(); + } +} diff --git a/core/libraries/Hubzero/Form/Exception/MissingData.php b/core/libraries/Hubzero/Form/Exception/MissingData.php new file mode 100644 index 00000000000..127cfe5aff5 --- /dev/null +++ b/core/libraries/Hubzero/Form/Exception/MissingData.php @@ -0,0 +1,31 @@ +getMessage(); + } + + /** + * Returns to error message + * + * @return string Error message + */ + public function toString() + { + return $this->__toString(); + } +} diff --git a/core/libraries/Hubzero/Form/Field.php b/core/libraries/Hubzero/Form/Field.php new file mode 100644 index 00000000000..0b4e708bff2 --- /dev/null +++ b/core/libraries/Hubzero/Form/Field.php @@ -0,0 +1,619 @@ + XML element that describes the form field. + * + * @var object + */ + protected $element; + + /** + * The Form object of the form attached to the form field. + * + * @var object + */ + protected $form; + + /** + * The form control prefix for field names from the Form object attached to the form field. + * + * @var string + */ + protected $formControl; + + /** + * The hidden state for the form field. + * + * @var boolean + */ + protected $hidden = false; + + /** + * True to translate the field label string. + * + * @var boolean + */ + protected $translateLabel = true; + + /** + * True to translate the field description string. + * + * @var boolean + */ + protected $translateDescription = true; + + /** + * The document id for the form field. + * + * @var string + */ + protected $id; + + /** + * The input for the form field. + * + * @var string + */ + protected $input; + + /** + * The label for the form field. + * + * @var string + */ + protected $label; + + /** + * The multiple state for the form field. If true then multiple values are allowed for the + * field. Most often used for list field types. + * + * @var boolean + */ + protected $multiple = false; + + /** + * The name of the form field. + * + * @var string + */ + protected $name; + + /** + * The name of the field. + * + * @var string + */ + protected $fieldname; + + /** + * The group of the field. + * + * @var string + */ + protected $group; + + /** + * The required state for the form field. If true then there must be a value for the field to + * be considered valid. + * + * @var boolean + */ + protected $required = false; + + /** + * The form field type. + * + * @var string + */ + protected $type; + + /** + * The validation method for the form field. This value will determine which method is used + * to validate the value for a field. + * + * @var string + */ + protected $validate; + + /** + * The value of the form field. + * + * @var mixed + */ + protected $value; + + /** + * The label's CSS class of the form field + * + * @var mixed + */ + protected $labelClass; + + /** + * The count value for generated name field + * + * @var integer + */ + protected static $count = 0; + + /** + * The string used for generated fields names + * + * @var integer + */ + protected static $generated_fieldname = '__field'; + + /** + * Method to instantiate the form field object. + * + * @param object $form The form to attach to the form field object. + * @return void + */ + public function __construct($form = null) + { + // If there is a form passed into the constructor set the form and form control properties. + if ($form instanceof Form) + { + $this->form = $form; + $this->formControl = $form->getFormControl(); + } + + // Detect the field type if not set + if (!isset($this->type)) + { + // Get the reflection info + $r = new ReflectionClass($this); + + // Is it namespaced? + if ($r->inNamespace()) + { + // It is! This makes things easy. + $this->type = $r->getShortName(); + } + else + { + // We'll assume a CamelCased name + // Split by words and take the last one + $parts = Str::splitCamel(get_class($this)); + + if ($parts[0] == 'J') + { + $this->type = ucfirst($parts[count($parts) - 1], '_'); + } + else + { + $this->type = ucfirst($parts[0], '_') . ucfirst($parts[count($parts) - 1], '_'); + } + } + } + } + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to the the value. + * @return mixed The property value or null. + */ + public function __get($name) + { + switch ($name) + { + case 'class': + case 'description': + case 'formControl': + case 'hidden': + case 'id': + case 'multiple': + case 'name': + case 'required': + case 'type': + case 'validate': + case 'value': + case 'labelClass': + case 'fieldname': + case 'group': + return $this->$name; + break; + + case 'input': + // If the input hasn't yet been generated, generate it. + if (empty($this->input)) + { + $this->input = $this->getInput(); + } + + return $this->input; + break; + + case 'label': + // If the label hasn't yet been generated, generate it. + if (empty($this->label)) + { + $this->label = $this->getLabel(); + } + + return $this->label; + break; + case 'title': + return $this->getTitle(); + break; + } + + return null; + } + + /** + * Method to attach a Form object to the field. + * + * @param object $form The Form object to attach to the form field. + * @return object The form field object so that the method can be used in a chain. + */ + public function setForm(Form $form) + { + $this->form = $form; + $this->formControl = $form->getFormControl(); + + return $this; + } + + /** + * Method to attach a JForm object to the field. + * + * @param object &$element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @return boolean True on success. + */ + public function setup(&$element, $value, $group = null) + { + // Make sure there is a valid JFormField XML element. + if (!($element instanceof SimpleXMLElement) || (string) $element->getName() != 'field') + { + return false; + } + + // Reset the input and label values. + $this->input = null; + $this->label = null; + + // Set the XML element object. + $this->element = $element; + // Get some important attributes from the form field element. + $class = (string) $element['class']; + $id = (string) $element['id']; + $multiple = (string) $element['multiple']; + $name = (string) $element['name']; + $required = (string) $element['required']; + + // Set the required and validation options. + $this->required = ($required == 'true' || $required == 'required' || $required == '1'); + $this->validate = (string) $element['validate']; + + // Add the required class if the field is required. + if ($this->required) + { + if ($class) + { + if (strpos($class, 'required') === false) + { + $this->element['class'] = $class . ' required'; + } + } + else + { + $this->element->addAttribute('class', 'required'); + } + } + + // Set the multiple values option. + $this->multiple = ($multiple == 'true' || $multiple == 'multiple'); + + // Allow for field classes to force the multiple values option. + if (isset($this->forceMultiple)) + { + $this->multiple = (bool) $this->forceMultiple; + } + + // Set the field description text. + $this->description = (string) $element['description']; + + // Set the visibility. + $this->hidden = ((string) $element['type'] == 'hidden' || (string) $element['hidden'] == 'true'); + + // Determine whether to translate the field label and/or description. + $this->translateLabel = !((string) $this->element['translate_label'] == 'false' || (string) $this->element['translate_label'] == '0'); + $this->translateDescription = !((string) $this->element['translate_description'] == 'false' + || (string) $this->element['translate_description'] == '0'); + + // Set the group of the field. + $this->group = $group; + + // Set the field name and id. + $this->fieldname = $this->getFieldName($name); + $this->name = $this->getName($this->fieldname); + $this->id = $this->getId($id, $this->fieldname); + + // Set the field default value. + $this->value = $value; + + // Set the field placeholder + $this->placeholder = (string) $element['placeholder']; + + // Set the CSS class of field label + $this->labelClass = (string) $element['labelclass']; + + return true; + } + + /** + * Method to get the id used for the field input tag. + * + * @param string $fieldId The field element id. + * @param string $fieldName The field element name. + * @return string The id to be used for the field input tag. + */ + protected function getId($fieldId, $fieldName) + { + // Initialise variables. + $id = ''; + + // If there is a form control set for the attached form add it first. + if ($this->formControl) + { + $id .= $this->formControl; + } + + // If the field is in a group add the group control to the field id. + if ($this->group) + { + // If we already have an id segment add the group control as another level. + if ($id) + { + $id .= '_' . str_replace('.', '_', $this->group); + } + else + { + $id .= str_replace('.', '_', $this->group); + } + } + + // If we already have an id segment add the field id/name as another level. + if ($id) + { + $id .= '_' . ($fieldId ? $fieldId : $fieldName); + } + else + { + $id .= ($fieldId ? $fieldId : $fieldName); + } + + // Clean up any invalid characters. + $id = preg_replace('#\W#', '_', $id); + + return $id; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + */ + abstract protected function getInput(); + + /** + * Method to get the field title. + * + * @return string The field title. + */ + protected function getTitle() + { + // Initialise variables. + $title = ''; + + if ($this->hidden) + { + + return $title; + } + + // Get the label text from the XML element, defaulting to the element name. + $title = $this->element['label'] ? (string) $this->element['label'] : (string) $this->element['name']; + $title = $this->translateLabel ? Lang::txt($title) : $title; + + return $title; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + */ + protected function getLabel() + { + // Initialise variables. + $label = ''; + + if ($this->hidden) + { + return $label; + } + + // Get the label text from the XML element, defaulting to the element name. + $text = $this->element['label'] ? (string) $this->element['label'] : (string) $this->element['name']; + $text = $this->translateLabel ? Lang::txt($text) : $text; + + // Build the class for the label. + $class = !empty($this->description) ? 'hasTip' : ''; + $class = $this->required == true ? $class . ' required-field' : $class; + $class = !empty($this->labelClass) ? $class . ' ' . $this->labelClass : $class; + + // Add the opening label tag and main attributes attributes. + $label .= ''; + } + else + { + $label .= '>' . $text . ''; + } + + return $label; + } + + /** + * Method to get the name used for the field input tag. + * + * @param string $fieldName The field element name. + * @return string The name to be used for the field input tag. + */ + protected function getName($fieldName) + { + // Initialise variables. + $name = ''; + + // If there is a form control set for the attached form add it first. + if ($this->formControl) + { + $name .= $this->formControl; + } + + // If the field is in a group add the group control to the field name. + if ($this->group) + { + // If we already have a name segment add the group control as another level. + $groups = explode('.', $this->group); + if ($name) + { + foreach ($groups as $group) + { + $name .= '[' . $group . ']'; + } + } + else + { + $name .= array_shift($groups); + foreach ($groups as $group) + { + $name .= '[' . $group . ']'; + } + } + } + + // If we already have a name segment add the field name as another level. + if ($name) + { + $name .= '[' . $fieldName . ']'; + } + else + { + $name .= $fieldName; + } + + // If the field should support multiple values add the final array segment. + if ($this->multiple) + { + $name .= '[]'; + } + + return $name; + } + + /** + * Method to get the field name used. + * + * @param string $fieldName The field element name. + * @return string The field name + */ + protected function getFieldName($fieldName) + { + if ($fieldName) + { + return $fieldName; + } + + self::$count = self::$count + 1; + return self::$generated_fieldname . self::$count; + } + + /** + * Method to get an attribute of the field + * + * @param string $name Name of the attribute to get + * @param mixed $default Optional value to return if attribute not found + * @return mixed Value of the attribute / default + */ + public function getAttribute($name, $default = null) + { + if ($this->element instanceof SimpleXMLElement) + { + $attributes = $this->element->attributes(); + + // Ensure that the attribute exists + if (property_exists($attributes, $name)) + { + $value = $attributes->$name; + + if ($value !== null) + { + return (string) $value; + } + } + } + + return $default; + } + + /** + * Simple method to set the value + * + * @param mixed $value Value to set + * @return void + */ + public function setValue($value) + { + $this->value = $value; + } +} diff --git a/core/libraries/Hubzero/Form/Fields/Accesslevel.php b/core/libraries/Hubzero/Form/Fields/Accesslevel.php new file mode 100644 index 00000000000..92cb015cee5 --- /dev/null +++ b/core/libraries/Hubzero/Form/Fields/Accesslevel.php @@ -0,0 +1,49 @@ +element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $attr .= ((string) $this->element['disabled'] == 'true') ? ' disabled="disabled"' : ''; + $attr .= $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; + $attr .= $this->multiple ? ' multiple="multiple"' : ''; + + // Initialize JavaScript field attributes. + $attr .= $this->element['onchange'] ? ' onchange="' . (string) $this->element['onchange'] . '"' : ''; + + // Get the field options. + $options = $this->getOptions(); + + return Access::level($this->name, $this->value, $attr, $options, $this->id); + } +} diff --git a/core/libraries/Hubzero/Form/Fields/Cachehandler.php b/core/libraries/Hubzero/Form/Fields/Cachehandler.php new file mode 100644 index 00000000000..1cbadb9fcda --- /dev/null +++ b/core/libraries/Hubzero/Form/Fields/Cachehandler.php @@ -0,0 +1,46 @@ + name array. + foreach (Manager::getStores() as $store) + { + $options[] = Dropdown::option($store, App::get('language')->txt('JLIB_FORM_VALUE_CACHE_' . $store), 'value', 'text'); + } + + $options = array_merge(parent::getOptions(), $options); + + return $options; + } +} diff --git a/core/libraries/Hubzero/Form/Fields/Calendar.php b/core/libraries/Hubzero/Form/Fields/Calendar.php new file mode 100644 index 00000000000..d07171e7a40 --- /dev/null +++ b/core/libraries/Hubzero/Form/Fields/Calendar.php @@ -0,0 +1,112 @@ +element['format'] ? (string) $this->element['format'] : 'yy-mm-dd'; + + // Build the attributes array. + $attributes = array(); + if ($this->element['size']) + { + $attributes['size'] = (int) $this->element['size']; + } + if ($this->element['maxlength']) + { + $attributes['maxlength'] = (int) $this->element['maxlength']; + } + if ($this->element['class']) + { + $attributes['class'] = (string) $this->element['class']; + } + if ((string) $this->element['readonly'] == 'true') + { + $attributes['readonly'] = 'readonly'; + } + if ((string) $this->element['disabled'] == 'true') + { + $attributes['disabled'] = 'disabled'; + } + $attributes['time'] = false; + if ((string) $this->element['time'] == 'true') + { + $attributes['time'] = true; + } + if ($this->element['onchange']) + { + $attributes['onchange'] = (string) $this->element['onchange']; + } + + // Handle the special case for "now". + if (strtoupper($this->value) == 'NOW') + { + $this->value = strftime($format); + } + + // If a known filter is given use it. + switch (strtoupper((string) $this->element['filter'])) + { + case 'SERVER_UTC': + // Convert a date to UTC based on the server timezone. + if (intval($this->value)) + { + // Get a date object based on the correct timezone. + $date = new Date($this->value, 'UTC'); + $date->setTimezone(new DateTimeZone(App::get('config')->get('offset'))); + + // Transform the date string. + $this->value = $date->format('Y-m-d H:i:s', true, false); + } + break; + + case 'USER_UTC': + // Convert a date to UTC based on the user timezone. + if (intval($this->value)) + { + // Get a date object based on the correct timezone. + $date = new Date($this->value, 'UTC'); + $date->setTimezone(new DateTimeZone(App::get('user')->getParam('timezone', App::get('config')->get('offset')))); + + // Transform the date string. + $this->value = $date->format('Y-m-d H:i:s', true, false); + } + break; + } + + $attributes['id'] = $this->id; + $attributes['format'] = $format; + + return Input::calendar($this->name, $this->value, $attributes); + } +} diff --git a/core/libraries/Hubzero/Form/Fields/Category.php b/core/libraries/Hubzero/Form/Fields/Category.php new file mode 100644 index 00000000000..a41e6daf426 --- /dev/null +++ b/core/libraries/Hubzero/Form/Fields/Category.php @@ -0,0 +1,117 @@ +element['extension'] ? (string) $this->element['extension'] : (string) $this->element['scope']; + $published = (string) $this->element['published']; + $name = (string) $this->element['name']; + + // Load the category options for a given extension. + if (!empty($extension)) + { + // Filter over published state or not depending upon if it is present. + if ($published) + { + $options = Cat::options($extension, array('filter.published' => explode(',', $published))); + } + else + { + $options = Cat::options($extension); + } + + // Verify permissions. If the action attribute is set, then we scan the options. + if ((string) $this->element['action']) + { + + // Get the current user object. + $user = App::get('user')->getInstance(); + + // For new items we want a list of categories you are allowed to create in. + if (!$this->form->getValue($name)) + { + foreach ($options as $i => $option) + { + // To take save or create in a category you need to have create rights for that category + // unless the item is already in that category. + // Unset the option if the user isn't authorised for it. In this field assets are always categories. + if ($user->authorise('core.create', $extension . '.category.' . $option->value) != true) + { + unset($options[$i]); + } + } + } + // If you have an existing category id things are more complex. + else + { + $categoryOld = $this->form->getValue($name); + + foreach ($options as $i => $option) + { + // If you are only allowed to edit in this category but not edit.state, you should not get any + // option to change the category. + if ($user->authorise('core.edit.state', $extension . '.category.' . $categoryOld) != true) + { + if ($option->value != $categoryOld) + { + unset($options[$i]); + } + } + // However, if you can edit.state you can also move this to another category for which you have + // create permission and you should also still be able to save in the current category. + elseif (($user->authorise('core.create', $extension . '.category.' . $option->value) != true) && $option->value != $categoryOld) + { + unset($options[$i]); + } + } + } + } + + if (isset($this->element['show_root'])) + { + array_unshift($options, Dropdown::option('0', App::get('language')->txt('JGLOBAL_ROOT'))); + } + } + else + { + App::abort(500, App::get('language')->txt('JLIB_FORM_ERROR_FIELDS_CATEGORY_ERROR_EXTENSION_EMPTY')); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } +} diff --git a/core/libraries/Hubzero/Form/Fields/Checkbox.php b/core/libraries/Hubzero/Form/Fields/Checkbox.php new file mode 100644 index 00000000000..c84992368a0 --- /dev/null +++ b/core/libraries/Hubzero/Form/Fields/Checkbox.php @@ -0,0 +1,43 @@ +element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $disabled = ((string) $this->element['disabled'] == 'true') ? ' disabled="disabled"' : ''; + $checked = ((string) $this->element['value'] == $this->value) ? ' checked="checked"' : ''; + + // Initialize JavaScript field attributes. + $onclick = $this->element['onclick'] ? ' onclick="' . (string) $this->element['onclick'] . '"' : ''; + + return ''; + } +} diff --git a/core/libraries/Hubzero/Form/Fields/Checkboxes.php b/core/libraries/Hubzero/Form/Fields/Checkboxes.php new file mode 100644 index 00000000000..47961af6d3e --- /dev/null +++ b/core/libraries/Hubzero/Form/Fields/Checkboxes.php @@ -0,0 +1,174 @@ +element['class'] ? ' class="radio ' . (string) $this->element['class'] . '"' : ' class="radio"'; + + // Start the checkbox field output. + $html[] = '
'; + + // Get the field options. + $options = $this->getOptions(); + + $found = false; + $values = (array)$this->value; + + // Build the checkbox field output. + $html[] = '
    '; + foreach ($options as $i => $option) + { + // Initialize some option attributes. + $checked = (in_array((string) $option->value, $values) ? ' checked="checked"' : ''); + $class = !empty($option->class) ? ' class="' . $option->class . '"' : ''; + $disabled = !empty($option->disable) ? ' disabled="disabled"' : ''; + + // Add data attributes + $dataAttributes = ''; + foreach ($option as $field => $value) + { + $dataField = strtolower(substr($field, 0, 4)); + if ($dataField == 'data') + { + $dataAttributes .= ' ' . $field . '="' . $value . '"'; + } + } + + if ($checked) + { + foreach ($values as $k => $v) + { + if ($v == $option->value) + { + unset($values[$k]); + } + } + $found = true; + } + + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? ' onclick="' . $option->onclick . '"' : ''; + + $html[] = '
  • '; + $html[] = ''; + + $html[] = ''; + $html[] = '
  • '; + } + + if ($this->element['option_other']) + { + $values = implode('', $values); + $values = trim($values); + + $checked = ''; + if (!empty($values)) + { + $checked = ' checked="checked"'; + } + $html[] = '
  • '; + $html[] = ''; + $html[] = ''; + $html[] = ''; + $html[] = '
  • '; + } + + $html[] = '
'; + + // End the checkbox field output. + $html[] = '
'; + + return implode($html); + } + + /** + * Method to get the field options. + * + * @return array The field option objects. + */ + protected function getOptions() + { + // Initialize variables. + $options = array(); + + foreach ($this->element->children() as $option) + { + // Only add
'; + } + self::$open = false; + $content .= ''; + return $content; + } + + /** + * Begins the display of a new panel. + * + * @param string $text Text to display. + * @param string $id Identifier of the panel. + * @return string HTML to start a panel + */ + public static function panel($text, $id) + { + $content = ''; + if (self::$open) + { + $content .= ''; + } + else + { + self::$open = true; + } + $content .= '

' . $text . '

'; + + return $content; + } + + /** + * Load the JavaScript behavior. + * + * @param string $group The pane identifier. + * @param array $params Array of options. + * @return void + */ + protected static function behavior($group, $params = array()) + { + static $loaded = array(); + + if (!array_key_exists($group, $loaded)) + { + $loaded[$group] = true; + + $display = (isset($params['startOffset']) && isset($params['startTransition']) && $params['startTransition']) + ? (int) $params['startOffset'] : null; + $show = (isset($params['startOffset']) && !(isset($params['startTransition']) && $params['startTransition'])) + ? (int) $params['startOffset'] : null; + + $opt = array(); + $opt['heightStyle'] = "'content'"; + /*$opt['onActive'] = "function(toggler, i) {toggler.addClass('pane-toggler-down');" . + "toggler.removeClass('pane-toggler');i.addClass('pane-down');i.removeClass('pane-hide');Cookie.write('jpanesliders_" . $group . "',$('div#" . $group . ".pane-sliders > .panel > h3').indexOf(toggler));}"; + $opt['onBackground'] = "function(toggler, i) {toggler.addClass('pane-toggler');" . + "toggler.removeClass('pane-toggler-down');i.addClass('pane-hide');i.removeClass('pane-down');if($('div#" + . $group . ".pane-sliders > .panel > h3').length==$('div#" . $group . ".pane-sliders > .panel > h3.pane-toggler').length) Cookie.write('jpanesliders_" . $group . "',-1);}"; + $opt['duration'] = (isset($params['duration'])) ? (int) $params['duration'] : 300; + $opt['display'] = (isset($params['useCookie']) && $params['useCookie']) ? Request::getInt('jpanesliders_' . $group, $display, 'cookie') + : $display; + $opt['show'] = (isset($params['useCookie']) && $params['useCookie']) ? Request::getInt('jpanesliders_' . $group, $show, 'cookie') : $show; + $opt['opacity'] = (isset($params['opacityTransition']) && ($params['opacityTransition'])) ? 'true' : 'false'; + $opt['alwaysHide'] = (isset($params['allowAllClose']) && (!$params['allowAllClose'])) ? 'false' : 'true';*/ + + $options = array(); + foreach ($opt as $k => $v) + { + if ($v) + { + $options[] = $k . ': ' . $v; + } + } + $options = '{' . implode(',', $options) . '}'; + + Behavior::framework(true); + + \App::get('document')->addScriptDeclaration( + "jQuery(document).ready(function($){ + $('div#" . $group . "').accordion(" . $options . "); + });" + ); + } + } +} diff --git a/core/libraries/Hubzero/Html/Builder/Tabs.php b/core/libraries/Hubzero/Html/Builder/Tabs.php new file mode 100644 index 00000000000..3ca39c6409b --- /dev/null +++ b/core/libraries/Hubzero/Html/Builder/Tabs.php @@ -0,0 +1,118 @@ +'; + } + + /** + * Close the current pane + * + * @return string HTML to close the pane + */ + public static function end() + { + self::$open = false; + + return ''; + } + + /** + * Begins the display of a new panel. + * + * @param string $text Text to display. + * @param string $id Identifier of the panel. + * @return string HTML to start a new panel + */ + public static function panel($text, $id) + { + $content = ''; + + if (self::$open) + { + $content .= ''; + } + else + { + self::$open = true; + } + $content .= '
' . $text . '
'; + + return $content; + } + + /** + * Load the JavaScript behavior. + * + * @param string $group The pane identifier. + * @param array $params Array of options. + * @return void + */ + protected static function behavior($group, $params = array()) + { + static $loaded = array(); + + if (!array_key_exists((string) $group, $loaded)) + { + $options = array(); + + $opt['onActive'] = (isset($params['onActive'])) ? $params['onActive'] : null; + $opt['onBackground'] = (isset($params['onBackground'])) ? $params['onBackground'] : null; + $opt['display'] = (isset($params['startOffset'])) ? (int) $params['startOffset'] : null; + $opt['useStorage'] = (isset($params['useCookie']) && $params['useCookie']) ? 'true' : 'false'; + $opt['titleSelector'] = "'dt.tabs'"; + $opt['descriptionSelector'] = "'dd.tabs'"; + + foreach ($opt as $k => $v) + { + if ($v) + { + $options[] = $k . ': ' . $v; + } + } + + $options = '{' . implode(',', $options) . '}'; + + Behavior::framework(true); + + \App::get('document')->addScriptDeclaration( + 'jQuery(document).ready(function($){ + $("dl#' . $group . '.tabs").tabs(); + });' + ); + + Asset::script('system/jquery.tabs.js', false, true); + + $loaded[(string) $group] = true; + } + } +} diff --git a/core/libraries/Hubzero/Html/Editor.php b/core/libraries/Hubzero/Html/Editor.php new file mode 100644 index 00000000000..7883497d859 --- /dev/null +++ b/core/libraries/Hubzero/Html/Editor.php @@ -0,0 +1,393 @@ +name = $editor; + } + + /** + * Returns the global Editor object, only creating it + * if it doesn't already exist. + * + * @param string $editor The editor to use. + * @return object The Editor object. + */ + public static function getInstance($editor = 'none') + { + $signature = serialize($editor); + + if (empty(self::$instances[$signature])) + { + self::$instances[$signature] = new self($editor); + } + + return self::$instances[$signature]; + } + + /** + * Get the name of the Editor + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the state of the Editor object + * + * @return mixed The state of the object. + */ + public function getState() + { + return $this->state; + } + + /** + * Attach an observer object + * + * @param object $observer An observer object to attach + * @return void + */ + public function attach($observer) + { + // [!] For Joomla compatibility only + } + + /** + * Detach an observer object + * + * @param object $observer An observer object to detach. + * @return boolean True if the observer object was detached. + */ + public function detach($observer) + { + // [!] For Joomla compatibility only + } + + /** + * Initialise the editor + * + * @return void + */ + public function initialise() + { + if (is_null($this->editor)) + { + return; + } + + $return = ''; + $results[] = $this->editor->onInit(); + + foreach ($results as $result) + { + if (trim($result)) + { + //$return .= $result; + $return = $result; + } + } + + $document = \App::get('document'); + if ($document->getType() != 'html') + { + return; + } + $document->addCustomTag($return); + } + + /** + * Display the editor area. + * + * @param string $name The control name. + * @param string $html The contents of the text area. + * @param string $width The width of the text area (px or %). + * @param string $height The height of the text area (px or %). + * @param integer $col The number of columns for the textarea. + * @param integer $row The number of rows for the textarea. + * @param boolean $buttons True and the editor buttons will be displayed. + * @param string $id An optional ID for the textarea (note: since 1.6). If not supplied the name is used. + * @param string $asset The object asset + * @param object $author The author. + * @param array $params Associative array of editor parameters. + * @return string + */ + public function display($name, $html, $width, $height, $col, $row, $buttons = true, $id = null, $asset = null, $author = null, $params = array()) + { + $this->asset = $asset; + $this->author = $author; + $this->load($params); + + // Check whether editor is already loaded + if (is_null($this->editor)) + { + return; + } + + // Backwards compatibility. Width and height should be passed without a semicolon from now on. + // If editor plugins need a unit like "px" for CSS styling, they need to take care of that + $width = str_replace(';', '', $width); + $height = str_replace(';', '', $height); + + $id = $id ?: $name; + + // Initialise variables. + $return = null; + + $results[] = $this->editor->onDisplay($name, $html, $width, $height, $col, $row, $buttons, $id, $asset, $author, $params); + + foreach ($results as $result) + { + if (trim($result)) + { + $return .= $result; + } + } + return $return; + } + + /** + * Save the editor content + * + * @param string $editor The name of the editor control + * @return string + */ + public function save($editor) + { + $this->load(); + + // Check whether editor is already loaded + if (is_null($this->editor)) + { + return; + } + + $return = ''; + $results[] = $this->editor->onSave($editor); + + foreach ($results as $result) + { + if (trim($result)) + { + $return .= $result; + } + } + + return $return; + } + + /** + * Get the editor contents + * + * @param string $editor The name of the editor control + * @return string + */ + public function getContent($editor) + { + $this->load(); + + $return = ''; + $results[] = $this->editor->onGetContent($editor); + + foreach ($results as $result) + { + if (trim($result)) + { + $return .= $result; + } + } + + return $return; + } + + /** + * Set the editor contents + * + * @param string $editor The name of the editor control + * @param string $html The contents of the text area + * @return string + */ + public function setContent($editor, $html) + { + $this->load(); + + $return = ''; + $results[] = $this->editor->onSetContent($editor, $html); + + foreach ($results as $result) + { + if (trim($result)) + { + $return .= $result; + } + } + + return $return; + } + + /** + * Get the editor extended buttons (usually from plugins) + * + * @param string $editor The name of the editor. + * @param mixed $buttons Can be boolean or array, if boolean defines if the buttons are + * displayed, if array defines a list of buttons not to show. + * @return array + */ + public function getButtons($editor, $buttons = true) + { + $result = array(); + + if (is_bool($buttons) && !$buttons) + { + return $result; + } + + // Get plugins + $plugins = Plugin::byType('editors-xtd'); + + foreach ($plugins as $plugin) + { + if (is_array($buttons) && in_array($plugin->name, $buttons)) + { + continue; + } + + Plugin::import('editors-xtd', $plugin->name, false); + $className = 'plgButton' . $plugin->name; + + if (class_exists($className)) + { + $plugin = new $className($this, (array) $plugin); + } + + // Try to authenticate + if ($temp = $plugin->onDisplay($editor, $this->asset, $this->author)) + { + $result[] = $temp; + } + } + + return $result; + } + + /** + * Load the editor + * + * @param array $config Associative array of editor config paramaters + * @return mixed + */ + protected function load($config = array()) + { + // Check whether editor is already loaded + if (!is_null($this->editor)) + { + return; + } + + // Build the path to the needed editor plugin + $name = (string) preg_replace('/[^A-Z0-9_\.-]/i', '', $this->name); + $name = ltrim($name, '.'); + + $path = PATH_APP . '/plugins/editors/' . $name . '/' . $name . '.php'; + if (!is_file($path)) + { + $path = PATH_CORE . '/plugins/editors/' . $name . '/' . $name . '.php'; + if (!is_file($path)) + { + \Notify::error(Lang::txt('JLIB_HTML_EDITOR_CANNOT_LOAD')); + return false; + } + } + + // Require plugin file + require_once $path; + + // Get the plugin + $plugin = Plugin::byType('editors', $this->name); + + $params = new Registry($plugin->params); + $params->merge($config); + + $plugin->params = $params; + + // Build editor plugin classname + $name = 'plgEditor' . $this->name; + + if ($this->editor = new $name($this, (array) $plugin)) + { + // Load plugin parameters + $this->initialise(); + + Plugin::import('editors-xtd'); + } + } +} diff --git a/core/libraries/Hubzero/Html/Grid.php b/core/libraries/Hubzero/Html/Grid.php new file mode 100644 index 00000000000..ceb1ad8e6cd --- /dev/null +++ b/core/libraries/Hubzero/Html/Grid.php @@ -0,0 +1,216 @@ +_elementPath[] = __DIR__ . DS . 'Parameter' . DS . 'Element'; + + if ($data = trim($data)) + { + $this->parse($data); + } + + if ($path) + { + $this->loadSetupFile($path); + } + + $this->_raw = $data; + } + + /** + * Sets a default value if not alreay assigned. + * + * @param string $key The name of the parameter. + * @param string $default An optional value for the parameter. + * @param string $group An optional group for the parameter. + * @return string The value set, or the default if the value was not previously set (or null). + */ + public function def($key, $default = '', $group = '_default') + { + $value = $this->get($key, (string) $default, $group); + + return $this->set($key, $value); + } + + /** + * Sets the XML object from custom XML files. + * + * @param object &$xml An XML object. + * @return void + */ + public function setXML(&$xml) + { + if (is_object($xml)) + { + if ($group = $xml['group']) + { + $this->_xml[(string) $group] = $xml; + } + else + { + $this->_xml['_default'] = $xml; + } + + if ($dir = $xml['addpath']) + { + $this->addElementPath(PATH_ROOT . str_replace('/', DS, (string) $dir)); + } + } + } + + /** + * Render the form control. + * + * @param string $name An optional name of the HTML form control. The default is 'params' if not supplied. + * @param string $group An optional group to render. The default group is used if not supplied. + * @return string HTML + */ + public function render($name = 'params', $group = '_default') + { + if (!isset($this->_xml[$group])) + { + return false; + } + + $params = $this->getParams($name, $group); + $html = array(); + + if ($description = $this->_xml[$group]['description']) + { + // Add the params description to the display + $html[] = '

' . \App::get('language')->txt((string) $description) . '

'; + } + + foreach ($params as $param) + { + if ($param[0]) + { + $html[] = '
'; + $html[] = $param[0]; + $html[] = $param[1]; + $html[] = '
'; + } + else + { + $html[] = $param[1]; + } + } + + if (count($params) < 1) + { + $html[] = '

' . \App::get('language')->txt('JLIB_HTML_NO_PARAMETERS_FOR_THIS_ITEM') . '

'; + } + + return implode(PHP_EOL, $html); + } + + /** + * Render all parameters to an array. + * + * @param string $name An optional name of the HTML form control. The default is 'params' if not supplied. + * @param string $group An optional group to render. The default group is used if not supplied. + * @return array + */ + public function renderToArray($name = 'params', $group = '_default') + { + if (!isset($this->_xml[$group])) + { + return false; + } + + $results = array(); + + foreach ($this->_xml[$group]->children() as $param) + { + $result = $this->getParam($param, $name, $group); + $results[$result[5]] = $result; + } + + return $results; + } + + /** + * Return the number of parameters in a group. + * + * @param string $group An optional group. The default group is used if not supplied. + * @return mixed False if no params exist or integer number of parameters that exist. + */ + public function getNumParams($group = '_default') + { + if (!isset($this->_xml[$group]) || !count($this->_xml[$group]->children())) + { + return false; + } + + return count($this->_xml[$group]->children()); + } + + /** + * Get the number of params in each group. + * + * @return array Array of all group names as key and parameters count as value. + */ + public function getGroups() + { + if (!is_array($this->_xml)) + { + + return false; + } + + $results = array(); + foreach ($this->_xml as $name => $group) + { + $results[$name] = $this->getNumParams($name); + } + + return $results; + } + + /** + * Render all parameters. + * + * @param string $name An optional name of the HTML form control. The default is 'params' if not supplied. + * @param string $group An optional group to render. The default group is used if not supplied. + * @return array An array of all parameters, each as array of the label, the form element and the tooltip. + */ + public function getParams($name = 'params', $group = '_default') + { + if (!isset($this->_xml[$group])) + { + return false; + } + + $results = array(); + foreach ($this->_xml[$group]->children() as $param) + { + $results[] = $this->getParam($param, $name, $group); + } + + return $results; + } + + /** + * Render a parameter type. + * + * @param object &$node A parameter XML element. + * @param string $control_name An optional name of the HTML form control. The default is 'params' if not supplied. + * @param string $group An optional group to render. The default group is used if not supplied. + * @return array Any array of the label, the form element and the tooltip. + */ + public function getParam(&$node, $control_name = 'params', $group = '_default') + { + // Get the type of the parameter. + $type = (string) $node['type']; + + $element = $this->loadElement($type); + + // Check for an error. + if ($element === false) + { + $result = array(); + $result[0] = (string) $node['name']; + $result[1] = \App::get('language')->txt('Element not defined for type') . ' = ' . $type; + $result[5] = $result[0]; + return $result; + } + + // Get value. + $value = $this->get((string) $node['name'], (string) $node['default'], $group); + + return $element->render($node, $value, $control_name); + } + + /** + * Loads an XML setup file and parses it. + * + * @param string $path A path to the XML setup file. + * @return object + */ + public function loadSetupFile($path) + { + $result = false; + + if ($path) + { + if (!file_exists($path)) + { + return $result; + } + + $xml = simplexml_load_file($path); + + if ($params = $xml->params) + { + foreach ($params as $param) + { + $this->setXML($param); + $result = true; + } + } + } + else + { + $result = true; + } + + return $result; + } + + /** + * Loads an element type. + * + * @param string $type The element type. + * @param boolean $new False (default) to reuse parameter elements; true to load the parameter element type again. + * @return object + */ + public function loadElement($type, $new = false) + { + if ($type == 'list') + { + $type = 'select'; + } + + $signature = md5($type); + + if ((isset($this->_elements[$signature]) && !($this->_elements[$signature] instanceof __PHP_Incomplete_Class)) && $new === false) + { + return $this->_elements[$signature]; + } + + $elementClass = __NAMESPACE__ . '\\Parameter\\Element\\' . ucfirst($type); + + if (!class_exists($elementClass)) + { + if (isset($this->_elementPath)) + { + $dirs = $this->_elementPath; + } + else + { + $dirs = array(); + } + + preg_match('/^[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*([\\\\\/][A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$/', str_replace('_', DS, $type) . '.php', $matches); + $file = @ (string) $matches[0]; + + if ($elementFile = \App::get('filesystem')->find($dirs, $file)) + { + include_once $elementFile; + } + else + { + $false = false; + return $false; + } + } + + if (!class_exists($elementClass)) + { + $false = false; + return $false; + } + + $this->_elements[$signature] = new $elementClass($this); + + return $this->_elements[$signature]; + } + + /** + * Add a directory where Parameter should search for element types. + * + * You may either pass a string or an array of directories. + * + * Parameter will be searching for a element type in the same + * order you added them. If the parameter type cannot be found in + * the custom folders, it will look in + * Parameter/types. + * + * @param mixed $path Directory (string) or directories (array) to search. + * @return object + */ + public function addElementPath($path) + { + // Just force path to array. + settype($path, 'array'); + + // Loop through the path directories. + foreach ($path as $dir) + { + // No surrounding spaces allowed! + $dir = trim($dir); + + // Add trailing separators as needed. + if (substr($dir, -1) != DIRECTORY_SEPARATOR) + { + // Directory + $dir .= DIRECTORY_SEPARATOR; + } + + // Add to the top of the search dirs. + array_unshift($this->_elementPath, $dir); + } + + return $this; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element.php b/core/libraries/Hubzero/Html/Parameter/Element.php new file mode 100644 index 00000000000..f0150d6d549 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element.php @@ -0,0 +1,121 @@ +_parent = $parent; + } + + /** + * Get the element name + * + * @return string type of the parameter + */ + public function getName() + { + return $this->_name; + } + + /** + * Method to render an xml element + * + * @param string &$xmlElement Name of the element + * @param string $value Value of the element + * @param string $control_name Name of the control + * @return array Attributes of an element + */ + public function render(&$xmlElement, $value, $control_name = 'params') + { + $name = (string) $xmlElement['name']; + $label = (string) $xmlElement['label']; + $descr = (string) $xmlElement['description']; + + //make sure we have a valid label + $label = $label ? $label : $name; + $result[0] = $this->fetchTooltip($label, $descr, $xmlElement, $control_name, $name); + $result[1] = $this->fetchElement($name, $value, $xmlElement, $control_name); + $result[2] = $descr; + $result[3] = $label; + $result[4] = $value; + $result[5] = $name; + + return $result; + } + + /** + * Method to get a tool tip from an XML element + * + * @param string $label Label attribute for the element + * @param string $description Description attribute for the element + * @param object &$xmlElement The element object + * @param string $control_name Control name + * @param string $name Name attribut + * @return string + */ + public function fetchTooltip($label, $description, &$xmlElement, $control_name = '', $name = '') + { + $output = ''; + + return $output; + } + + /** + * Fetch an element + * + * @param string $name Name attribute of the element + * @param string $value Value attribute of the element + * @param object &$xmlElement Element object + * @param string $control_name Control name of the element + * @return void + */ + public function fetchElement($name, $value, &$xmlElement, $control_name) + { + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Calendar.php b/core/libraries/Hubzero/Html/Parameter/Element/Calendar.php new file mode 100644 index 00000000000..1a25720aba2 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Calendar.php @@ -0,0 +1,48 @@ +attributes('format') ? $node->attributes('format') : '%Y-%m-%d'; + $class = $node->attributes('class') ? $node->attributes('class') : 'inputbox'; + + return Input::calendar($name, $value, array( + 'format' => $format, + 'class' => $class + )); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Category.php b/core/libraries/Hubzero/Html/Parameter/Element/Category.php new file mode 100644 index 00000000000..9d7f7432508 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Category.php @@ -0,0 +1,72 @@ + $class + ), + 'value', + 'text', + (int) $value, + $control_name . $name + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/ComponentLayouts.php b/core/libraries/Hubzero/Html/Parameter/Element/ComponentLayouts.php new file mode 100644 index 00000000000..2a2a61df91e --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/ComponentLayouts.php @@ -0,0 +1,87 @@ +getQuery() + ->select('template') + ->from('#__template_styles') + ->whereEquals('client_id', 0) + ->whereEquals('home', 1) + ->limit(1); + $db->setQuery($query->toString()); + $template = $db->loadResult(); + + $view = (string) $node['view']; + $extn = (string) $node['extension']; + if ($view && $extn) + { + $view = preg_replace('#\W#', '', $view); + $extn = preg_replace('#\W#', '', $extn); + $path1 = PATH_CORE . '/components/' . $extn . '/site/views/' . $view . '/tmpl'; + $path2 = PATH_ROOT . '/templates/' . $template . '/html/' . $extn . '/' . $view; + $options[] = Builder\Select::option('', App::get('language')->txt('JOPTION_USE_MENU_REQUEST_SETTING')); + } + + if ($path1 && $path2) + { + $path1 = Util::normalizePath($path1); + $path2 = Util::normalizePath($path2); + + $files = App::get('filesystem')->files($path1, '^[^_]*\.php$'); + foreach ($files as $file) + { + $options[] = Builder\Select::option(App::get('filesystem')->extension($file)); + } + + if (is_dir($path2) && $files = App::get('filesystem')->files($path2, '^[^_]*\.php$')) + { + $options[] = Builder\Select::optgroup(App::get('language')->txt('JOPTION_FROM_DEFAULT_TEMPLATE')); + foreach ($files as $file) + { + $options[] = Builder\Select::option(App::get('filesystem')->extension($file)); + } + $options[] = Builder\Select::optgroup(App::get('language')->txt('JOPTION_FROM_DEFAULT_TEMPLATE')); + } + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::_getOptions($node), $options); + + return $options; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/ContentLanguages.php b/core/libraries/Hubzero/Html/Parameter/Element/ContentLanguages.php new file mode 100644 index 00000000000..2042b2ca3ee --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/ContentLanguages.php @@ -0,0 +1,56 @@ +getQuery() + ->select('a.lang_code', 'value') + ->select('a.title', 'title') + ->select('a.title_native') + ->from('#__languages', 'a') + ->where('a.published', '>=', '0') + ->order('a.title', 'asc'); + + // Get the options. + $db->setQuery($query->toString()); + $options = $db->loadObjectList(); + + // Check for a database error. + if ($db->getErrorNum()) + { + throw new Exception($db->getErrorMsg(), 500); + } + + // Merge any additional options in the XML definition. + return array_merge(parent::_getOptions($node), $options); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Editors.php b/core/libraries/Hubzero/Html/Parameter/Element/Editors.php new file mode 100644 index 00000000000..3aa283c4176 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Editors.php @@ -0,0 +1,64 @@ +getQuery() + ->select('element', 'value') + ->select('name', 'text') + ->from('#__extensions') + ->whereEquals('folder', 'editors') + ->whereEquals('type', 'plugin') + ->whereEquals('enabled', 1) + ->order('ordering', 'asc') + ->order('name', 'asc'); + + $db->setQuery($query->toString()); + $editors = $db->loadObjectList(); + + array_unshift($editors, Builder\Select::option('', App::get('language')->txt('JOPTION_SELECT_EDITOR'))); + + return Builder\Select::genericlist( + $editors, + $control_name . '[' . $name . ']', + array( + 'id' => $control_name . $name, + 'list.attr' => 'class="inputbox"', + 'list.select' => $value + ) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Filelist.php b/core/libraries/Hubzero/Html/Parameter/Element/Filelist.php new file mode 100644 index 00000000000..b367c4a26f2 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Filelist.php @@ -0,0 +1,82 @@ +files($path, $filter); + + $options = array(); + + if (!$node['hide_none']) + { + $options[] = Builder\Select::option('-1', App::get('language')->txt('JOPTION_DO_NOT_USE')); + } + + if (!$node['hide_default']) + { + $options[] = Builder\Select::option('', App::get('language')->txt('JOPTION_USE_DEFAULT')); + } + + if (is_array($files)) + { + foreach ($files as $file) + { + if ($exclude) + { + if (preg_match(chr(1) . $exclude . chr(1), $file)) + { + continue; + } + } + if ($stripExt) + { + $file = App::get('filesystem')->extension($file); + } + $options[] = Builder\Select::option($file, $file); + } + } + + return Builder\Select::genericlist( + $options, + $control_name . '[' . $name . ']', + array('id' => 'param' . $name, 'list.attr' => 'class="inputbox"', 'list.select' => $value) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Folderlist.php b/core/libraries/Hubzero/Html/Parameter/Element/Folderlist.php new file mode 100644 index 00000000000..ec71755d7de --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Folderlist.php @@ -0,0 +1,73 @@ +folders($path, $filter); + + $options = array(); + foreach ($folders as $folder) + { + if ($exclude) + { + if (preg_match(chr(1) . $exclude . chr(1), $folder)) + { + continue; + } + } + $options[] = Builder\Select::option($folder, $folder); + } + + if (!$node['hide_none']) + { + array_unshift($options, Builder\Select::option('-1', App::get('language')->txt('JOPTION_DO_NOT_USE'))); + } + + if (!$node['hide_default']) + { + array_unshift($options, Builder\Select::option('', App::get('language')->txt('JOPTION_USE_DEFAULT'))); + } + + return Builder\Select::genericlist( + $options, + $control_name . '[' . $name . ']', + array('id' => 'param' . $name, 'list.attr' => 'class="inputbox"', 'list.select' => $value) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Helpsites.php b/core/libraries/Hubzero/Html/Parameter/Element/Helpsites.php new file mode 100644 index 00000000000..3b85e492171 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Helpsites.php @@ -0,0 +1,92 @@ +txt('local'))); + + return Builder\Select::genericlist( + $helpsites, + $control_name . '[' . $name . ']', + array( + 'id' => $control_name . $name, + 'list.attr' => 'class="inputbox"', + 'list.select' => $value + ) + ); + } + + /** + * Builds a list of the help sites which can be used in a select option. + * + * @param string $pathToXml Path to an XML file. + * @param string $selected Language tag to select (if exists). + * @return array An array of arrays (text, value, selected). + */ + public static function createSiteList($pathToXml, $selected = null) + { + $list = array(); + $xml = false; + + if (!empty($pathToXml)) + { + // Disable libxml errors and allow to fetch error information as needed + libxml_use_internal_errors(true); + + // Try to load the XML file + $xml = simplexml_load_file($pathToXml); + } + + if (!$xml) + { + $option['text'] = 'English (US) hubzero.org'; + $option['value'] = 'http://hubzero.org/documentation'; + $list[] = $option; + } + else + { + $option = array(); + + foreach ($xml->sites->site as $site) + { + $option['text'] = (string) $site; + $option['value'] = (string) $site->attributes()->url; + + $list[] = $option; + } + } + + return $list; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Hidden.php b/core/libraries/Hubzero/Html/Parameter/Element/Hidden.php new file mode 100644 index 00000000000..d53f4ec063f --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Hidden.php @@ -0,0 +1,56 @@ + $class)); + } + + /** + * Fetch tooltip for a hidden element + * + * @param string $label Element label + * @param string $description Element description (which renders as a tool tip) + * @param object &$xmlElement Element object + * @param string $control_name Control name + * @param string $name Element name + * @return string + */ + public function fetchTooltip($label, $description, &$xmlElement, $control_name = '', $name = '') + { + return false; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Html.php b/core/libraries/Hubzero/Html/Parameter/Element/Html.php new file mode 100644 index 00000000000..5884811c60e --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Html.php @@ -0,0 +1,37 @@ +addAttribute('filter', $filter); + + $parameter = $this->_parent->loadElement('filelist'); + + return $parameter->fetchElement($name, $value, $node, $control_name); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Languages.php b/core/libraries/Hubzero/Html/Parameter/Element/Languages.php new file mode 100644 index 00000000000..55ec856682c --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Languages.php @@ -0,0 +1,52 @@ +createLanguageList($value, constant('JPATH_' . strtoupper($client)), true); + array_unshift($languages, Builder\Select::option('', App::get('language')->txt('JOPTION_SELECT_LANGUAGE'))); + + return Builder\Select::genericlist( + $languages, + $control_name . '[' . $name . ']', + array( + 'id' => $control_name . $name, + 'list.attr' => 'class="inputbox"', + 'list.select' => $value + ) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Menu.php b/core/libraries/Hubzero/Html/Parameter/Element/Menu.php new file mode 100644 index 00000000000..12f5d0e8448 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Menu.php @@ -0,0 +1,51 @@ +txt('JOPTION_SELECT_MENU'))); + + return Builder\Select::genericlist( + $options, + $control_name . '[' . $name . ']', + array('id' => $control_name . $name, 'list.attr' => 'class="inputbox"', 'list.select' => $value) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/MenuItem.php b/core/libraries/Hubzero/Html/Parameter/Element/MenuItem.php new file mode 100644 index 00000000000..75a088eee4c --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/MenuItem.php @@ -0,0 +1,167 @@ +_parent->get('menu_type'); + if (!empty($menuType)) + { + $where = ' WHERE menutype = ' . $db->quote($menuType); + } + else + { + $where = ' WHERE 1'; + } + + // Load the list of menu types + // TODO: move query to model + $query = $db->getQuery() + ->select('menutype') + ->select('title') + ->from('#__menu_types') + ->order('title', 'asc'); + + $db->setQuery($query->toString()); + $menuTypes = $db->loadObjectList(); + + if ($state = $node->attributes('state')) + { + $where .= ' AND published = ' . (int) $state; + } + + // load the list of menu items + // TODO: move query to model + $query = $db->getQuery() + ->select('id') + ->select('parent_id') + ->select('title') + ->select('menutype') + ->select('type') + ->from('#__menu'); + + $menuType = $this->_parent->get('menu_type'); + if (!empty($menuType)) + { + $query->whereEquals('menutype', $menuType); + } + if ($state = $node->attributes('state')) + { + $query->whereEquals('published', (int) $state); + } + + $query + ->order('menutype', 'asc') + ->order('parent_id', 'asc') + ->order('ordering', 'asc'); + + $db->setQuery($query->toString()); + $menuItems = $db->loadObjectList(); + + // Establish the hierarchy of the menu + // TODO: use node model + $children = array(); + + if ($menuItems) + { + // First pass - collect children + foreach ($menuItems as $v) + { + $pt = $v->parent_id; + $list = @$children[$pt] ? $children[$pt] : array(); + array_push($list, $v); + $children[$pt] = $list; + } + } + + // Second pass - get an indent list of the items + $list = Builder\Menu::treerecurse(0, '', array(), $children, 9999, 0, 0); + + // Assemble into menutype groups + $n = count($list); + $groupedList = array(); + foreach ($list as $k => $v) + { + $groupedList[$v->menutype][] = &$list[$k]; + } + + // Assemble menu items to the array + $options = array(); + $options[] = Builder\Select::option('', App::get('language')->txt('JOPTION_SELECT_MENU_ITEM')); + + foreach ($menuTypes as $type) + { + if ($menuType == '') + { + $options[] = Builder\Select::option('0', ' ', 'value', 'text', true); + $options[] = Builder\Select::option($type->menutype, $type->title . ' - ' . App::get('language')->txt('JGLOBAL_TOP'), 'value', 'text', true); + } + if (isset($groupedList[$type->menutype])) + { + $n = count($groupedList[$type->menutype]); + for ($i = 0; $i < $n; $i++) + { + $item = &$groupedList[$type->menutype][$i]; + + // If menutype is changed but item is not saved yet, use the new type in the list + if (App::get('request')->getString('option', '', 'get') == 'com_menus') + { + $currentItemArray = App::get('request')->getVar('cid', array(0), '', 'array'); + $currentItemId = (int) $currentItemArray[0]; + $currentItemType = App::get('request')->getString('type', $item->type, 'get'); + if ($currentItemId == $item->id && $currentItemType != $item->type) + { + $item->type = $currentItemType; + } + } + + $disable = strpos($node->attributes('disable'), $item->type) !== false ? true : false; + $options[] = Builder\Select::option($item->id, '   ' . $item->treename, 'value', 'text', $disable); + + } + } + } + + return Builder\Select::genericlist( + $options, + $control_name . '[' . $name . ']', + array( + 'id' => $control_name . $name, + 'list.attr' => 'class="inputbox"', + 'list.select' => $value + ) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/ModuleLayouts.php b/core/libraries/Hubzero/Html/Parameter/Element/ModuleLayouts.php new file mode 100644 index 00000000000..b7e5d123c42 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/ModuleLayouts.php @@ -0,0 +1,88 @@ +getQuery() + ->select('template') + ->from('#__template_styles') + ->whereEquals('client_id', (int) $clientId) + ->whereEquals('home', '1'); + $db->setQuery($query->toString()); + $template = $db->loadResult(); + + if ($module = (string) $node['module']) + { + $base = ($clientId == 1) ? PATH_CORE : PATH_CORE; + $module = preg_replace('#\W#', '', $module); + $path1 = PATH_CORE . '/modules/' . $module . '/tmpl'; + $path2 = PATH_APP . '/templates/' . $template . '/html/' . $module; + $options[] = Builder\Select::option('', ''); + } + + if ($path1 && $path2) + { + $path1 = Util::normalizePath($path1); + $path2 = Util::normalizePath($path2); + + $files = App::get('filesystem')->files($path1, '^[^_]*\.php$'); + foreach ($files as $file) + { + $options[] = Builder\Select::option(App::get('filesystem')->extension($file)); + } + + if (is_dir($path2) && $files = App::get('filesystem')->files($path2, '^[^_]*\.php$')) + { + $options[] = Builder\Select::optgroup(App::get('language')->txt('JOPTION_FROM_DEFAULT')); + foreach ($files as $file) + { + $options[] = Builder\Select::option(App::get('filesystem')->extension($file)); + } + $options[] = Builder\Select::optgroup(App::get('language')->txt('JOPTION_FROM_DEFAULT')); + } + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::_getOptions($node), $options); + + return $options; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Password.php b/core/libraries/Hubzero/Html/Parameter/Element/Password.php new file mode 100644 index 00000000000..c419dccfc99 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Password.php @@ -0,0 +1,43 @@ +'; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Radio.php b/core/libraries/Hubzero/Html/Parameter/Element/Radio.php new file mode 100644 index 00000000000..a13cb2ad130 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Radio.php @@ -0,0 +1,73 @@ +children() as $option) + { + $val = (string) $option['value']; + $text = (string) $option; + $options[] = Builder\Select::option($val, $text); + } + + return Builder\Select::radiolist($options, '' . $control_name . '[' . $name . ']', 'class="option"', 'value', 'text', $value, $control_name . $name, true) . ''; + } + + /** + * Method to get a tool tip from an XML element + * + * @param string $label Label attribute for the element + * @param string $description Description attribute for the element + * @param object &$xmlElement The element object + * @param string $control_name Control name + * @param string $name Name attribut + * @return string + */ + public function fetchTooltip($label, $description, &$xmlElement, $control_name = '', $name = '') + { + $output = '
txt($label) . '::' . App::get('language')->txt($description) . '">'; + } + else + { + $output .= '>'; + } + $output .= App::get('language')->txt($label) . ''; + + return $output; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Select.php b/core/libraries/Hubzero/Html/Parameter/Element/Select.php new file mode 100644 index 00000000000..52603ad9c83 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Select.php @@ -0,0 +1,85 @@ +children() as $option) + { + $val = $option['value']; + $text = (string) $option; + $options[] = Builder\Select::option($val, \App::get('language')->txt($text)); + } + return $options; + } + + /** + * Fetch a calendar element + * + * @param string $name Element name + * @param string $value Element value + * @param object &$node XMLElement node object containing the settings for the element + * @param string $control_name Control name + * @return string + */ + public function fetchElement($name, $value, &$node, $control_name) + { + $ctrl = $control_name . '[' . $name . ']'; + $attribs = ' '; + + if ($v = $node['size']) + { + $attribs .= 'size="' . (string) $v . '"'; + } + if ($v = $node['class']) + { + $attribs .= 'class="' . (string) $v . '"'; + } + else + { + $attribs .= 'class="inputbox"'; + } + if ($m = $node['multiple']) + { + $attribs .= 'multiple="multiple"'; + $ctrl .= '[]'; + } + + return Builder\Select::genericlist( + $this->_getOptions($node), + $ctrl, + array( + 'id' => $control_name . $name, + 'list.attr' => $attribs, + 'list.select' => $value + ) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Spacer.php b/core/libraries/Hubzero/Html/Parameter/Element/Spacer.php new file mode 100644 index 00000000000..98852e90a04 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Spacer.php @@ -0,0 +1,57 @@ +txt($value); + } + + return ' '; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Sql.php b/core/libraries/Hubzero/Html/Parameter/Element/Sql.php new file mode 100644 index 00000000000..242dcecdd14 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Sql.php @@ -0,0 +1,71 @@ +setQuery((string) $node['query']); + + $key = (string) $node['key_field']; + $key = $key ?: 'value'; + + $val = (string) $node['value_field']; + $val = $val ?: $name; + + $options = $db->loadObjectlist(); + + // Check for an error. + if ($db->getErrorNum()) + { + throw new Exception($db->getErrorMsg(), 500); + } + + if (!$options) + { + $options = array(); + } + + return Builder\Select::genericlist( + $options, + $control_name . '[' . $name . ']', + array( + 'id' => $control_name . $name, + 'list.attr' => 'class="inputbox"', + 'list.select' => $value, + 'option.key' => $key, + 'option.text' => $val + ) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/TemplateStyle.php b/core/libraries/Hubzero/Html/Parameter/Element/TemplateStyle.php new file mode 100644 index 00000000000..43e2826be57 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/TemplateStyle.php @@ -0,0 +1,75 @@ +getQuery() + ->select('*') + ->from('#__template_styles') + ->whereEquals('client_id', '0') + ->whereEquals('home', '0'); + + $db->setQuery($query->toString()); + $data = $db->loadObjectList(); + + $default = Builder\Select::option(0, App::get('language')->txt('JOPTION_USE_DEFAULT'), 'id', 'description'); + array_unshift($data, $default); + + $selected = $this->_getSelected(); + $html = Builder\Select::genericlist($data, $control_name . '[' . $name . ']', 'class="inputbox" size="6"', 'id', 'description', $selected); + + return $html; + } + + /** + * Get the selected template style. + * + * @return integer The template style id. + */ + protected function _getSelected() + { + $id = App::get('request')->getVar('cid', 0); + + $db = App::get('db'); + $query = $db->getQuery() + ->select('template_style_id') + ->from('#__menu') + ->whereEquals('id', (int) $id[0]); + $db->setQuery($query->toString()); + + return $db->loadResult(); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Text.php b/core/libraries/Hubzero/Html/Parameter/Element/Text.php new file mode 100644 index 00000000000..47708cd85ef --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Text.php @@ -0,0 +1,47 @@ +'; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Textarea.php b/core/libraries/Hubzero/Html/Parameter/Element/Textarea.php new file mode 100644 index 00000000000..e3a80f1bcfa --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Textarea.php @@ -0,0 +1,44 @@ + tags so they are not visible when editing + $value = str_replace('
', "\n", $value); + + return ''; + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/Timezones.php b/core/libraries/Hubzero/Html/Parameter/Element/Timezones.php new file mode 100644 index 00000000000..44dbf70064b --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/Timezones.php @@ -0,0 +1,96 @@ +get('offset'); + } + + $lang = App::get('language'); + + // LOCALE SETTINGS + $timezones = array( + Builder\Select::option(-12, $lang->txt('UTC__12_00__INTERNATIONAL_DATE_LINE_WEST')), + Builder\Select::option(-11, $lang->txt('UTC__11_00__MIDWAY_ISLAND__SAMOA')), + Builder\Select::option(-10, $lang->txt('UTC__10_00__HAWAII')), + Builder\Select::option(-9.5, $lang->txt('UTC__09_30__TAIOHAE__MARQUESAS_ISLANDS')), + Builder\Select::option(-9, $lang->txt('UTC__09_00__ALASKA')), + Builder\Select::option(-8, $lang->txt('UTC__08_00__PACIFIC_TIME__US__AMP__CANADA_')), + Builder\Select::option(-7, $lang->txt('UTC__07_00__MOUNTAIN_TIME__US__AMP__CANADA_')), + Builder\Select::option(-6, $lang->txt('UTC__06_00__CENTRAL_TIME__US__AMP__CANADA___MEXICO_CITY')), + Builder\Select::option(-5, $lang->txt('UTC__05_00__EASTERN_TIME__US__AMP__CANADA___BOGOTA__LIMA')), + Builder\Select::option(-4, $lang->txt('UTC__04_00__ATLANTIC_TIME__CANADA___CARACAS__LA_PAZ')), + Builder\Select::option(-4.5, $lang->txt('UTC__04_30__VENEZUELA')), + Builder\Select::option(-3.5, $lang->txt('UTC__03_30__ST__JOHN_S__NEWFOUNDLAND__LABRADOR')), + Builder\Select::option(-3, $lang->txt('UTC__03_00__BRAZIL__BUENOS_AIRES__GEORGETOWN')), + Builder\Select::option(-2, $lang->txt('UTC__02_00__MID_ATLANTIC')), + Builder\Select::option(-1, $lang->txt('UTC__01_00__AZORES__CAPE_VERDE_ISLANDS')), + Builder\Select::option(0, $lang->txt('UTC_00_00__WESTERN_EUROPE_TIME__LONDON__LISBON__CASABLANCA')), + Builder\Select::option(1, $lang->txt('UTC__01_00__AMSTERDAM__BERLIN__BRUSSELS__COPENHAGEN__MADRID__PARIS')), + Builder\Select::option(2, $lang->txt('UTC__02_00__ISTANBUL__JERUSALEM__KALININGRAD__SOUTH_AFRICA')), + Builder\Select::option(3, $lang->txt('UTC__03_00__BAGHDAD__RIYADH__MOSCOW__ST__PETERSBURG')), + Builder\Select::option(3.5, $lang->txt('UTC__03_30__TEHRAN')), + Builder\Select::option(4, $lang->txt('UTC__04_00__ABU_DHABI__MUSCAT__BAKU__TBILISI')), + Builder\Select::option(4.5, $lang->txt('UTC__04_30__KABUL')), + Builder\Select::option(5, $lang->txt('UTC__05_00__EKATERINBURG__ISLAMABAD__KARACHI__TASHKENT')), + Builder\Select::option(5.5, $lang->txt('UTC__05_30__BOMBAY__CALCUTTA__MADRAS__NEW_DELHI__COLOMBO')), + Builder\Select::option(5.75, $lang->txt('UTC__05_45__KATHMANDU')), Builder\Select::option(6, $lang->txt('UTC__06_00__ALMATY__DHAKA')), + Builder\Select::option(6.5, $lang->txt('UTC__06_30__YAGOON')), + Builder\Select::option(7, $lang->txt('UTC__07_00__BANGKOK__HANOI__JAKARTA__PHNOM_PENH')), + Builder\Select::option(8, $lang->txt('UTC__08_00__BEIJING__PERTH__SINGAPORE__HONG_KONG')), + Builder\Select::option(8.75, $lang->txt('UTC__08_00__WESTERN_AUSTRALIA')), + Builder\Select::option(9, $lang->txt('UTC__09_00__TOKYO__SEOUL__OSAKA__SAPPORO__YAKUTSK')), + Builder\Select::option(9.5, $lang->txt('UTC__09_30__ADELAIDE__DARWIN__YAKUTSK')), + Builder\Select::option(10, $lang->txt('UTC__10_00__EASTERN_AUSTRALIA__GUAM__VLADIVOSTOK')), + Builder\Select::option(10.5, $lang->txt('UTC__10_30__LORD_HOWE_ISLAND__AUSTRALIA_')), + Builder\Select::option(11, $lang->txt('UTC__11_00__MAGADAN__SOLOMON_ISLANDS__NEW_CALEDONIA')), + Builder\Select::option(11.5, $lang->txt('UTC__11_30__NORFOLK_ISLAND')), + Builder\Select::option(12, $lang->txt('UTC__12_00__AUCKLAND__WELLINGTON__FIJI__KAMCHATKA')), + Builder\Select::option(12.75, $lang->txt('UTC__12_45__CHATHAM_ISLAND')), Builder\Select::option(13, $lang->txt('UTC__13_00__TONGA')), + Builder\Select::option(14, $lang->txt('UTC__14_00__KIRIBATI')) + ); + + return Builder\Select::genericlist( + $timezones, + $control_name . '[' . $name . ']', + array( + 'id' => $control_name . $name, + 'list.attr' => 'class="inputbox"', + 'list.select' => $value + ) + ); + } +} diff --git a/core/libraries/Hubzero/Html/Parameter/Element/UserGroup.php b/core/libraries/Hubzero/Html/Parameter/Element/UserGroup.php new file mode 100644 index 00000000000..af39cb711e2 --- /dev/null +++ b/core/libraries/Hubzero/Html/Parameter/Element/UserGroup.php @@ -0,0 +1,59 @@ +_name = $name; + + // Set base path to find buttons. + $this->_buttonPath[] = __DIR__ . DS . 'Toolbar' . DS . 'Button'; + } + + /** + * Push button onto the end of the toolbar array. + * + * @return string The set value. + */ + public function appendButton() + { + $btn = func_get_args(); + + array_push($this->_bar, $btn); + + return true; + } + + /** + * Get the list of toolbar links. + * + * @return array + */ + public function getItems() + { + return $this->_bar; + } + + /** + * Get the name of the toolbar. + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Insert button into the front of the toolbar array. + * + * @return boolean + */ + public function prependButton() + { + $btn = func_get_args(); + + array_unshift($this->_bar, $btn); + + return true; + } + + /** + * Render a tool bar. + * + * @return string HTML for the toolbar. + */ + public function render() + { + $html = array(); + + // Start toolbar div. + $html[] = '
'; + $html[] = '
    '; + + foreach ($this->_bar as $key => $button) + { + $this->_bar[$key][9] = array(); + if ($button[0] == 'Separator') + { + continue; + } + if (!isset($this->_bar[$key - 1]) || $this->_bar[$key - 1][0] == 'Separator') + { + $this->_bar[$key][9][] = 'first'; + } + if (!isset($this->_bar[$key + 1]) || $this->_bar[$key + 1][0] == 'Separator') + { + $this->_bar[$key][9][] = 'last'; + } + } + + // Render each button in the toolbar. + foreach ($this->_bar as $button) + { + $html[] = $this->renderButton($button); + } + + // End toolbar div. + $html[] = '
'; + $html[] = '
'; + + return implode("\n", $html); + } + + /** + * Render a button. + * + * @param object &$node A toolbar node. + * @return string + */ + public function renderButton(&$node) + { + // Get the button type. + $type = $node[0]; + + $button = $this->loadButtonType($type); + + // Check for error. + if ($button === false) + { + return \Lang::txt('JLIB_HTML_BUTTON_NOT_DEFINED', $type); + } + return $button->render($node); + } + + /** + * Loads a button type. + * + * @param string $type Button Type + * @param boolean $new False by default + * @return object + */ + public function loadButtonType($type, $new = false) + { + $signature = md5($type); + if (isset($this->_buttons[$signature]) && $new === false) + { + return $this->_buttons[$signature]; + } + + $buttonClass = __NAMESPACE__ . '\\Toolbar\\Button\\' . $type; + if (!class_exists($buttonClass)) + { + $dirs = isset($this->_buttonPath) ? $this->_buttonPath : array(); + $file = preg_replace('/[^A-Z0-9_\.-]/i', '', str_replace('_', DIRECTORY_SEPARATOR, strtolower($type))) . '.php'; + + if ($buttonFile = $this->find($dirs, $file)) + { + include_once $buttonFile; + } + else + { + throw new \InvalidArgumentException(\Lang::txt('JLIB_HTML_BUTTON_NO_LOAD', $buttonClass, $buttonFile), 500); + } + } + + if (!class_exists($buttonClass)) + { + throw new \Exception("Module file $buttonFile does not contain class $buttonClass.", 500); + } + + $this->_buttons[$signature] = new $buttonClass($this); + + return $this->_buttons[$signature]; + } + + /** + * Searches the directory paths for a given file. + * + * @param mixed $paths An path string or array of path strings to search in + * @param string $file The file name to look for. + * @return mixed The full path and file name for the target file, or boolean false if the file is not found in any of the paths. + */ + protected function find($paths, $file) + { + settype($paths, 'array'); //force to array + + // Start looping through the path set + foreach ($paths as $path) + { + // Get the path to the file + $fullname = $path . '/' . $file; + + // Is the path based on a stream? + if (strpos($path, '://') === false) + { + // Not a stream, so do a realpath() to avoid directory + // traversal attempts on the local file system. + $path = realpath($path); // needed for substr() later + $fullname = realpath($fullname); + } + + // The substr() check added to make sure that the realpath() + // results in a directory registered so that + // non-registered directories are not accessible via directory + // traversal attempts. + if (file_exists($fullname) && substr($fullname, 0, strlen($path)) == $path) + { + return $fullname; + } + } + + // Could not find the file in the set of paths + return false; + } + + /** + * Add a directory where ToolBar should search for button types in LIFO order. + * + * You may either pass a string or an array of directories. + * + * Toolbar will be searching for an element type in the same order you + * added them. If the parameter type cannot be found in the custom folders, + * it will look in __DIR__ . /toolbar/button. + * + * @param mixed $path Directory or directories to search. + * @return void + */ + public function addButtonPath($path) + { + // Just force path to array. + settype($path, 'array'); + + // Loop through the path directories. + foreach ($path as $dir) + { + // No surrounding spaces allowed! + $dir = trim($dir); + + // Add trailing separators as needed. + if (substr($dir, -1) != DIRECTORY_SEPARATOR) + { + // Directory + $dir .= DIRECTORY_SEPARATOR; + } + + // Add to the top of the search dirs. + array_unshift($this->_buttonPath, $dir); + } + } + + /** + * Method to add a menu item. Alias for appendButton() + * + * @param string $name Name of the menu item. + * @param string $link URL of the menu item. + * @param bool True if the item is active, false otherwise. + */ + public function addEntry($name, $link = '', $active = false) + { + $this->appendButton($name, $link, $active); + } +} diff --git a/core/libraries/Hubzero/Html/Toolbar/Button.php b/core/libraries/Hubzero/Html/Toolbar/Button.php new file mode 100644 index 00000000000..5e1353eb62d --- /dev/null +++ b/core/libraries/Hubzero/Html/Toolbar/Button.php @@ -0,0 +1,104 @@ +_parent = $parent; + } + + /** + * Get the element name + * + * @return string type of the parameter + */ + public function getName() + { + return $this->_name; + } + + /** + * Get the HTML to render the button + * + * @param array &$definition Parameters to be passed + * @return string + */ + public function render(&$definition) + { + // Initialise some variables + $html = null; + $cls = array(); + if (isset($definition[9])) + { + $cls = array_pop($definition); + } + $id = call_user_func_array(array(&$this, 'fetchId'), $definition); + $action = call_user_func_array(array(&$this, 'fetchButton'), $definition); + + // Build id attribute + if ($id) + { + $id = 'id="' . $id . '"'; + } + + // Build the HTML Button + $html .= '
  • \n"; + $html .= $action; + $html .= "
  • \n"; + + return $html; + } + + /** + * Method to get the CSS class name for an icon identifier + * + * @param string $identifier Icon identification string + * @return string CSS class name + */ + public function fetchIconClass($identifier) + { + return "icon-$identifier icon-32-$identifier"; + } + + /** + * Get the button + * + * @return string + */ + abstract public function fetchButton(); +} diff --git a/core/libraries/Hubzero/Html/Toolbar/Button/Confirm.php b/core/libraries/Hubzero/Html/Toolbar/Button/Confirm.php new file mode 100644 index 00000000000..ef77cba1c88 --- /dev/null +++ b/core/libraries/Hubzero/Html/Toolbar/Button/Confirm.php @@ -0,0 +1,101 @@ +fetchIconClass($name); + $message = $this->_getCommand($msg, $name, $task, $list); + + $cls = 'toolbar toolbar-confirm'; + + $attr = array(); + $attr[] = 'data-title="' . $text . '"'; + $attr[] = 'data-task="' . $task . '"'; + $attr[] = 'data-confirm="' . $msg . '"'; + + if ($list) + { + $cls .= ' toolbar-list'; + + $attr[] = ' data-message="' . $message . '"'; + } + + $html = "\n"; + $html .= "\n"; + $html .= "$text\n"; + $html .= "\n"; + $html .= "\n"; + + return $html; + } + + /** + * Get the button CSS Id + * + * @param string $type Button type + * @param string $name Name to be used as apart of the id + * @param string $text Button text + * @param string $task The task associated with the button + * @param boolean $list True to allow use of lists + * @param boolean $hideMenu True to hide the menu on click + * @return string Button CSS Id + */ + public function fetchId($type = 'Confirm', $name = '', $text = '', $task = '', $list = true, $hideMenu = false) + { + return $this->_parent->getName() . '-' . $name; + } + + /** + * Get the JavaScript command for the button + * + * @param object $msg The message to display. + * @param string $name Not used. + * @param string $task The task used by the application + * @param boolean $list True is requires a list confirmation. + * @return string + */ + protected function _getCommand($msg, $name, $task, $list) + { + Behavior::framework(); + + $message = \Lang::txt('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'); + $message = str_replace('"', '"', $message); + + return $message; + } +} diff --git a/core/libraries/Hubzero/Html/Toolbar/Button/Custom.php b/core/libraries/Hubzero/Html/Toolbar/Button/Custom.php new file mode 100644 index 00000000000..29ee37bde2a --- /dev/null +++ b/core/libraries/Hubzero/Html/Toolbar/Button/Custom.php @@ -0,0 +1,51 @@ +_parent->getName() . '-' . $id; + } +} diff --git a/core/libraries/Hubzero/Html/Toolbar/Button/Help.php b/core/libraries/Hubzero/Html/Toolbar/Button/Help.php new file mode 100644 index 00000000000..5db59ff0c6e --- /dev/null +++ b/core/libraries/Hubzero/Html/Toolbar/Button/Help.php @@ -0,0 +1,204 @@ +fetchIconClass('help'); + $msg = \Lang::txt('JHELP', true); + + if (!strstr('?', $url) + && !strstr('&', $url) + && substr($url, 0, 4) != 'http') + { + $url = \Route::url('index.php?option=com_help&component=' . \Request::getCmd('option') . '&page=' . $url); + } + else + { + $url = $this->_getCommand($ref = $type, $com = false, $override = false, $component = \Request::getCmd('option')); + } + + $html = '' . "\n"; + $html .= '' . "\n"; + $html .= $text . "\n"; + $html .= '' . "\n"; + $html .= '' . "\n"; + + return $html; + } + + /** + * Get the button id + * + * @return string Button CSS Id + */ + public function fetchId() + { + return $this->_parent->getName() . '-' . 'help'; + } + + /** + * Get the JavaScript command for the button + * + * @param string $ref The name of the help screen (its key reference). + * @param boolean $com Use the help file in the component directory. + * @param string $override Use this URL instead of any other. + * @param string $component Name of component to get Help (null for current component) + * @return string JavaScript command string + */ + protected function _getCommand($ref, $com, $override, $component) + { + // Get Help URL + $url = self::createURL($ref, $com, $override, $component); + $url = htmlspecialchars($url, ENT_QUOTES); + //$cmd = "Hubzero.popupWindow('$url', '" . \Lang::txt('JHELP', true) . "', 700, 500, 1)"; + + return $url; //$cmd; + } + + /** + * Create a URL for a given help key reference + * + * @param string $ref The name of the help screen (its key reference) + * @param boolean $useComponent Use the help file in the component directory + * @param string $override Use this URL instead of any other + * @param string $component Name of component (or null for current component) + * @return string + */ + public static function createURL($ref, $useComponent = false, $override = null, $component = null) + { + $local = false; + + // Determine the location of the help file. At this stage the URL + // can contain substitution codes that will be replaced later. + + if ($override) + { + $url = $override; + } + else + { + // Get the user help URL. + $user = \User::getInstance(); + $url = $user->getParam('helpsite'); + + // If user hasn't specified a help URL, then get the global one. + if ($url == '') + { + $url = $app->getCfg('helpurl'); + } + + // Component help URL overrides user and global. + if ($useComponent) + { + // Look for help URL in component parameters. + $params = \Component::params($component); + $url = $params->get('helpURL'); + + if ($url == '') + { + $local = true; + $url = 'components/{component}/help/{language}/{keyref}'; + } + } + + // Set up a local help URL. + if (!$url) + { + $local = true; + $url = 'help/{language}/{keyref}'; + } + } + + // If the URL is local then make sure we have a valid file extension on the URL. + if ($local) + { + if (!preg_match('#\.html$|\.xml$#i', $ref)) + { + $url .= '.html'; + } + } + + // Replace substitution codes in the URL. + $lang = \App::get('language'); + $version = HVERSION; + $hver = explode('.', $version); + $hlang = explode('-', $lang->getTag()); + + $debug = $lang->setDebug(false); + $keyref = $lang->txt($ref); + $lang->setDebug($debug); + + // Replace substitution codes in help URL. + $search = array( + '{app}', // Application name (eg. 'Administrator') + '{component}', // Component name (eg. 'com_content') + '{keyref}', // Help screen key reference + '{language}', // Full language code (eg. 'en-GB') + '{langcode}', // Short language code (eg. 'en') + '{langregion}', // Region code (eg. 'GB') + '{major}', // major version number + '{minor}', // minor version number + '{maintenance}'// maintenance version number + ); + + $replace = array( + \App::get('client')->name, // {app} + $component, // {component} + $keyref, // {keyref} + $lang->getTag(), // {language} + $hlang[0], // {langcode} + $hlang[1], // {langregion} + $hver[0], // {major} + $hver[1], // {minor} + $hver[2]// {maintenance} + ); + + // If the help file is local then check it exists. + // If it doesn't then fallback to English. + if ($local) + { + $try = str_replace($search, $replace, $url); + + if (!\Filesystem::exists(PATH_ROOT . '/' . $try)) + { + $replace[3] = 'en-GB'; + $replace[4] = 'en'; + $replace[5] = 'GB'; + } + } + + $url = str_replace($search, $replace, $url); + + return $url; + } +} diff --git a/core/libraries/Hubzero/Html/Toolbar/Button/Link.php b/core/libraries/Hubzero/Html/Toolbar/Button/Link.php new file mode 100644 index 00000000000..8659d045f6c --- /dev/null +++ b/core/libraries/Hubzero/Html/Toolbar/Button/Link.php @@ -0,0 +1,70 @@ +fetchIconClass($name); + $doTask = $this->_getCommand($url); + + $html = "\n"; + $html .= "\n"; + $html .= "$text\n"; + $html .= "\n"; + $html .= "\n"; + + return $html; + } + + /** + * Get the button CSS Id + * + * @param string $type The button type. + * @param string $name The name of the button. + * @return string Button CSS Id + */ + public function fetchId($type = 'Link', $name = '') + { + return $this->_parent->getName() . '-' . $name; + } + + /** + * Get the JavaScript command for the button + * + * @param object $url Button definition + * @return string JavaScript command string + */ + protected function _getCommand($url) + { + return $url; + } +} diff --git a/core/libraries/Hubzero/Html/Toolbar/Button/Popup.php b/core/libraries/Hubzero/Html/Toolbar/Button/Popup.php new file mode 100644 index 00000000000..4ad9f538c33 --- /dev/null +++ b/core/libraries/Hubzero/Html/Toolbar/Button/Popup.php @@ -0,0 +1,92 @@ +fetchIconClass($name); + $url = $this->_getCommand($name, $url, $width, $height, $top, $left); + + $html = "\n"; + $html .= "\n"; + $html .= "$text\n"; + $html .= "\n"; + $html .= "\n"; + + return $html; + } + + /** + * Get the button id + * + * @param string $type Button type + * @param string $name Button name + * @return string Button CSS Id + */ + public function fetchId($type, $name) + { + return $this->_parent->getName() . '-popup-' . $name; + } + + /** + * Get the JavaScript command for the button + * + * @param string $name Button name + * @param string $url URL for popup + * @param integer $width Unused formerly width. + * @param integer $height Unused formerly height. + * @param integer $top Unused formerly top attribute. + * @param integer $left Unused formerly left attribure. + * @return string Command string + */ + protected function _getCommand($name, $url, $width, $height, $top, $left) + { + if (substr($url, 0, 4) !== 'http') + { + $root = rtrim(\Request::root(true), '/'); + if (substr($url, 0, strlen($root)) != $root) + { + $url = $root . '/' . ltrim($url, '/'); + } + } + + return $url; + } +} diff --git a/core/libraries/Hubzero/Html/Toolbar/Button/Separator.php b/core/libraries/Hubzero/Html/Toolbar/Button/Separator.php new file mode 100644 index 00000000000..838e9154981 --- /dev/null +++ b/core/libraries/Hubzero/Html/Toolbar/Button/Separator.php @@ -0,0 +1,52 @@ +\n\n"; + } + + /** + * Empty implementation (not required for separator) + * + * @return void + */ + public function fetchButton() + { + } +} diff --git a/core/libraries/Hubzero/Html/Toolbar/Button/Standard.php b/core/libraries/Hubzero/Html/Toolbar/Button/Standard.php new file mode 100644 index 00000000000..31be8c02da9 --- /dev/null +++ b/core/libraries/Hubzero/Html/Toolbar/Button/Standard.php @@ -0,0 +1,96 @@ +fetchIconClass($name); + $message = $this->_getCommand($text, $task, $list); + + $cls = 'toolbar toolbar-submit'; + + $attr = array(); + $attr[] = 'data-title="' . $i18n_text . '"'; + $attr[] = 'data-task="' . $task . '"'; + + if ($list) + { + $cls .= ' toolbar-list'; + + $attr[] = ' data-message="' . $message . '"'; + } + + $html = "\n"; + $html .= "\n"; + $html .= "$i18n_text\n"; + $html .= "\n"; + $html .= "\n"; + + return $html; + } + + /** + * Get the button CSS Id + * + * @param string $type Unused string. + * @param string $name Name to be used as apart of the id + * @param string $text Button text + * @param string $task The task associated with the button + * @param boolean $list True to allow use of lists + * @param boolean $hideMenu True to hide the menu on click + * @return string Button CSS Id + */ + public function fetchId($type = 'Standard', $name = '', $text = '', $task = '', $list = true, $hideMenu = false) + { + return $this->_parent->getName() . '-' . $name; + } + + /** + * Get the JavaScript command for the button + * + * @param string $name The task name as seen by the user + * @param string $task The task used by the application + * @param boolean $list True is requires a list confirmation. + * @return string + */ + protected function _getCommand($name, $task, $list) + { + Behavior::framework(); + + $message = \Lang::txt('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'); + $message = addslashes($message); + + return $message; + } +} diff --git a/core/libraries/Hubzero/Http/RedirectResponse.php b/core/libraries/Hubzero/Http/RedirectResponse.php new file mode 100644 index 00000000000..c626b94c29a --- /dev/null +++ b/core/libraries/Hubzero/Http/RedirectResponse.php @@ -0,0 +1,165 @@ +headers->set($key, $value, $replace); + + return $this; + } + + /** + * Flash a piece of data to the session. + * + * @param string $key + * @param mixed $value + * @return object + */ + public function with($key, $value = null) + { + $key = is_array($key) ? $key : [$key => $value]; + + foreach ($key as $k => $v) + { + $this->session->set($k, $v); + } + + return $this; + } + + /** + * Get the request instance. + * + * @return object + */ + public function getRequest() + { + return $this->request; + } + + /** + * Set the request instance. + * + * @param object $request + * @return void + */ + public function setRequest(Request $request) + { + $this->request = $request; + } + + /** + * Get the session store implementation. + * + * @return object + */ + public function getSession() + { + return $this->session; + } + + /** + * Set the session store implementation. + * + * @param object $session + * @return void + */ + public function setSession(SessionStore $session) + { + $this->session = $session; + } + + /** + * Prepares the Response before it is sent to the client. + * + * This method tweaks the Response to ensure that it is + * compliant with RFC 2616. Most of the changes are based on + * the Request that is "associated" with this Response. + * + * @param object $request A Request instance + * @return object The current response. + */ + public function send() + { + if ($this->request) + { + $url = $this->getContent(); + + // Check for relative internal links. + if (preg_match('/^index2?\.php/', $url)) + { + $url = $this->request->base() . $url; + } + + // Strip out any line breaks. + $url = preg_split("/[\r\n]/", $url); + $url = $url[0]; + + // If we don't start with a http we need to fix this before we proceed. + // We could validly start with something else (e.g. ftp), though this would + // be unlikely and isn't supported by this API. + if (!preg_match('/^http/i', $url)) + { + $prefix = $this->request->scheme() . $this->request->getUserInfo() . $this->request->host(); + + if ($url[0] == '/') + { + // We just need the prefix since we have a path relative to the root. + $url = $prefix . $url; + } + else + { + // It's relative to where we are now, so lets add that. + $parts = explode('/', $this->request->path()); + array_pop($parts); + $path = implode('/', $parts) . '/'; + $url = $prefix . $path . $url; + } + } + + $this->setContent($url); + } + + return parent::send(); //prepare($request); + } +} diff --git a/core/libraries/Hubzero/Http/Request.php b/core/libraries/Hubzero/Http/Request.php new file mode 100755 index 00000000000..fcc6c7cab24 --- /dev/null +++ b/core/libraries/Hubzero/Http/Request.php @@ -0,0 +1,827 @@ + '/-?[0-9]+/', + 'float' => '/-?[0-9]+(\.[0-9]+)?/', + 'cmd' => '/[^A-Z0-9_\.-]/i', + 'word' => '/[^A-Z_]/i' + ); + + /** + * Set a variable in one of the request variables. + * + * @param string $name Name + * @param string $value Value + * @param string $hash Hash + * @param boolean $overwrite Boolean + * @return boolean + */ + public function setVar($name, $value = null, $hash = 'method', $overwrite = true) + { + // If overwrite is true, makes sure the variable hasn't been set yet + if (!$overwrite && $this->has($name)) + { + return $this->getVar($name); + } + + // Get the request hash value + $hash = strtolower($hash); + if ($hash === 'method') + { + $hash = strtolower($this->getMethod()); + } + + switch ($hash) + { + case 'server': + $hash = 'server'; + break; + + case 'cookie': + case 'cookies': + $hash = 'cookies'; + break; + + case 'file': + case 'files': + $hash = 'files'; + break; + + case 'head': + case 'get': + case 'query': + $hash = 'query'; + break; + + case 'header': + case 'headers': + $hash = 'headers'; + break; + + case 'post': + case 'request': + case 'delete': + case 'put': + $hash = 'request'; + break; + + default: + $hash = 'query'; + break; + } + + return $this->$hash->set($name, $value); + } + + /** + * Get var + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @param string $type Return type for the variable. [!] Deprecated. Joomla legacy support. + * @param string $mask Filter mask for the variable. [!] Deprecated. Joomla legacy support. + * @return integer Request variable + */ + public function getVar($key, $default = null, $hash = 'input', $type = 'none', $mask = 0) + { + $hash = strtolower($hash); + + switch ($hash) + { + case 'server': + return $this->server($key, $default); + break; + + case 'cookie': + return $this->cookie($key, $default); + break; + + case 'files': + /*$result = $this->file($key, $default); + if ($type == 'array') + { + $res = array( + 'name' => null, + 'tmp_name' => null, + 'mime_type' => null, + 'extension' => null, + 'size' => null + ); + if ($result) + { + var_dump($_FILES); die(); + $res = array( + 'name' => $result->getClientOriginalName(), + 'tmp_name' => $result->getPathName(), + 'mime_type' => $result->getClientMimeType(), + 'extension' => $result->getExtension(), + 'size' => $result->getClientSize() + ); + } + $result = $res; + }*/ + $result = null; + if (isset($_FILES[$key]) && $_FILES[$key] !== null) + { + $result = $_FILES[$key]; + } + $result = ($result !== null ? $result : $default); + if ($type == 'array') + { + $result = (array) $result; + } + return $result; + break; + + case 'post': + return $this->request($key, $default); + break; + + case 'get': + return $this->query($key, $default); + break; + + default: + return $this->input($key, $default); + break; + } + } + + /** + * Get integer + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @return itneger Request variable + */ + public function getInt($key, $default = 0, $hash = 'input') + { + $str = $this->getVar($key, $default, $hash); + $str = is_array($str) ? self::_flatten('', $str) : $str; + preg_match('/-?[0-9]+/', $str, $matches); + $result = @ $matches[0]; + return (!is_null($result) ? (int) $result : $default); + } + + /** + * Get unsigned integer + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @return integer Request variable + */ + public function getUInt($name, $default = 0, $hash = 'input') + { + $result = $this->getInt($name, $default, $hash); + return (!is_null($result) ? abs($result) : $default); + } + + /** + * Get float + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @return integer Request variable + */ + public function getFloat($name, $default = 0.0, $hash = 'input') + { + $result = $this->getVar($key, $default, $hash); + $result = is_array($result) ? self::_flatten('', $result) : $result; + return preg_replace(static::$filters['float'], '', $result); + } + + /** + * Get boolean + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @return boolean Request variable + */ + public function getBool($key = null, $default = null, $hash = 'input') + { + $result = (bool) $this->getVar($key, $default, $hash); + return $result ? true : false; + } + + /** + * Get word + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @return string Request variable + */ + public function getWord($key, $default = null, $hash = 'input') + { + $result = $this->getVar($key, $default, $hash); + $result = is_array($result) ? self::_flatten('', $result) : $result; + return preg_replace(static::$filters['word'], '', $result); + } + + /** + * Get cmd + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @return string Request variable + */ + public function getCmd($key = null, $default = null, $hash = 'input') + { + $result = $this->getVar($key, $default, $hash); + $result = is_array($result) ? self::_flatten('', $result) : $result; + $result = (string) preg_replace(static::$filters['cmd'], '', $result); + return ltrim($result, '.'); + } + + /** + * Fetches and returns a given variable as an array. + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @return array Request variable + */ + public function getArray($key = null, $default = array(), $hash = 'input') + { + return (array) $this->getVar($key, $default, $hash); + } + + /** + * Fetches and returns a given variable as a string. + * + * @param string $key Request key + * @param mixed $default Default value + * @param string $hash Where the var should come from (POST, GET, FILES, COOKIE, METHOD) + * @return string Request variable + */ + public function getString($name, $default = null, $hash = 'input') + { + $result = $this->getVar($name, $default, $hash); + $result = is_array($result) ? self::_flatten('', $result) : $result; + return (string) $result; + } + + /** + * Return the Request instance. + * + * @return object + */ + public function instance() + { + return $this; + } + + /** + * Get the request method. + * + * @return string + */ + public function method() + { + return $this->getMethod(); + } + + /** + * Get the root URL for the application. + * + * @param boolean $pathonly If false, prepend the scheme, host and port information. Default is false. + * @return string + */ + public function root($pathonly = false) + { + $root = rtrim(($pathonly ? '' : $this->getSchemeAndHttpHost()) . $this->getBasePath(), '/'); + $root = explode('/', $root); + if (in_array(end($root), array('administrator', 'api'))) + { + array_pop($root); + } + + return implode('/', $root) . '/'; + } + + /** + * Get the URL (no query string) for the request. + * + * @return string + */ + public function current($query = false) + { + return ($query ? $this->getUri() : rtrim(preg_replace('/\?.*/', '', $this->getUri()), '/')); + } + + /** + * Get the full URL for the request. + * + * @return string + */ + /*public function fullUrl() + { + $query = $this->getQueryString(); + + return $query ? $this->current() . '?' . $query : $this->current(); + }*/ + + /** + * Get the current path info for the request. + * + * @return string + */ + public function path() + { + $pattern = trim($this->getPathInfo(), '/'); + + return $pattern == '' ? '/' : '/' . $pattern; + } + + /** + * Get the current path info for the request. + * + * @param boolean $pathonly If false, prepend the scheme, host and port information. Default is false. + * @return string + */ + public function base($pathonly = false) + { + $path = $this->getBasePath(); + + if ($pathonly) + { + return $path; + } + + return $this->getSchemeAndHttpHost() . '/' . ($path ? trim($path, '/') . '/' : ''); + } + + /** + * Get a segment from the URI (1 based index). + * + * @param string $index + * @param mixed $default + * @return string + */ + public function segment($index, $default = null) + { + $segments = $this->segments(); + + return isset($segments[$index - 1]) ? $segments[$index - 1] : $default; + } + + /** + * Get all of the segments for the request path. + * + * @return array + */ + public function segments() + { + $segments = explode('/', $this->path()); + + return array_values(array_filter($segments, function ($v) + { + return $v != ''; + })); + } + + /** + * Determine if the request is the result of an AJAX call. + * + * @return bool + */ + public function ajax() + { + return $this->isXmlHttpRequest(); + } + + /** + * Determine if the request is over HTTPS. + * + * @return bool + */ + public function secure() + { + return $this->isSecure(); + } + + /** + * Get the IP address of the client. + * + * @return string + */ + public function ip() + { + return $this->getClientIp(); + } + + /** + * Get the request scheme. + * + * @return string + */ + public function scheme() + { + return $this->getScheme(); + } + + /** + * Get the HTTP host. + * + * @return string + */ + public function host() + { + return $this->getHost(); + } + + /** + * Determine if the request contains a given input item. + * + * @param mixed $key string|array + * @return bool + */ + public function has($key) + { + if (count(func_get_args()) > 1) + { + foreach (func_get_args() as $value) + { + if (!$this->has($value)) + { + return false; + } + } + + return true; + } + + if (is_bool($this->input($key)) || is_array($this->input($key))) + { + return true; + } + + return (trim((string) $this->input($key)) !== ''); + } + + /** + * Retrieve an input item from the request. + * + * @param string $key + * @param mixed $default + * @return string + */ + public function input($key = null, $default = null) + { + $input = $this->getInputSource()->all() + $this->query->all(); + + return isset($input[$key]) ? $input[$key] : $default; + } + + /** + * Get the input source for the request. + * + * @return object + */ + protected function getInputSource() + { + return $this->getMethod() == 'GET' ? $this->query : $this->request; + } + + /** + * Retrieve a post item from the request. + * + * @param string $key + * @param mixed $default + * @return string + */ + public function request($key = null, $default = null) + { + return $this->retrieveItem('request', $key, $default); + } + + /** + * Retrieve a query string item from the request. + * + * @param string $key + * @param mixed $default + * @return string + */ + public function query($key = null, $default = null) + { + return $this->retrieveItem('query', $key, $default); + } + + /** + * Retrieve a cookie from the request. + * + * @param string $key + * @param mixed $default + * @return string + */ + public function cookie($key = null, $default = null) + { + return $this->retrieveItem('cookies', $key, $default); + } + + /** + * Retrieve a file from the request. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function file($key = null, $default = null) + { + $array = $this->files->all(); + + if (is_null($key)) + { + return $array; + } + + if (isset($array[$key])) + { + return $array[$key]; + } + + return $default; + } + + /** + * Retrieve a header from the request. + * + * @param string $key + * @param mixed $default + * @return string + */ + public function header($key = null, $default = null) + { + return $this->retrieveItem('headers', $key, $default); + } + + /** + * Retrieve a server variable from the request. + * + * @param string $key + * @param mixed $default + * @return string + */ + public function server($key = null, $default = null) + { + return $this->retrieveItem('server', $key, $default); + } + + /** + * Retrieve a parameter item from a given source. + * + * @param string $source + * @param string $key + * @param mixed $default + * @return string + */ + protected function retrieveItem($source, $key, $default) + { + if (is_null($key)) + { + return $this->$source->all(); + } + + return $this->$source->get($key, $default, true); + } + + /** + * Normalizes a query string. + * + * It builds a normalized query string, where keys/value pairs are alphabetized, + * have consistent escaping and unneeded delimiters are removed. + * + * @param string $qs Query string + * @return string A normalized query string for the Request + */ + public static function normalizeQueryString($qs) + { + if ('' == $qs) + { + return ''; + } + + $parts = array(); + $order = array(); + + foreach (explode('&', $qs) as $param) + { + if ('' === $param || '=' === $param[0]) + { + // Ignore useless delimiters, e.g. "x=y&". + // Also ignore pairs with empty key, even if there was a value, e.g. "=value", as such nameless values cannot be retrieved anyway. + // PHP also does not include them when building _GET. + continue; + } + + $keyValuePair = explode('=', $param, 2); + + // GET parameters, that are submitted from a HTML form, encode spaces as "+" by default (as defined in enctype application/x-www-form-urlencoded). + // PHP also converts "+" to spaces when filling the global _GET or when using the function parse_str. This is why we use urldecode and then normalize to + // RFC 3986 with rawurlencode. + $parts[] = isset($keyValuePair[1]) ? + rawurlencode(urldecode($keyValuePair[0])) . '=' . rawurlencode(urldecode($keyValuePair[1])) : + rawurlencode(urldecode($keyValuePair[0])); + $order[] = urldecode($keyValuePair[0]); + } + + // [!] Work around Symfony's HttpFoundation Request + // reordering incoming GET vars. The following: + // + // post[]=18&post[]=17&post[]=19&post[]=20&post[]=21&post[]=22 + // + // ... would incorrectly result in this: + // + // Array + // ( + // [0] => 17 <- Wrong! + // [1] => 18 <- Wrong! + // [2] => 19 + // [3] => 20 + // [4] => 21 + // [5] => 22 + // ) + //array_multisort($order, SORT_ASC, $parts); + + return implode('&', $parts); + } + + /** + * Gets the value of a user state variable. + * + * @param string $key The key of the user state variable. + * @param string $request The name of the variable passed in a request. + * @param string $default The default value for the variable if not found. Optional. + * @param string $type Filter for the variable. Optional. + * @return The request user state. + */ + public function getState($key, $request, $default = null, $type = 'none') + { + $cur_state = App::has('user') ? App::get('user')->getState($key, $default) : $default; + $new_state = $this->getVar($request, null, 'default', $type); + + // Save the new value only if it was set in this request. + if ($new_state !== null) + { + switch ($type) + { + case 'int': + $new_state = self::_flatten('', $new_state); + $new_state = intval($new_state); + break; + case 'word': + $new_state = (string) self::_flatten('', $new_state); + $new_state = preg_replace('/[^A-Z_]/i', '', $new_state); + break; + case 'cmd': + $new_state = (string) self::_flatten('', $new_state); + $new_state = preg_replace('/[^A-Z0-9_\.-]/i', '', $new_state); + break; + case 'bool': + $new_state = (bool) $new_state; + break; + case 'float': + $new_state = (string) self::_flatten('', $new_state); + $new_state = (float) preg_replace('/-?[0-9]+(\.[0-9]+)?/', '', $new_state); + break; + case 'string': + $new_state = (string) self::_flatten('', $new_state); + break; + case 'array': + $new_state = (array) $new_state; + break; + } + + if (App::has('user')) + { + App::get('user')->setState($key, $new_state); + } + } + else + { + $new_state = $cur_state; + } + + return $new_state; + } + + /** + * Flatten a multi-dimensional array + * + * @param string $separator + * @param mixed $arrayvar + * @return string + */ + private function _flatten($separator, $arrayvar) + { + $out = ''; + + if (is_array($arrayvar)) + { + foreach ($arrayvar as $av) + { + if (is_array($av)) + { + $out .= self::_flatten($separator, $av); // Recursive Use of the Array + } + else + { + $out .= $separator . $av; + } + } + } + else + { + $out .= $separator . $arrayvar; + } + + return $out; + } + + /** + * Checks for a form token in the request. + * + * Use in conjunction with Html::input('token'). + * + * @param string $method The request method in which to look for the token key. + * @return boolean True if found and valid, false otherwise. + */ + public function checkToken($method = 'post') + { + return App::get('session')->checkToken($method); + } + + /** + * Checks for a honeypot in the request + * + * @param string $name + * @param integer $delay + * @return boolean True if found and valid, false otherwise. + */ + public function checkHoneypot($name = null, $delay = 3) + { + $name = $name ?: Honeypot::getName(); + + if ($honey = self::getVar($name, array(), 'post')) + { + if (!Honeypot::isValid($honey['p'], $honey['t'], $delay)) + { + if (App::has('log')) + { + $fallback = 'option=' . $this->getCmd('option') . '&controller=' . $this->getCmd('controller') . '&task=' . $this->getCmd('task'); + + $from = $this->getVar('REQUEST_URI', $fallback, 'server'); + $from = $from ?: $fallback; + + $msg = 'spam honeypot ' . $this->ip(); + if (App::has('user')) + { + $msg .= ' ' . App::get('user')->get('id') . ' ' . App::get('user')->get('username'); + } + $msg .= ' ' . $from; + + App::get('log')->logger('spam')->info($msg); + } + + return false; + } + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Http/Response.php b/core/libraries/Hubzero/Http/Response.php new file mode 100644 index 00000000000..56091623027 --- /dev/null +++ b/core/libraries/Hubzero/Http/Response.php @@ -0,0 +1,164 @@ +compress = (bool) $value; + + return $this; + } + + /** + * Compress the data + * + * Checks the accept encoding of the browser and compresses the data before + * sending it to the client. + * + * @param string $data Content to compress for output. + * @return string compressed data + */ + protected function squeeze($data) + { + $encoding = $this->acceptEncoding(); + + if (!$encoding) + { + return $data; + } + + if (!extension_loaded('zlib') || ini_get('zlib.output_compression')) + { + return $data; + } + + if (headers_sent()) + { + return $data; + } + + if (connection_status() !== 0) + { + return $data; + } + + // Ideal level + $level = 4; + + /* + $size = strlen($data); + $crc = crc32($data); + + $gzdata = "\x1f\x8b\x08\x00\x00\x00\x00\x00"; + $gzdata .= gzcompress($data, $level); + + $gzdata = substr($gzdata, 0, strlen($gzdata) - 4); + $gzdata .= pack("V",$crc) . pack("V", $size); + */ + + $gzdata = gzencode($data, $level); + + $this->headers->set('Content-Encoding', $encoding); + $this->headers->set('X-Content-Encoded-By', 'HUBzero'); + + return $gzdata; + } + + /** + * Check, whether client supports compressed data + * + * @return mixed + */ + protected function acceptEncoding() + { + $encoding = false; + + if (!isset($_SERVER['HTTP_ACCEPT_ENCODING'])) + { + return $encoding; + } + + if (false !== strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) + { + $encoding = 'gzip'; + } + + if (false !== strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip')) + { + $encoding = 'x-gzip'; + } + + return $encoding; + } + + /** + * Set a header on the Response. + * + * @param string $key + * @param mixed $values + * @param bool $replace + * @return object $this + */ + public function header($key, $values, $replace = true) + { + $this->headers->set($key, $values, $replace); + + return $this; + } + + /** + * Sends HTTP headers and content. + * + * @return object Response + */ + public function send($flush = false) + { + if ($this->compress) + { + $this->setContent($this->squeeze($this->getContent())); + } + + $this->sendHeaders(); + $this->sendContent(); + + if ($flush) + { + if (function_exists('fastcgi_finish_request')) + { + fastcgi_finish_request(); + } + elseif ('cli' !== PHP_SAPI) + { + static::closeOutputBuffers(0, true); + flush(); + } + } + + return $this; + } +} diff --git a/core/libraries/Hubzero/Image/Identicon.php b/core/libraries/Hubzero/Image/Identicon.php new file mode 100644 index 00000000000..60831e7b854 --- /dev/null +++ b/core/libraries/Hubzero/Image/Identicon.php @@ -0,0 +1,294 @@ +displayImage('foo'); + * + * // Get image data + * $imageData = $identicon->getImageData('bar'); + * + * // Generate and get the base 64 image uri ready for integrate into an HTML img tag. + * $imageDataUri = $identicon->getImageDataUri('bar'); + * bar Identicon + * + * Based on work by Benjamin Laugueux + */ +class Identicon +{ + /** + * @var string + */ + private $hash; + + /** + * @var integer + */ + private $color; + + /** + * @var integer + */ + private $size; + + /** + * @var integer + */ + private $pixelRatio; + + /** + * @var array + */ + private $arrayOfSquare = array(); + + /** + * Set the image size + * + * @param integer $size + * @return object + */ + public function setSize($size) + { + $this->size = $size; + $this->pixelRatio = round($size / 5); + + return $this; + } + + /** + * Get the image size + * + * @return integer + */ + public function getSize() + { + return $this->size; + } + + /** + * Generate a hash fron the original string + * + * @param string $string + * @return object + */ + public function setString($string) + { + if (null === $string) + { + throw new Exception('The string cannot be null.'); + } + + $this->hash = md5($string); + + $this->convertHashToArrayOfBoolean(); + + return $this; + } + + /** + * Get the identicon string hash + * + * @return string + */ + public function getHash() + { + return $this->hash; + } + + /** + * Convert the hash into an multidimensionnal array of boolean + * + * @return object + */ + private function convertHashToArrayOfBoolean() + { + preg_match_all('/(\w)(\w)/', $this->hash, $chars); + + foreach ($chars[1] as $i => $char) + { + if ($i % 3 == 0) + { + $this->arrayOfSquare[$i/3][0] = $this->convertHexaToBoolean($char); + $this->arrayOfSquare[$i/3][4] = $this->convertHexaToBoolean($char); + } + elseif ($i % 3 == 1) + { + $this->arrayOfSquare[$i/3][1] = $this->convertHexaToBoolean($char); + $this->arrayOfSquare[$i/3][3] = $this->convertHexaToBoolean($char); + } + else + { + $this->arrayOfSquare[$i/3][2] = $this->convertHexaToBoolean($char); + } + ksort($this->arrayOfSquare[$i/3]); + } + + $this->color[0] = hexdec(array_pop($chars[1]))*16; + $this->color[1] = hexdec(array_pop($chars[1]))*16; + $this->color[2] = hexdec(array_pop($chars[1]))*16; + + return $this; + } + + /** + * Convert an heaxecimal number into a boolean + * + * @param string $hexa + * @return boolean + */ + private function convertHexaToBoolean($hexa) + { + return (bool) intval(round(hexdec($hexa)/10)); + } + + /** + * Get arrayOfSquare + * + * @return array + */ + public function getArrayOfSquare() + { + return $this->arrayOfSquare; + } + + + /** + * Generate the Identicon image + * + * @param string $string + * @param integer $size + * @param string $hexaColor + * @return void + */ + private function generateImage($string, $size, $color) + { + $this->setString($string); + $this->setSize($size); + + // prepare the image + $image = imagecreatetruecolor($this->pixelRatio * 5, $this->pixelRatio * 5); + $background = imagecolorallocate($image, 0, 0, 0); + imagecolortransparent($image, $background); + + // prepage the color + if (null !== $color) + { + $this->setColor($color); + } + $color = imagecolorallocate($image, $this->color[0], $this->color[1], $this->color[2]); + + // draw the content + foreach ($this->arrayOfSquare as $lineKey => $lineValue) + { + foreach ($lineValue as $colKey => $colValue) + { + if (true === $colValue) + { + imagefilledrectangle($image, $colKey * $this->pixelRatio, $lineKey * $this->pixelRatio, ($colKey + 1) * $this->pixelRatio, ($lineKey + 1) * $this->pixelRatio, $color); + } + } + } + + imagepng($image); + } + + /** + * Set the image color + * + * @param mixed $color The color in hexa (6 chars) or rgb array + * @return object + */ + public function setColor($color) + { + if (is_array($color)) + { + $this->color[0] = $color[0]; + $this->color[1] = $color[1]; + $this->color[2] = $color[2]; + } + else + { + if (false !== strpos($color, '#')) + { + $color = substr($color, 1); + } + $this->color[0] = hexdec(substr($color, 0, 2)); + $this->color[1] = hexdec(substr($color, 2, 2)); + $this->color[2] = hexdec(substr($color, 4, 2)); + } + + return $this; + } + + /** + * Get the color + * + * @return arrray + */ + public function getColor() + { + return $this->color; + } + + /** + * Display an Identicon image + * + * @param string $string + * @param integer $size + * @param string $hexaColor + * @return void + */ + public function displayImage($string, $size = 64, $hexaColor = null) + { + header("Content-Type: image/png"); + $this->generateImage($string, $size, $hexaColor); + } + + /** + * Get an Identicon PNG image data + * + * @param string $string + * @param integer $size + * @param string $hexaColor + * @return string + */ + public function getImageData($string, $size = 64, $hexaColor = null) + { + ob_start(); + $this->generateImage($string, $size, $hexaColor); + $imageData = ob_get_contents(); + ob_end_clean(); + + return $imageData; + } + + /** + * Get an Identicon PNG image data + * + * @param string $string + * @param integer $size + * @param string $hexaColor + * @return string + */ + public function getImageDataUri($string, $size = 64, $hexaColor = null) + { + return sprintf('data:image/png;base64,%s', base64_encode($this->getImageData($string, $size, $hexaColor))); + } +} diff --git a/core/libraries/Hubzero/Image/Initialcon.php b/core/libraries/Hubzero/Image/Initialcon.php new file mode 100644 index 00000000000..f4686635c4d --- /dev/null +++ b/core/libraries/Hubzero/Image/Initialcon.php @@ -0,0 +1,321 @@ +displayImage('foo'); + * + * // Get image data + * $imageData = $identicon->getImageData('bar'); + * + * // Generate and get the base 64 image uri ready for integrate into an HTML img tag. + * $imageDataUri = $identicon->getImageDataUri('bar'); + * bar Identicon + * + * Based on work by Benjamin Laugueux + */ +class Initialcon +{ + /** + * @var string + */ + private $hash; + + /** + * @var string + */ + private $string; + + /** + * @var integer + */ + private $color; + + /** + * @var integer + */ + private $size; + + /** + * @var string + */ + private $fontPath; + + /** + * @var integer + */ + private $pixelRatio; + + /** + * Set the image size + * + * @param integer $size + * @return object + */ + public function setSize($size) + { + $this->size = $size; + $this->pixelRatio = round($size / 5); + + return $this; + } + + /** + * Get the image size + * + * @return integer + */ + public function getSize() + { + return $this->size; + } + + /** + * Generate a hash fron the original string + * + * @param string $string + * @return object + */ + public function setString($string) + { + if (null === $string) + { + throw new Exception('The string cannot be null.'); + } + + $this->string = $string; + $this->hash = md5($string); + + $this->convertHash(); + + return $this; + } + + /** + * Get the identicon string hash + * + * @return string + */ + public function getHash() + { + return $this->hash; + } + + /** + * Convert the hash into an multidimensionnal array of boolean + * + * @return object + */ + private function convertHash() + { + preg_match_all('/(\w)(\w)/', $this->hash, $chars); + + $this->color[0] = hexdec(array_pop($chars[1]))*16; + $this->color[1] = hexdec(array_pop($chars[1]))*16; + $this->color[2] = hexdec(array_pop($chars[1]))*16; + + return $this; + } + + /** + * Generate the image + * + * @param string $string + * @param integer $size + * @param string $color + * @return void + */ + private function generateImage($string, $size, $color) + { + $this->setString($string); + $this->setSize($size); + + if ($this->fontPath === null) + { + $this->setFontPath(__DIR__ . '/fonts/OpenSans-Regular.ttf'); + } + + // Prepare the image + $image = imagecreatetruecolor($this->pixelRatio * 5, $this->pixelRatio * 5); + $background = imagecolorallocate($image, 0, 0, 0); + + // Prepage the color + if (null !== $color) + { + $this->setColor($color); + } + $color = imagecolorallocate($image, $this->color[0], $this->color[1], $this->color[2]); + imagefilledrectangle($image, 0, 0, $this->size, $this->size, $color); + + // Allocate A Color For The Text + $white = imagecolorallocate($image, 255, 255, 255); + + $rnd = ceil($this->size / 20); + + $fontsize = round($this->size * 0.4); //ceil(($this->size / 3) + $rnd); + + $tb = imagettfbbox($fontsize, 0, $this->getFontPath(), $string); + + // Calculate x baseline + /*if ($tb[0] >= -1) + { + $tb['x'] = abs($tb[0] + 1) * -1; + } + else + { + $tb['x'] = abs($tb[0] + 2); + } + + // Calculate actual text width + $tb['width'] = abs($tb[2] - $tb[0]); + if ($tb[0] < -1) + { + $tb['width'] = abs($tb[2]) + abs($tb[0]) - 1; + } + + // Calculate y baseline + $tb['y'] = abs($tb[5] + 1); + + // Calculate actual text height + $tb['height'] = abs($tb[7]) - abs($tb[1]); + if ($tb[3] > 0) + { + $tb['height'] = abs($tb[7] - $tb[1]) - 1; + }*/ + + // Horizontally centr the text + $x = ceil((($this->size - $tb[2]) / 2) - $rnd); + //$x = ceil(($this->size - $tb['width']) / 2); + + // Vertically center the text + $y = ceil(($this->size - $tb[7] - $rnd) / 2); + //$y = ceil(($this->size - $tb['height']) / 2); + //$y = $this->size - $y; + + // Print Text On Image + imagettftext($image, $fontsize, 0, $x, $y, $white, $this->getFontPath(), $string); + + imagepng($image); + } + + /** + * Set the image color + * + * @param mixed $color The color in hexa (6 chars) or rgb array + * @return object + */ + public function setColor($color) + { + if (is_array($color)) + { + $this->color[0] = $color[0]; + $this->color[1] = $color[1]; + $this->color[2] = $color[2]; + } + else + { + $color = ltrim($color, '#'); + + $this->color[0] = hexdec(substr($color, 0, 2)); + $this->color[1] = hexdec(substr($color, 2, 2)); + $this->color[2] = hexdec(substr($color, 4, 2)); + } + + return $this; + } + + /** + * Get the color + * + * @return arrray + */ + public function getColor() + { + return $this->color; + } + + /** + * Get the font path. + * + * @return string + */ + public function getFontPath() + { + return realpath($this->fontPath); + } + + /** + * Set the font path. + * + * @param string $path + * @return object + */ + public function setFontPath($path) + { + $this->fontPath = $path; + + return $this; + } + + /** + * Display an Identicon image + * + * @param string $string + * @param integer $size + * @param string $color + * @return void + */ + public function displayImage($string, $size = 64, $color = null) + { + header("Content-Type: image/png"); + $this->generateImage($string, $size, $color); + } + + /** + * Get an Identicon PNG image data + * + * @param string $string + * @param integer $size + * @param string $color + * @return string + */ + public function getImageData($string, $size = 64, $color = null) + { + ob_start(); + $this->generateImage($string, $size, $color); + $imageData = ob_get_contents(); + ob_end_clean(); + + return $imageData; + } + + /** + * Get an Identicon PNG image data + * + * @param string $string + * @param integer $size + * @param string $color + * @return string + */ + public function getImageDataUri($string, $size = 64, $color = null) + { + return sprintf('data:image/png;base64,%s', base64_encode($this->getImageData($string, $size, $color))); + } +} diff --git a/core/libraries/Hubzero/Image/Mozify.php b/core/libraries/Hubzero/Image/Mozify.php new file mode 100644 index 00000000000..383c3ee485d --- /dev/null +++ b/core/libraries/Hubzero/Image/Mozify.php @@ -0,0 +1,374 @@ +setImageUrl($config['imageUrl']); + + //set alt text if we have it + if (isset($config['imageAlt'])) + { + $this->setImageAlt($config['imageAlt']); + } + + //set mosaic size if we have it + if (isset($config['mosaicSize'])) + { + $this->setMosaicSize($config['mosaicSize']); + } + + //set mosaic size if we have it + if (isset($config['cssClassName'])) + { + $this->setCssClassName($config['cssClassName']); + } + } + } + + /** + * Mozify! + * + * @return string + */ + public function mozify() + { + if ($this->imageUrl == '') + { + return; + } + + $html = $this->_mozifyCss() . PHP_EOL; + $html .= $this->_mozifyStartMsoHack() . PHP_EOL; + $html .= $this->_mozifyImageReplacement() . PHP_EOL; + $html .= $this->_mozifyMosaic() . PHP_EOL; + $html .= $this->_mozifyEndWrapper() . PHP_EOL; + $html .= $this->_mozifyEndMsoHack() . PHP_EOL; + + $this->counter++; + + return $html; + } + + /** + * Convert an image to a mosaic + * + * @return string + */ + public function mosaic() + { + if ($this->imageUrl == '') + { + return; + } + + return $this->_mozifyMosaic(); + } + + /** + * Accessor Method to get Image Url + * + * @return string + */ + public function getImageUrl() + { + return $this->imageUrl; + } + + /** + * Mutator Method to set Image Url + * + * @param string $imageUrl + * @return void + */ + public function setImageUrl($imageUrl = '') + { + $this->imageUrl = $imageUrl; + $imageSizes = @getimagesize($this->imageUrl); + if ($imageSizes) + { + list($this->imageWidth, $this->imageHeight) = $imageSizes; + } + else + { + $this->setError('Unable to get details of image'); + } + } + + /** + * Accessor Method to get Image Alt Text + * + * @return string + */ + public function getImageAlt() + { + return $this->imageAlt; + } + + /** + * Mutator Method to set Image Alt Text + * + * @param string $imageAlt + * @return void + */ + public function setImageAlt($imageAlt = '') + { + $this->imageAlt = $imageAlt; + } + + /** + * Accessor Method to get Mosaic Size + * + * @return string + */ + public function getMosaicSize() + { + return $this->mosaicSize; + } + + /** + * Mutator Method to set Mosaic Size + * + * @param string $mosaicSize + * @return void + */ + public function setMosaicSize($mosaicSize = '') + { + $this->mosaicSize = $mosaicSize; + } + + /** + * Accessor Method to get CSS Class Name + * + * @return string + */ + public function getCssClassName() + { + return $this->cssClassName . $this->counter; + } + + /** + * Mutator Method to set CSS Class Name + * + * @param string $cssClassName + * @return void + */ + public function setCssClassName($cssClassName = '') + { + $this->cssClassName = $cssClassName; + } + + /** + * Generate CSS needed for mozify + * + * @return string + */ + private function _mozifyCss() + { + //get the class + $class = $this->getCssClassName(); + + //build css needed for mozify + $css = ''; + return $css; + } + + /** + * Output the start of the MSO hack + * + * @return string + */ + private function _mozifyStartMsoHack() + { + //get the class + $class = $this->getCssClassName(); + + //build mso hack + $mosHack = ''; + return $mosHack; + } + + /** + * Output the end of the MSO hack + * + * @return string + */ + private function _mozifyEndMsoHack() + { + $msoHack = ''; + return $msoHack; + } + + /** + * Image replacement + * + * @return string + */ + private function _mozifyImageReplacement() + { + //get the class + $class = $this->getCssClassName(); + + //build replacement html + $replacement = ''; + $replacement .= ''; + $replacement .= ''; + $replacement .= ''; + $wrapper .= ''; + $wrapper .= ''; + $wrapper .= '
    '; + $replacement .= '
    '; + $replacement .= ''; + $replacement .= ''; + $replacement .= ''; + $replacement .= ''; + $replacement .= ''; + $replacement .= ''; + $replacement .= '
    '; + $replacement .= '
    '; + return $replacement; + } + + /** + * Create a mosaic + * + * @return string + */ + private function _mozifyMosaic() + { + //get image resource + $resource = imagecreatefromstring(file_get_contents($this->imageUrl)); + + //get the class + $class = $this->getCssClassName(); + + //build mosaic html + $mosaic = ''; + $mosaic .= ''; + for ($y = 0; $y < $this->imageHeight; $y+=$this->mosaicSize) + { + $mosaic .= ''; + for ($x = 0; $x < $this->imageWidth; $x+=$this->mosaicSize) + { + $color = imagecolorat($resource, $x, $y); + $rgba = imagecolorsforindex($resource, $color); + //$rgba['alpha'] = $rgba['alpha']; + $color_string = $this->_rgb2hex($rgba); + $mosaic .= '' . PHP_EOL; + } + $mosaic .= '' . PHP_EOL; + } + $mosaic .= ''; + $mosaic .= '
    '; + return $mosaic; + } + + /** + * Cinvert an RGB value to hex + * + * @param array $rgb + * @return string + */ + private function _rgb2hex(array $rgb) + { + if (isset($rgb['alpha'])) + { + unset($rgb['alpha']); + } + $out = ""; + foreach ($rgb as $c) + { + $hex = base_convert($c, 10, 16); + $out .= ($c < 16) ? ("0" . $hex) : $hex; + } + return '#' . strtoupper($out); + } + + /** + * Output the end of the wrapper + * + * @return string + */ + private function _mozifyEndWrapper() + { + $wrapper = '
    '; + return $wrapper; + } +} diff --git a/core/libraries/Hubzero/Image/MozifyHelper.php b/core/libraries/Hubzero/Image/MozifyHelper.php new file mode 100644 index 00000000000..f57f86559d2 --- /dev/null +++ b/core/libraries/Hubzero/Image/MozifyHelper.php @@ -0,0 +1,43 @@ +]*)>/', $html, $matches, PREG_SET_ORDER); + + //if we have matches mozify the images + if (count($matches) > 0) + { + foreach ($matches as $match) + { + $config = array( + 'imageUrl' => $match[1], + 'mosaicSize' => $mosaicSize + ); + $him = new Mozify($config); + $html = str_replace($match[0], $him->mozify(), $html); + } + } + + return $html; + } +} diff --git a/core/libraries/Hubzero/Image/Processor.php b/core/libraries/Hubzero/Image/Processor.php new file mode 100644 index 00000000000..83414b70b7a --- /dev/null +++ b/core/libraries/Hubzero/Image/Processor.php @@ -0,0 +1,645 @@ +source = $image_source; + $this->config = $config; + + if (!$this->checkPackageRequirements('gd')) + { + return; + } + + // check to see if we have an image to work with + if (is_null($this->source)) + { + $this->setError(\Lang::txt('[ERROR] Image Source not set.')); + return; + } + + //check to make sure its a file if not remote + if (!$isRemoteImage && !is_file($this->source)) + { + $this->setError(\Lang::txt('[ERROR] Image doesn\'t exist on the server.')); + return; + } + + //open image + if (!$this->openImage()) + { + $this->setError(\Lang::txt('[ERROR] Invalid/corrupted image file')); + return; + } + } + + /** + * Set the image type + * + * @param string $type Image type to set + * @return void + */ + public function setImageType($type) + { + if ($type) + { + $this->image_type = $type; + } + + if ($this->image_type == IMAGETYPE_PNG && $this->resource) + { + imagealphablending($this->resource, false); + imagesavealpha($this->resource, true); + } + } + + /** + * Check if a required package is installed + * + * @param integer $package Package name + * @return boolean True on success + */ + private function checkPackageRequirements($package = '') + { + if ($package == '') + { + return false; + } + + $installed_exts = get_loaded_extensions(); + if (!in_array($package, $installed_exts)) + { + $this->setError(\Lang::txt('[ERROR] You are missing the required PHP package %s.', $package)); + return false; + } + + return true; + } + + /** + * Open an image and get it's type (png, jpg, gif) + * + * @return bool + */ + private function openImage() + { + try + { + $image_atts = getimagesize($this->source); + if (empty($image_atts)) + { + return false; + } + + switch ($image_atts['mime']) + { + case 'image/jpeg': + $this->image_type = IMAGETYPE_JPEG; + $this->resource = imagecreatefromjpeg($this->source); + break; + case 'image/gif': + $this->image_type = IMAGETYPE_GIF; + $this->resource = imagecreatefromgif($this->source); + break; + case 'image/png': + case 'image/x-png': + $this->image_type = IMAGETYPE_PNG; + $this->resource = imagecreatefrompng($this->source); + break; + default: + return false; + break; + } + + if ($this->image_type == IMAGETYPE_PNG) + { + imagesavealpha($this->resource, true); + imagealphablending($this->resource, false); + } + + if (isset($this->config['auto_rotate']) && $this->config['auto_rotate'] == true) + { + $this->autoRotate(); + } + + if (!empty($this->resource)) + { + return true; + } + return false; + } + catch (Exception $error) + { + $this->setError($error->getMessage()); + return false; + } + } + + /** + * Image Rotation + * + * @return void + */ + public function autoRotate() + { + if (!$this->checkPackageRequirements('exif')) + { + $this->setError(\Lang::txt('You need the PHP exif library installed to rotate image based on Exif Orientation value.')); + return false; + } + + if ($this->image_type == IMAGETYPE_JPEG) + { + try + { + $this->exif_data = exif_read_data($this->source); + } + catch (Exception $e) + { + $this->exif_data = array(); + } + + if (isset($this->exif_data['Orientation'])) + { + switch ($this->exif_data['Orientation']) + { + case 2: + $this->flip(true, false); + break; + + case 3: + $this->rotate(180); + break; + + case 4: + $this->flip(false, true); + break; + + case 5: + $this->rotate(270); + $this->flip(true, false); + break; + + case 6: + $this->rotate(270); + break; + + case 7: + $this-rotate(90); + $this->flip(true, false); + break; + + case 8: + $this->rotate(90); + break; + } + } + } + } + + /** + * Image Flip + * + * @param integer $rotation Degrees to rotate + * @param integer $background Point to rotate from + * @return void + */ + public function rotate($rotation = 0, $background = 0) + { + if (empty($this->resource)) + { + return false; + } + $resource = imagerotate($this->resource, $rotation, $background); + imagedestroy($this->resource); + $this->resource = $resource; + } + + /** + * Image Flip + * + * @param boolean $flip_horizontal Flip the image horizontally? + * @param boolean $flip_vertical Flip the image vertically? + * @return void + */ + public function flip($flip_horizontal, $flip_vertical = false) + { + if (empty($this->resource)) + { + return false; + } + $resource = $this->resource; + $width = imagesx($resource); + $height = imagesy($resource); + $new_resource = imagecreatetruecolor($width, $height); + + for ($x=0; $x<$width; $x++) + { + for ($y=0; $y<$height; $y++) + { + if ($flip_horizontal && $flip_vertical) + { + imagecopy($new_resource, $resource, $width-$x-1, $height-$y-1, $x, $y, 1, 1); + } + else if ($flip_horizontal) + { + imagecopy($new_resource, $resource, $width-$x-1, $y, $x, $y, 1, 1); + } + else if ($flip_vertical) + { + imagecopy($new_resource, $resource, $x, $height-$y-1, $x, $y, 1, 1); + } + } + } + + $this->resource = $new_resource; + imagedestroy($resource); + } + + /** + * Image Crop + * + * @param integer $top Top point to crop from + * @param integer $right Right point to crop from + * @param integer $bottom Bottom point to crop from + * @param integer $left Left point to crop from + * @return void + */ + public function crop($top, $right = 0, $bottom = 0, $left = 0) + { + if (empty($this->resource)) + { + return false; + } + $width = imagesx($this->resource); + $height = imagesy($this->resource); + $new_width = $width - ($left + $right); + $new_height = $height - ($top + $bottom); + + $resource = imagecreatetruecolor($new_width, $new_height); + imagecopy($resource, $this->resource, 0, 0, $left, $top, $new_width, $new_height); + + imagedestroy($this->resource); + $this->resource = $resource; + } + + /** + * Image Resize + * + * @param integer $new_dimension Size to resize image to + * @param boolean $use_height Use the height as the baseline? (uses width by default) + * @param boolean $squared Make the image square? + * @param boolean $resample Resample the image? + * @return void + */ + public function resize($new_dimension, $use_height = false, $squared = false, $resample = true) + { + if (empty($this->resource)) + { + return false; + } + $percent = false; + $width = imagesx($this->resource); + $height = imagesy($this->resource); + $w = $width; + $h = $height; + $x = 0; + $y = 0; + + if (($new_dimension > $width && !$use_height) || ($new_dimension > $height && $use_height)) + { + return; + } + + if ($new_dimension < 1) + { + $percent = $new_dimension; + } + elseif (substr($new_dimension, -1) == '%') + { + $percent = (substr($new_dimension, 0, -1) / 100); + } + + if ($percent !== false) + { + $new_dimension = $use_height ? ($height * $percent) : ($width * $percent); + $new_dimension = round($new_dimension); + } + + if ($squared) + { + $new_w = $new_dimension; + $new_h = $new_dimension; + if (!$use_height) + { + $x = ceil(($width - $height) / 2); + $w = $height; + $h = $height; + } + else + { + $y = ceil(($height - $width) / 2); + $w = $width; + $h = $width; + } + } + else + { + if (!empty($this->config['resize_by']) && $this->config['resize_by'] == 'largest') + { + // find whatever is larger and use it for resizing + $use_height = false; + if ($height > $width) + { + $use_height = true; + } + } + + if (!$use_height) + { + $new_w = $new_dimension; + $new_h = floor($height * ($new_w / $width)); + } + else + { + $new_h = $new_dimension; + $new_w = floor($width * ($new_h / $height)); + } + } + + $resource = imagecreatetruecolor($new_w, $new_h); + + $transparencyIndex = imagecolortransparent($this->resource); + $transparencyColor = array('red' => 255, 'green' => 255, 'blue' => 255); + if ($transparencyIndex >= 0) + { + $transparencyColor = imagecolorsforindex($this->resource, $transparencyIndex); + } + $transparencyIndex = imagecolorallocate( + $resource, + $transparencyColor['red'], + $transparencyColor['green'], + $transparencyColor['blue'] + ); + imagefill($resource, 0, 0, $transparencyIndex); + imagecolortransparent($resource, $transparencyIndex); + + if ($resample) + { + imagecopyresampled($resource, $this->resource, 0, 0, $x, $y, $new_w, $new_h, $w, $h); + } + else + { + imagecopyresized($resource, $this->resource, 0, 0, $x, $y, $new_w, $new_h, $w, $h); + } + + imagedestroy($this->resource); + $this->resource = $resource; + } + + /** + * Image Geo Location Data + * + * @return void + */ + public function getGeoLocation() + { + if (!$this->checkPackageRequirements('exif')) + { + $this->setError(\Lang::txt('You need the PHP exif library installed to rotate image based on Exif Orientation value.')); + return false; + } + + try + { + $this->exif_data = exif_read_data($this->source); + } + catch (Exception $e) + { + $this->exif_data = array(); + } + + if (isset($this->exif_data['GPSLatitude'])) + { + $lat = $this->exif_data['GPSLatitude']; + $lat_dir = $this->exif_data['GPSLatitudeRef']; + $long = $this->exif_data['GPSLongitude']; + $long_dir = $this->exif_data['GPSLongitudeRef']; + + $latitude = $this->geo_single_fracs2dec($lat); + $longitude = $this->geo_single_fracs2dec($long); + $latitude_formatted = $this->geo_pretty_fracs2dec($lat) . $lat_dir; + $longitude_formatted = $this->geo_pretty_fracs2dec($long) . $long_dir; + + if ($lat_dir == 'S') + { + $latitude *= -1; + } + + if ($long_dir == 'W') + { + $longitude *= -1; + } + + $geo = array( + 'latitude' => $latitude, + 'longitude' => $longitude, + 'latitude_formatted' => $latitude_formatted, + 'longitude_formatted' => $longitude_formatted + ); + } + else + { + $geo = array(); + } + + return $geo; + } + + /** + * Convert a fraction to decimal + * + * @param string $str Fraction to convert + * @return integer + */ + private function geo_frac2dec($str) + { + list($n, $d) = explode('/', $str); + + if (!empty($d)) + { + return $n / $d; + } + + return $str; + } + + /** + * Convert fractions to decimals with formatting + * + * @param array $fracs Fractions to convert + * @return string + */ + private function geo_pretty_fracs2dec($fracs) + { + return $this->geo_frac2dec($fracs[0]) . '° ' . $this->geo_frac2dec($fracs[1]) . '′ ' . $this->geo_frac2dec($fracs[2]) . '″ '; + } + + /** + * Convert fractions to decimals + * + * @param array $fracs Fractions to convert + * @return integer + */ + private function geo_single_fracs2dec($fracs) + { + return $this->geo_frac2dec($fracs[0]) + $this->geo_frac2dec($fracs[1]) / 60 + $this->geo_frac2dec($fracs[2]) / 3600; + } + + /** + * Display an image + * + * @return void + */ + public function display() + { + $image_atts = getimagesize($this->source); + header('Content-type: ' . $image_atts['mime']); + $this->output(null); + } + + /** + * Display an image inline + * + * @return string + **/ + public function inline() + { + // Start buffer and grab output + ob_start(); + $this->output(null); + $image_data = ob_get_contents(); + ob_end_clean(); + + // Encode and build data uri + $base64 = base64_encode($image_data); + $image_atts = getimagesize($this->source); + + return 'data:' . $image_atts['mime'] . ';base64,' . $base64; + } + + /** + * Save an image + * + * @param string $save_path Path to save image + * @param boolean $make_paths Allow for path generation? + * @return void + */ + public function save($save_path = null, $make_paths = false) + { + $path = $this->source; + + if (!is_null($save_path)) + { + $info = pathinfo($save_path); + + if ($make_paths) + { + \App::get('filesystem')->makeDirectory($info['dirname']); + } + + if (!is_dir($info['dirname']) && $make_paths == false) + { + $this->setError(\Lang::txt('You must supply a valid path or allow save function to create recursive path')); + return; + } + + $path = $save_path; + } + + $this->output($path); + } + + /** + * Generate an image and save to a location + * + * @param string $save_path Path to save image + * @return void + */ + private function output($save_path) + { + if ($this->resource != null) + { + switch ($this->image_type) + { + case IMAGETYPE_PNG: + imagepng($this->resource, $save_path); + break; + + case IMAGETYPE_GIF: + imagegif($this->resource, $save_path); + break; + + case IMAGETYPE_JPEG: + imagejpeg($this->resource, $save_path); + break; + } + } + } +} diff --git a/core/libraries/Hubzero/Image/fonts/OpenSans-Regular.ttf b/core/libraries/Hubzero/Image/fonts/OpenSans-Regular.ttf new file mode 100755 index 00000000000..cfd8402eee9 Binary files /dev/null and b/core/libraries/Hubzero/Image/fonts/OpenSans-Regular.ttf differ diff --git a/core/libraries/Hubzero/Item/Announcement.php b/core/libraries/Hubzero/Item/Announcement.php new file mode 100644 index 00000000000..3522c288155 --- /dev/null +++ b/core/libraries/Hubzero/Item/Announcement.php @@ -0,0 +1,254 @@ + 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'created', + 'created_by' + ); + + /** + * Automatically fillable fields + * + * @var array + */ + public $always = array( + 'publish_up', + 'publish_down' + ); + + /** + * Fields to be parsed + * + * @var array + */ + protected $parsed = array( + 'content' + ); + + /** + * Sets up additional custom rules + * + * @return void + */ + public function setup() + { + $this->addRule('publish_down', function($data) + { + if (!$data['publish_down'] || $data['publish_down'] == '0000-00-00 00:00:00') + { + return false; + } + return $data['publish_down'] >= $data['publish_up'] ? false : Lang::txt('The entry cannot end before it begins'); + }); + } + + /** + * Generates automatic owned by field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticPublishUp($data) + { + if (!isset($data['publish_up'])) + { + $data['publish_up'] = null; + } + + $publish_up = $data['publish_up']; + + if (!$publish_up || $publish_up == '0000-00-00 00:00:00') + { + $publish_up = ($data['id'] ? $this->created : Date::of('now')->toSql()); + } + + return $publish_up; + } + + /** + * Generates automatic owned by field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticPublishDown($data) + { + if (!isset($data['publish_down']) || !$data['publish_down']) + { + $data['publish_down'] = null; + } + return $data['publish_down']; + } + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function creator() + { + return $this->belongsToOne('Hubzero\User\User', 'created_by'); + } + + /** + * Check if the entry is available + * + * @return boolean + */ + public function inPublishWindow() + { + if ($this->started() && !$this->ended()) + { + return true; + } + + return false; + } + + /** + * Has the publish window started? + * + * @return boolean + */ + public function started() + { + // If it doesn't exist or isn't published + if ($this->isNew()) + { + return false; + } + + if ($this->get('publish_up') + && $this->get('publish_up') != '0000-00-00 00:00:00' + && $this->get('publish_up') > Date::toSql()) + { + return false; + } + + return true; + } + + /** + * Has the publish window ended? + * + * @return boolean + */ + public function ended() + { + // If it doesn't exist or isn't published + if ($this->isNew()) + { + return true; + } + + if ($this->get('publish_down') + && $this->get('publish_down') != '0000-00-00 00:00:00' + && $this->get('publish_down') <= Date::toSql()) + { + return true; + } + + return false; + } + + /** + * Method to check if announcement belongs to entity + * + * @param string $scope + * @param integer $scope_id + * @return boolean + */ + public function belongsToObject($scope, $scope_id) + { + // Make sure we have an id + if ($this->isNew()) + { + return true; + } + + // Make sure scope and id match + if ($this->get('scope') == (string)$scope + && $this->get('scope_id') == (int)$scope_id) + { + return true; + } + + return false; + } + + /** + * Return a formatted timestamp + * + * @param string $as What format to return + * @return string + */ + public function published($as='') + { + if (!$this->get('publish_up') || $this->get('publish_up') == '0000-00-00 00:00:00') + { + $this->set('publish_up', $this->get('created')); + } + + $as = strtolower($as); + + if ($as) + { + if ($as == 'date') + { + return Date::of($this->get('publish_up'))->toLocal(Lang::txt('DATE_FORMAT_HZ1')); + } + + if ($as == 'time') + { + return Date::of($this->get('publish_up'))->toLocal(Lang::txt('TIME_FORMAT_HZ1')); + } + + return Date::of($this->get('publish_up'))->toLocal($as); + } + + return $this->get('publish_up'); + } +} diff --git a/core/libraries/Hubzero/Item/Comment.php b/core/libraries/Hubzero/Item/Comment.php new file mode 100644 index 00000000000..b288c5f4f0a --- /dev/null +++ b/core/libraries/Hubzero/Item/Comment.php @@ -0,0 +1,460 @@ + 'notempty', + 'item_id' => 'positive|nonzero', + 'item_type' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + **/ + public $initiate = array( + 'created', + 'created_by' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + **/ + public $always = array( + 'modified', + 'modified_by', + 'item_type' + ); + + /** + * Return a formatted Created timestamp + * + * @param string $as What data to return + * @return string + */ + public function created($as='') + { + $as = strtolower($as); + + if ($as == 'date') + { + return Date::of($this->get('created'))->toLocal(Lang::txt('DATE_FORMAT_HZ1')); + } + + if ($as == 'time') + { + return Date::of($this->get('created'))->toLocal(Lang::txt('TIME_FORMAT_HZ1')); + } + + return $this->get('created'); + } + + /** + * Defines a belongs to one relationship between comment and user + * + * @return object + */ + public function creator() + { + return $this->belongsToOne('Hubzero\User\User', 'created_by'); + } + + /** + * Generates automatic created field value + * + * @return string + */ + public function automaticModified() + { + return Date::of('now')->toSql(); + } + + /** + * Generates automatic created by field value + * + * @return int + */ + public function automaticModifiedBy() + { + return User::get('id'); + } + + /** + * Generates automatic created field value + * + * @param array $data + * @return string + */ + public function automaticItemType($data) + { + return strtolower(preg_replace("/[^a-zA-Z0-9\-]/", '', trim($data['item_type']))); + } + + /** + * Determine if record was modified + * + * @return boolean True if modified, false if not + */ + public function wasModified() + { + if ($this->get('modified') && $this->get('modified') != '0000-00-00 00:00:00') + { + return true; + } + + return false; + } + + /** + * Return a formatted Modified timestamp + * + * @param string $as What data to return + * @return string + */ + public function modified($as='') + { + $as = strtolower($as); + + if ($as == 'date') + { + return Date::of($this->get('modified'))->toLocal(Lang::txt('DATE_FORMAT_HZ1')); + } + + if ($as == 'time') + { + return Date::of($this->get('modified'))->toLocal(Lang::txt('TIME_FORMAT_HZ1')); + } + + return $this->get('modified'); + } + + /** + * Was the entry reported? + * + * @return boolean True if reported, False if not + */ + public function isReported() + { + return ($this->get('state') == 3); + } + + /** + * Get either a count of or list of replies + * + * @param array $filters Filters to apply to query + * @return object + */ + public function replies($filters = array()) + { + if (!isset($filters['item_id'])) + { + $filters['item_id'] = $this->get('item_id'); + } + + if (!isset($filters['item_type'])) + { + $filters['item_type'] = $this->get('item_type'); + } + + $entries = self::all() + ->whereEquals('parent', (int) $this->get('id')) + ->whereEquals('item_type', $filters['item_type']) + ->whereEquals('item_id', (int) $filters['item_id']); + + if (isset($filters['state'])) + { + $entries->whereIn('state', (array) $filters['state']); + } + + return $entries; + } + + /** + * Get parent comment + * + * @return object + */ + public function parent() + { + return self::oneOrFail($this->get('parent', 0)); + } + + /** + * Get a list of votes + * + * @return object + */ + public function votes() + { + return $this->oneShiftsToMany('Hubzero\Item\Vote', 'item_id', 'item_type'); + } + + /** + * Get a list of files + * + * @return object + */ + public function files() + { + return $this->oneToMany('Hubzero\Item\Comment\File', 'comment_id'); + } + + /** + * Check if a user has voted for this entry + * + * @param integer $user_id Optinal user ID to set as voter + * @param string $ip IP Address + * @return integer + */ + public function ballot($user_id = 0, $ip = null) + { + if (User::isGuest()) + { + $vote = new Vote(); + $vote->set('item_type', 'comment'); + $vote->set('item_id', $this->get('id')); + $vote->set('created_by', $user_id); + $vote->set('ip', $ip); + + return $vote; + } + + $user = $user_id ? User::getInstance($user_id) : User::getInstance(); + $ip = $ip ?: Request::ip(); + + // See if a person from this IP has already voted in the last week + $votes = $this->votes(); + + if ($user->get('id')) + { + $votes->whereEquals('created_by', $user->get('id')); + } + elseif ($ip) + { + $votes->whereEquals('ip', $ip); + } + + $vote = $votes + ->ordered() + ->limit(1) + ->row(); + + if (!$vote || !$vote->get('id')) + { + $vote = new Vote(); + $vote->set('item_type', 'comment'); + $vote->set('item_id', $this->get('id')); + $vote->set('created_by', $user_id); + if ($ip) + { + $vote->set('ip', $ip); + } + } + + return $vote; + } + + /** + * Vote for the entry + * + * @param integer $vote The vote [0, 1] + * @param integer $user_id Optinal user ID to set as voter + * @param string $ip Optional IP address + * @return boolean False if error, True on success + */ + public function vote($vote = 0, $user_id = 0, $ip = null) + { + if (!$this->get('id')) + { + $this->addError(Lang::txt('No record found')); + return false; + } + + if (!$vote) + { + $this->addError(Lang::txt('No vote provided')); + return false; + } + + $ip = $ip ?: Request::ip(); + + $al = $this->ballot($user_id, $ip); + $al->set('item_type', 'comment'); + $al->set('item_id', $this->get('id')); + $al->set('created_by', $user_id); + if ($ip) + { + $al->set('ip', $ip); + } + + $vote = $al->automaticVote(['vote' => $vote]); + + if ($this->get('created_by') == $user_id) + { + $this->addError(Lang::txt('Cannot vote for your own entry')); + return false; + } + + if ($vote != $al->get('vote', 0)) + { + if ($vote > 0) + { + $this->set('positive', (int) $this->get('positive') + 1); + if ($al->get('id')) + { + $this->set('negative', (int) $this->get('negative') - 1); + } + } + else + { + if ($al->get('id')) + { + $this->set('positive', (int) $this->get('positive') - 1); + } + $this->set('negative', (int) $this->get('negative') + 1); + } + + if (!$this->save()) + { + return false; + } + + $al->set('vote', $vote); + + if (!$al->save()) + { + $this->addError($al->getError()); + return false; + } + } + + return true; + } + + /** + * Saves the current model to the database + * + * @return bool + */ + public function save() + { + // Make sure children inherit states + if ($this->get('state') == self::STATE_DELETED + || $this->get('state') == self::STATE_UNPUBLISHED) + { + foreach ($this->replies()->rows() as $comment) + { + $comment->set('state', $this->get('state')); + + if (!$comment->save()) + { + $this->addError($comment->getError()); + + return false; + } + } + } + + return parent::save(); + } + + /** + * Delete the record and all associated data + * + * @return bool False if error, True on success + */ + public function destroy() + { + // Can't delete what doesn't exist + if ($this->isNew()) + { + return true; + } + + // Remove comments + foreach ($this->replies()->rows() as $comment) + { + if (!$comment->destroy()) + { + $this->addError($comment->getError()); + return false; + } + } + + // Remove votes + foreach ($this->votes()->rows() as $vote) + { + if (!$vote->destroy()) + { + $this->addError($vote->getError()); + return false; + } + } + + // Remove files + foreach ($this->files()->rows() as $file) + { + if (!$file->destroy()) + { + $this->addError($file->getError()); + return false; + } + } + + return parent::destroy(); + } +} diff --git a/core/libraries/Hubzero/Item/Comment/File.php b/core/libraries/Hubzero/Item/Comment/File.php new file mode 100644 index 00000000000..fa6ab9e68c9 --- /dev/null +++ b/core/libraries/Hubzero/Item/Comment/File.php @@ -0,0 +1,293 @@ + 'positive|nonzero', + 'filename' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + **/ + public $always = array( + 'filename' + ); + + /** + * Set the upload path + * + * @param string $path Path to set to + * @return object + */ + public function setUploadDir($path) + { + $path = str_replace(' ', '_', trim($path)); + $path = Util::normalizePath($path); + + $this->uploadDir = ($path ? $path : $this->uploadDir); + + return $this; + } + + /** + * Get the upload path + * + * @return string + */ + public function getUploadDir() + { + return PATH_APP . $this->uploadDir; + } + + /** + * Ensure no invalid characters + * + * @param array $data + * @return string + */ + public function automaticFilename($data) + { + $filename = $data['filename']; + $filename = preg_replace("/[^A-Za-z0-9.]/i", '-', $filename); + + $ext = strrchr($filename, '.'); + $prefix = substr($filename, 0, -strlen($ext)); + + if (strlen($prefix) > 240) + { + $prefix = substr($prefix, 0, 240); + $filename = $prefix . $ext; + } + + $data['filename'] = $filename; + + return $data['filename']; + } + + /** + * Ensure no conflicting file names by + * renaming the incoming file if the name + * already exists + * + * @param array $data + * @return string + */ + public function uniqueFilename($data) + { + $filename = $this->automaticFilename($data); + + if (file_exists($this->getUploadDir() . DS . $data['comment_id'] . DS . $filename)) + { + $ext = strrchr($filename, '.'); + $prefix = substr($filename, 0, -strlen($ext)); + + $i = 1; + + while (is_file($this->getUploadDir() . DS . $data['comment_id'] . DS . $filename)) + { + $filename = $prefix . ++$i . $ext; + } + } + + $data['filename'] = $filename; + + return $data['filename']; + } + + /** + * Delete record + * + * @return boolean True if successful, False if not + */ + public function destroy() + { + $path = $this->path(); + + if (file_exists($path)) + { + if (!\Filesystem::delete($path)) + { + $this->addError('Unable to delete file.'); + + return false; + } + } + + return parent::destroy(); + } + + /** + * Upload file + * + * @param string $name + * @param string $temp + * @return bool + */ + public function upload($name, $temp) + { + $destination = $this->getUploadDir() . DS . $this->get('comment_id'); + + if (!is_dir($destination)) + { + if (!\Filesystem::makeDirectory($destination)) + { + $this->addError('Unable to create upload path.'); + + return false; + } + } + + $filename = $this->uniqueFilename(array( + 'filename' => $name, + 'comment_id' => $this->get('comment_id') + )); + + $destination .= DS . $filename; + + if (!\Filesystem::upload($temp, $destination)) + { + $this->addError('Unable to upload file.'); + + return false; + } + + $this->set('filename', $filename); + + return true; + } + + /** + * File path + * + * @return integer + */ + public function path() + { + return $this->getUploadDir() . DS . $this->get('comment_id') . DS . $this->get('filename'); + } + + /** + * Is the file an image? + * + * @return boolean + */ + public function isImage() + { + return preg_match("/\.(bmp|gif|jpg|jpe|jpeg|png)$/i", $this->get('filename')); + } + + /** + * Is the file an image? + * + * @return boolean + */ + public function size() + { + if ($this->size === null) + { + $this->size = 0; + + $path = $this->path(); + + if (file_exists($path)) + { + $this->size = filesize($path); + } + } + + return $this->size; + } + + /** + * File width and height + * + * @return array + */ + public function dimensions() + { + if (!$this->dimensions) + { + $this->dimensions = array(0, 0); + + if ($this->isImage() && file_exists($this->path())) + { + $this->dimensions = getimagesize($this->path()); + } + } + + return $this->dimensions; + } + + /** + * File width + * + * @return integer + */ + public function width() + { + $dimensions = $this->dimensions(); + + return $dimensions[0]; + } + + /** + * File height + * + * @return integer + */ + public function height() + { + $dimensions = $this->dimensions(); + + return $dimensions[1]; + } +} diff --git a/core/libraries/Hubzero/Item/Vote.php b/core/libraries/Hubzero/Item/Vote.php new file mode 100644 index 00000000000..c0e37ad3597 --- /dev/null +++ b/core/libraries/Hubzero/Item/Vote.php @@ -0,0 +1,198 @@ + 'positive|nonzero', + 'item_type' => 'notempty', + 'vote' => 'notempty' + ); + + /** + * Automatically fillable fields + * + * @var array + */ + public $always = array( + 'vote', + 'item_type' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'created', + 'created_by' + ); + + /** + * Runs extra setup code when creating a new model + * + * @return void + */ + public function setup() + { + $this->addRule('ip', function($data) + { + if (isset($data['ip']) && !Validate::ip($data['ip'])) + { + return Lang::txt('Invalid IP address'); + } + + return false; + }); + } + + /** + * Generates automatic item type value + * + * @param array $data the data being saved + * @return string + */ + public function automaticItemType($data) + { + if (isset($data['item_type'])) + { + $data['item_type'] = strtolower(preg_replace("/[^a-zA-Z0-9\-]/", '', trim($data['item_type']))); + } + + return $data['item_type']; + } + + /** + * Generates automatic vote value + * + * @param array $data the data being saved + * @return integer + */ + public function automaticVote($data) + { + if (!isset($data['vote'])) + { + $data['vote'] = 1; + } + + switch ($data['vote']) + { + case 'no': + case 'down': + case 'dislike': + case 'negative': + case 'minus': + case '-': + case '-1': + case -1: + $data['vote'] = -1; + break; + + case 'yes': + case 'up': + case 'like': + case 'positive': + case 'plus': + case '+': + case '1': + case 1: + default: + $data['vote'] = 1; + break; + } + + return $data['vote']; + } + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object \Hubzero\Database\Relationship\BelongsToOne + */ + public function voter() + { + return $this->belongsToOne('Hubzero\User\User', 'created_by'); + } + + /** + * Load a record by scope and scope ID + * + * @param integer $item_id Item type + * @param string $item_type Item ID + * @param integer $created_by User ID + * @param string $ip IP address + * @return object + */ + public static function oneByScope($item_id, $item_type, $created_by = 0, $ip = null) + { + $model = self::all() + ->whereEquals('item_id', (int)$item_id) + ->whereEquals('item_type', (string)$item_type); + + if ($created_by) + { + $model->whereEquals('created_by', (int)$created_by); + } + + if ($ip) + { + $model->whereEquals('ip', $ip); + } + + return $model->order('created', 'desc')->row(); + } + + /** + * Check if a user has voted + * + * @param integer $item_type Item type + * @param integer $item_id Item ID + * @param integer $user_id User ID + * @param string $ip IP address + * @return integer + */ + public function hasVoted($item_type, $item_id, $user_id=null, $ip=null) + { + return self::oneByScope($item_type, $item_id, $user_id, $ip)->get('id'); + } +} diff --git a/core/libraries/Hubzero/Item/Watch.php b/core/libraries/Hubzero/Item/Watch.php new file mode 100644 index 00000000000..bbba80e527e --- /dev/null +++ b/core/libraries/Hubzero/Item/Watch.php @@ -0,0 +1,177 @@ + 'positive|nonzero', + 'item_type' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'created', + 'created_by' + ); + + /** + * Automatically fillable fields + * + * @var array + */ + public $always = array( + 'email', + 'item_type' + ); + + /** + * Generates automatic email field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticEmail($data) + { + if (!isset($data['email'])) + { + $data['email'] = User::get('email'); + } + + return $data['email']; + } + + /** + * Generates automatic email field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticItemType($data) + { + if (isset($data['item_type'])) + { + $data['item_type'] = strtolower(preg_replace("/[^a-zA-Z0-9\-]/", '', trim($data['item_type']))); + } + + return $data['item_type']; + } + + /** + * Defines a belongs to one relationship between article and user + * + * @return object + */ + public function creator() + { + return $this->belongsToOne('Hubzero\User\User', 'created_by'); + } + + /** + * Is user watching item? + * + * @param integer $item_id + * @param string $item_type + * @param integer $created_by + * @return boolean + */ + public static function isWatching($item_id, $item_type, $created_by) + { + if ($item_id && $item_type && $created_by) + { + $total = self::all() + ->whereEquals('state', 1) + ->whereEquals('created_by', (int)$created_by) + ->whereEquals('item_id', (int)$item_id) + ->whereEquals('item_type', (string)$item_type) + ->total(); + + if ($total) + { + return true; + } + } + + return false; + } + + /** + * Load a record by scope and scope ID + * + * @param integer $item_id + * @param string $item_type + * @param integer $created_by + * @param string $email + * @return object + */ + public static function oneByScope($item_id, $item_type, $created_by = 0, $email = null) + { + $model = self::all() + ->whereEquals('item_id', (int)$item_id) + ->whereEquals('item_type', (string)$item_type); + + if ($created_by) + { + $model->whereEquals('created_by', (int)$created_by); + } + + if ($email) + { + $model->whereEquals('email', (string)$email); + } + + return $model->row(); + } +} diff --git a/core/libraries/Hubzero/Language/Translator.php b/core/libraries/Hubzero/Language/Translator.php new file mode 100644 index 00000000000..fdbea759fbc --- /dev/null +++ b/core/libraries/Hubzero/Language/Translator.php @@ -0,0 +1,1553 @@ + null, + 'ignoredSearchWords' => null, + 'lowerLimitSearchWord' => null, + 'upperLimitSearchWord' => null, + 'searchDisplayedCharactersNumber' => null, + ); + + /** + * Constructor activating the default information of the language. + * + * @param string $lang The language + * @param boolean $debug Indicates if language debugging is enabled. + * @return void + */ + public function __construct($lang = null, $debug = false, $client = 'site') + { + $this->strings = array(); + + if ($lang == null) + { + $lang = $this->default; + } + + $this->client = $client; + $this->setLanguage($lang); + $this->setDebug($debug); + + // Client directories are all lowercase under PATH_APP + $filename = PATH_APP . "/bootstrap/" . strtolower($client) . "/language/overrides/$lang.override.ini"; + + if (file_exists($filename) && $contents = $this->parse($filename)) + { + if (is_array($contents)) + { + // Sort the underlying heap by key values to optimize merging + ksort($contents, SORT_STRING); + $this->override = $contents; + } + + unset($contents); + } + + // Look for a language specific localise class + $class = str_replace('-', '_', $lang . 'Localise'); + $paths = array(); + + // Client directories are all lowercase under PATH_APP (for now) + $paths[0] = PATH_APP . "/bootstrap/" . strtolower($client) . "/language/overrides/$lang.localise.php"; + $paths[1] = PATH_APP . "/bootstrap/" . strtolower($client) . "/language/$lang/$lang.localise.php"; + // Client directories are ucfirst (PSR-4) under PATH_CORE + $paths[2] = PATH_CORE . "/bootstrap/" . strtolower($client) . "/language/$lang/$lang.localise.php"; + $paths[3] = PATH_CORE . "/bootstrap/" . ucfirst($client) . "/language/$lang/$lang.localise.php"; + + ksort($paths); + $path = reset($paths); + + while (!class_exists($class) && $path) + { + if (file_exists($path)) + { + require_once $path; + } + $path = next($paths); + } + + if (class_exists($class)) + { + // Class exists. Try to find + // -a transliterate method, + // -a getPluralSuffixes method, + // -a getIgnoredSearchWords method + // -a getLowerLimitSearchWord method + // -a getUpperLimitSearchWord method + // -a getSearchDisplayCharactersNumber method + if (method_exists($class, 'transliterate')) + { + $this->transliterator = array($class, 'transliterate'); + } + + foreach ($this->callbacks as $callback) + { + $method = 'get' . ucfirst($callback); + + if (method_exists($class, $method)) + { + $this->callbacks[$callback] = array($class, $method); + } + } + } + + $this->load('', PATH_APP) || $this->load('', PATH_CORE); + } + + /** + * Returns a language object. + * + * @param string $lang The language to use. + * @param boolean $debug The debug mode. + * @return object The Language object. + */ + public static function getInstance($lang, $debug = false) + { + if (!isset(self::$languages[$lang . $debug])) + { + $language = new self($lang, $debug); + + self::$languages[$lang . $debug] = $language; + + // Check if Language was instantiated with a null $lang param; + // if so, retrieve the language code from the object and store + // the instance with the language code as well + if (is_null($lang)) + { + self::$languages[$language->getLanguage() . $debug] = $language; + } + } + + return self::$languages[$lang . $debug]; + } + + /** + * Translate function, mimics the php gettext (alias _) function. + * + * The function checks if $jsSafe is true, then if $interpretBackslashes is true. + * + * @param string $string The string to translate + * @param boolean $jsSafe Make the result javascript safe + * @param boolean $interpretBackSlashes Interpret \t and \n + * @return string The translation of the string + */ + public function translate($string, $jsSafe = false, $interpretBackSlashes = true) + { + // Detect empty string + if ($string == '') + { + return ''; + } + + $key = strtoupper($string); + + if (isset($this->strings[$key])) + { + $string = $this->debug ? '**' . $this->strings[$key] . '**' : $this->strings[$key]; + + // Store debug information + if ($this->debug) + { + $caller = $this->getCallerInfo(); + + if (!array_key_exists($key, $this->used)) + { + $this->used[$key] = array(); + } + + $this->used[$key][] = $caller; + } + } + else + { + if ($this->debug) + { + $caller = $this->getCallerInfo(); + $caller['string'] = $string; + + if (!array_key_exists($key, $this->orphans)) + { + $this->orphans[$key] = array(); + } + + $this->orphans[$key][] = $caller; + + $string = '??' . $string . '??'; + } + } + + if ($jsSafe) + { + // Javascript filter + $string = addslashes($string); + } + elseif ($interpretBackSlashes) + { + // Interpret \n and \t characters + $string = str_replace(array('\\\\', '\t', '\n'), array("\\", "\t", "\n"), $string); + } + + return $string; + } + + /** + * Transliterate function + * + * This method processes a string and replaces all accented UTF-8 characters by unaccented + * ASCII-7 "equivalents". + * + * @param string $string The string to transliterate. + * @return string The transliteration of the string. + */ + public function transliterate($string) + { + if ($this->transliterator !== null) + { + return call_user_func($this->transliterator, $string); + } + + $string = Latin::toAscii($string); + $string = strtolower($string); + + return $string; + } + + /** + * Getter for transliteration function + * + * @return string Function name or the actual function for PHP 5.3. + */ + public function getTransliterator() + { + return $this->transliterator; + } + + /** + * Set the transliteration function. + * + * @param mixed $function Function name (string) or the actual function for PHP 5.3 (function). + * @return mixed + */ + public function setTransliterator($function) + { + $this->transliterator = $function; + + return $this; + } + + /** + * Returns an array of suffixes for plural rules. + * + * @param integer $count The count number the rule is for. + * @return array The array of suffixes. + */ + public function getPluralSuffixes($count) + { + if ($this->callbacks['pluralSuffixes'] !== null) + { + return call_user_func($this->callbacks['pluralSuffixes'], $count); + } + else + { + return array((string) $count); + } + } + + /** + * Getter for pluralSuffixesCallback function. + * + * @return mixed Function name (string) or the actual function for PHP 5.3 (function). + */ + public function getPluralSuffixesCallback() + { + return $this->callbacks['pluralSuffixes']; + } + + /** + * Set the pluralSuffixes function. + * + * @param mixed $function Function name (string) or actual function for PHP 5.3 (function) + * @return mixed Function name or the actual function for PHP 5.3. + */ + public function setPluralSuffixesCallback($function) + { + $this->callbacks['pluralSuffixes'] = $function; + + return $this; + } + + /** + * Returns an array of ignored search words + * + * @return array The array of ignored search words. + */ + public function getIgnoredSearchWords() + { + if ($this->callbacks['ignoredSearchWords'] !== null) + { + return call_user_func($this->callbacks['ignoredSearchWords']); + } + + return array(); + } + + /** + * Getter for ignoredSearchWordsCallback function. + * + * @return mixed Function name (string) or the actual function for PHP 5.3 (function). + */ + public function getIgnoredSearchWordsCallback() + { + return $this->callbacks['ignoredSearchWords']; + } + + /** + * Setter for the ignoredSearchWordsCallback function + * + * @param mixed $function Function name (string) or actual function for PHP 5.3 (function) + * @return mixed Function name (string) or the actual function for PHP 5.3 (function) + */ + public function setIgnoredSearchWordsCallback($function) + { + $this->callbacks['ignoredSearchWords'] = $function; + + return $this; + } + + /** + * Returns a lower limit integer for length of search words + * + * @return integer The lower limit integer for length of search words (3 if no value was set for a specific language). + */ + public function getLowerLimitSearchWord() + { + if ($this->callbacks['lowerLimitSearchWord'] !== null) + { + return call_user_func($this->callbacks['lowerLimitSearchWord']); + } + + return 3; + } + + /** + * Getter for lowerLimitSearchWordCallback function + * + * @return mixed Function name (string) or the actual function for PHP 5.3 (function). + */ + public function getLowerLimitSearchWordCallback() + { + return $this->callbacks['lowerLimitSearchWord']; + } + + /** + * Setter for the lowerLimitSearchWordCallback function. + * + * @param mixed $function Function name (string) or actual function for PHP 5.3 (function) + * @return string|function Function name or the actual function for PHP 5.3. + */ + public function setLowerLimitSearchWordCallback($function) + { + $this->callbacks['lowerLimitSearchWord'] = $function; + + return $this; + } + + /** + * Returns an upper limit integer for length of search words + * + * @return integer The upper limit integer for length of search words (20 if no value was set for a specific language). + */ + public function getUpperLimitSearchWord() + { + if ($this->callbacks['upperLimitSearchWord'] !== null) + { + return call_user_func($this->callbacks['upperLimitSearchWord']); + } + + return 20; + } + + /** + * Getter for upperLimitSearchWordCallback function + * + * @return string|function Function name or the actual function for PHP 5.3. + */ + public function getUpperLimitSearchWordCallback() + { + return $this->callbacks['upperLimitSearchWord']; + } + + /** + * Setter for the upperLimitSearchWordCallback function + * + * @param string $function The name of the callback function. + * @return mixed Function name (string) or the actual function for PHP 5.3 (function). + */ + public function setUpperLimitSearchWordCallback($function) + { + $this->callbacks['upperLimitSearchWord'] = $function; + + return $this; + } + + /** + * Returns the number of characters displayed in search results. + * + * @return integer The number of characters displayed (200 if no value was set for a specific language). + */ + public function getSearchDisplayedCharactersNumber() + { + if ($this->callbacks['searchDisplayedCharactersNumber'] !== null) + { + return call_user_func($this->callbacks['searchDisplayedCharactersNumber']); + } + + return 200; + } + + /** + * Getter for searchDisplayedCharactersNumberCallback function + * + * @return mixed Function name or the actual function for PHP 5.3. + */ + public function getSearchDisplayedCharactersNumberCallback() + { + return $this->callbacks['searchDisplayedCharactersNumber']; + } + + /** + * Setter for the searchDisplayedCharactersNumberCallback function. + * + * @param string $function The name of the callback. + * @return mixed Function name (string) or the actual function for PHP 5.3 (function). + */ + public function setSearchDisplayedCharactersNumberCallback($function) + { + $this->callbacks['searchDisplayedCharactersNumber'] = $function; + + return $this; + } + + /** + * Checks if a language exists. + * + * This is a simple, quick check for the directory that should contain language files for the given user. + * + * @param string $lang Language to check. + * @param string $basePath Optional path to check. + * @return boolean True if the language exists. + */ + public static function exists($lang, $basePath = PATH_APP) + { + static $paths = array(); + + // Return false if no language was specified + if (!$lang) + { + return false; + } + + $path = $basePath . DS . 'language' . DS . $lang; + + // Return previous check results if it exists + if (isset($paths[$path])) + { + return $paths[$path]; + } + + // Check if the language exists + $paths[$path] = is_dir($path); + + return $paths[$path]; + } + + /** + * Loads a single language file and appends the results to the existing strings + * + * @param string $extension The extension for which a language file should be loaded. + * @param string $basePath The basepath to use. + * @param string $lang The language to load, default null for the current language. + * @param boolean $reload Flag that will force a language to be reloaded if set to true. + * @param boolean $default Flag that force the default language to be loaded if the current does not exist. + * @return boolean True if the file has successfully loaded. + */ + public function load($extension = 'hubzero', $basePath = PATH_APP, $lang = null, $reload = false, $default = true) + { + // Load the default language first if we're not debugging and a non-default language is requested to be loaded + // with $default set to true + if (!\App::get('config')->get('debug_lang') && ($lang != $this->default) && $default) + { + $this->load($extension, $basePath, $this->default, false, true); + } + + if (!$lang) + { + $lang = $this->lang; + } + + if ($basePath == PATH_APP || $basePath == PATH_CORE) + { + $basePath .= DS . 'bootstrap' . DS . $this->client; + } + + $path = self::getLanguagePath($basePath, $lang); + + $internal = $extension == 'hubzero' || $extension == ''; + $filename = $internal ? $lang : $lang . '.' . $extension; + $filename = "$path/$filename.ini"; + + $result = false; + + if (isset($this->paths[$extension][$filename]) && !$reload) + { + // This file has already been tested for loading. + $result = $this->paths[$extension][$filename]; + } + else + { + // Load the language file + $result = $this->loadLanguage($filename, $extension); + + // Check whether there was a problem with loading the file + if ($result === false && $default) + { + // No strings, so either file doesn't exist or the file is invalid + $oldFilename = $filename; + + // Check the standard file name + $path = self::getLanguagePath($basePath, $this->default); + $filename = $internal ? $this->default : $this->default . '.' . $extension; + $filename = "$path/$filename.ini"; + + // If the one we tried is different than the new name, try again + if ($oldFilename != $filename) + { + $result = $this->loadLanguage($filename, $extension, false); + } + } + } + + return $result; + } + + /** + * Loads a language file. + * + * This method will not note the successful loading of a file - use load() instead. + * + * @param string $filename The name of the file. + * @param string $extension The name of the extension. + * @return boolean True if new strings have been added to the language + */ + protected function loadLanguage($filename, $extension = 'unknown') + { + $this->counter++; + + $result = false; + $strings = false; + + if (file_exists($filename)) + { + $strings = $this->parse($filename); + } + + if ($strings) + { + if (is_array($strings)) + { + // Sort the underlying heap by key values to optimize merging + ksort($strings, SORT_STRING); + $this->strings = array_merge($this->strings, $strings); + } + + if (is_array($strings) && count($strings)) + { + // Do not bother with ksort here. Since the originals were sorted, PHP will already have chosen the best heap. + $this->strings = array_merge($this->strings, $this->override); + $result = true; + } + } + + // Record the result of loading the extension's file. + if (!isset($this->paths[$extension])) + { + $this->paths[$extension] = array(); + } + + $this->paths[$extension][$filename] = $result; + + return $result; + } + + /** + * Parses a language file. + * + * @param string $filename The name of the file. + * @return array The array of parsed strings. + */ + protected function parse($filename) + { + if ($this->debug) + { + // Capture hidden PHP errors from the parsing. + $php_errormsg = null; + $track_errors = ini_get('track_errors'); + ini_set('track_errors', true); + } + + $contents = file_get_contents($filename); + $contents = str_replace('_QQ_', '"\""', $contents); + $strings = @parse_ini_string($contents); + + if (!is_array($strings)) + { + $strings = array(); + } + + if ($this->debug) + { + // Restore error tracking to what it was before. + ini_set('track_errors', $track_errors); + + // Initialise variables for manually parsing the file for common errors. + $blacklist = array('YES', 'NO', 'NULL', 'FALSE', 'ON', 'OFF', 'NONE', 'TRUE'); + $regex = '/^(|(\[[^\]]*\])|([A-Z][A-Z0-9_\-\.]*\s*=(\s*(("[^"]*")|(_QQ_)))+))\s*(;.*)?$/'; + $this->debug = false; + $errors = array(); + + // Open the file as a stream. + $file = new \SplFileObject($filename); + + foreach ($file as $lineNumber => $line) + { + // Avoid BOM error as BOM is OK when using parse_ini + if ($lineNumber == 0) + { + $line = str_replace("\xEF\xBB\xBF", '', $line); + } + + // Check that the key is not in the blacklist and that the line format passes the regex. + $key = strtoupper(trim(substr($line, 0, strpos($line, '=')))); + + // Workaround to reduce regex complexity when matching escaped quotes + $line = str_replace('\"', '_QQ_', $line); + + if (!preg_match($regex, $line) || in_array($key, $blacklist)) + { + $errors[] = $lineNumber; + } + } + + // Check if we encountered any errors. + if (count($errors)) + { + $this->errorfiles[$filename] = $filename . ' : error(s) in line(s) ' . implode(', ', $errors); + } + elseif ($php_errormsg) + { + // We didn't find any errors but there's probably a parse notice. + $this->errorfiles['PHP' . $filename] = 'PHP parser errors :' . $php_errormsg; + } + + $this->debug = true; + } + + return $strings; + } + + /** + * Get a metadata language property. + * + * @param string $property The name of the property. + * @param mixed $default The default value. + * @return mixed The value of the property. + */ + public function get($property, $default = null) + { + if (isset($this->metadata[$property])) + { + return $this->metadata[$property]; + } + + return $default; + } + + /** + * Determine who called the translator. + * + * @return array Caller information. + */ + protected function getCallerInfo() + { + // Try to determine the source if none was provided + if (!function_exists('debug_backtrace')) + { + return null; + } + + $backtrace = debug_backtrace(); + $info = array(); + + // Search through the backtrace to our caller + $continue = true; + while ($continue && next($backtrace)) + { + $step = current($backtrace); + $class = @ $step['class']; + + // We're looking for something outside of language.php + if ($class != '\\Hubzero\\Language\\Translator' && $class != '\\Lang') + { + $info['function'] = @ $step['function']; + $info['class'] = $class; + $info['step'] = prev($backtrace); + + // Determine the file and name of the file + $info['file'] = @ $step['file']; + $info['line'] = @ $step['line']; + + $continue = false; + } + } + + return $info; + } + + /** + * Getter for Name. + * + * @return string Official name element of the language. + */ + public function getName() + { + return $this->metadata['name']; + } + + /** + * Get a list of language files that have been loaded. + * + * @param string $extension An optional extension name. + * @return array + */ + public function getPaths($extension = null) + { + if (isset($extension)) + { + if (isset($this->paths[$extension])) + { + return $this->paths[$extension]; + } + + return null; + } + + return $this->paths; + } + + /** + * Get a list of language files that are in error state. + * + * @return array + */ + public function getErrorFiles() + { + return $this->errorfiles; + } + + /** + * Getter for the language tag (as defined in RFC 3066) + * + * @return string The language tag. + */ + public function getTag() + { + return $this->metadata['tag']; + } + + /** + * Get the RTL property. + * + * @return boolean True is it an RTL language. + */ + public function isRTL() + { + return $this->metadata['rtl']; + } + + /** + * Set the Debug property. + * + * @param boolean $debug The debug setting. + * @return boolean Previous value. + */ + public function setDebug($debug) + { + $this->debug = (boolean) $debug; + + return $this; + } + + /** + * Get the Debug property. + * + * @return boolean True is in debug mode. + */ + public function getDebug() + { + return $this->debug; + } + + /** + * Get the default language code. + * + * @return string Language code. + */ + public function getDefault() + { + return $this->default; + } + + /** + * Set the default language code. + * + * @param string $lang The language code. + * @return string Previous value. + */ + public function setDefault($lang) + { + $this->default = $lang; + + return $this; + } + + /** + * Get the list of orphaned strings if being tracked. + * + * @return array Orphaned text. + */ + public function getOrphans() + { + return $this->orphans; + } + + /** + * Get the list of used strings. + * + * Used strings are those strings requested and found either as a string or a constant. + * + * @return array Used strings. + */ + public function getUsed() + { + return $this->used; + } + + /** + * Determines is a key exists. + * + * @param string $string The key to check. + * @return boolean True, if the key exists. + */ + public function hasKey($string) + { + $key = strtoupper($string); + + return isset($this->strings[$key]); + } + + /** + * Returns a associative array holding the metadata. + * + * @param string $lang The name of the language. + * @return mixed If $lang exists return key/value pair with the language metadata, otherwise return NULL. + */ + public static function getMetadata($lang) + { + $path = self::getLanguagePath(PATH_APP . DS . 'bootstrap' . DS . \App::get('client')->name, $lang); + $file = $lang . '.xml'; + + $result = null; + + if (!is_file("$path/$file")) + { + $path = self::getLanguagePath(PATH_CORE . DS . 'bootstrap' . DS . ucfirst(\App::get('client')->name), $lang); + } + + if (is_file("$path/$file")) + { + $result = self::parseXMLLanguageFile("$path/$file"); + } + + if (empty($result)) + { + return null; + } + + return $result; + } + + /** + * Returns a list of known languages for an area + * + * @param string $basePath The basepath to use + * @return array key/value pair with the language file and real name. + */ + public static function getKnownLanguages($basePath = PATH_APP) + { + $dir = self::getLanguagePath($basePath); + $knownLanguages = self::parseLanguageFiles($dir); + + return $knownLanguages; + } + + /** + * Get the path to a language + * + * @param string $basePath The basepath to use. + * @param string $language The language tag. + * @return string language related path or null. + */ + public static function getLanguagePath($basePath = PATH_APP, $language = null) + { + $dir = $basePath . DS . 'language'; + + if (!empty($language)) + { + $dir .= DS . $language; + } + + return $dir; + } + + /** + * Get the current language code. + * + * @return string The language code + */ + public function getLanguage() + { + return $this->lang; + } + + /** + * Set the language attributes to the given language. + * + * Once called, the language still needs to be loaded using JLanguage::load(). + * + * @param string $lang Language code. + * @return string Previous value. + */ + public function setLanguage($lang) + { + $this->lang = $lang; + $this->metadata = $this->getMetadata($this->lang); + + return $this; + } + + /** + * Get the language locale based on current language. + * + * @return array The locale according to the language. + */ + public function getLocale() + { + if (!isset($this->locale)) + { + $locale = str_replace(' ', '', isset($this->metadata['locale']) ? $this->metadata['locale'] : ''); + + if ($locale) + { + $this->locale = explode(',', $locale); + } + else + { + $this->locale = false; + } + } + + return $this->locale; + } + + /** + * Get the first day of the week for this language. + * + * @return integer The first day of the week according to the language + */ + public function getFirstDay() + { + return (int) (isset($this->metadata['firstDay']) ? $this->metadata['firstDay'] : 0); + } + + /** + * Searches for language directories within a certain base dir. + * + * @param string $dir directory of files. + * @return array Array holding the found languages as filename => real name pairs. + */ + public static function parseLanguageFiles($dir = null) + { + $languages = array(); + + if (is_dir($dir)) + { + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)); + + foreach ($iterator as $file) + { + $langs = array(); + $fileName = $file->getFilename(); + + if (!$file->isFile() || !preg_match("/^([-_A-Za-z]*)\.xml$/", $fileName)) + { + continue; + } + + try + { + $metadata = self::parseXMLLanguageFile($file->getRealPath()); + + if ($metadata) + { + $lang = str_replace('.xml', '', $fileName); + $langs[$lang] = $metadata; + } + + $languages = array_merge($languages, $langs); + } + catch (\RuntimeException $e) + { + } + } + } + + return $languages; + } + + /** + * Parse XML file for language information. + * + * @param string $path Path to the XML files. + * @return array Array holding the found metadata as a key => value pair. + */ + public static function parseXMLLanguageFile($path) + { + if (!is_readable($path)) + { + throw new \RuntimeException('File not found or not readable'); + } + + // Try to load the file + $xml = simplexml_load_file($path); + + if (!$xml) + { + return null; + } + + // Check that it's a metadata file + if ((string) $xml->getName() != 'metafile') + { + return null; + } + + $metadata = array(); + + foreach ($xml->metadata->children() as $child) + { + $metadata[$child->getName()] = (string) $child; + } + + return $metadata; + } + + /** + * Tries to detect the language. + * + * @return string locale or null if not found + */ + public function detect() + { + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) + { + $browserLangs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); + $systemLangs = $this->available(); + foreach ($browserLangs as $browserLang) + { + // Slice out the part before ; on first step, the part before - on second, place into array + $browserLang = substr($browserLang, 0, strcspn($browserLang, ';')); + $primary_browserLang = substr($browserLang, 0, 2); + foreach ($systemLangs as $systemLang) + { + // Take off 3 letters iso code languages as they can't match browsers' languages and default them to en + $Jinstall_lang = $systemLang->lang_code; + + if (strlen($Jinstall_lang) < 6) + { + if (strtolower($browserLang) == strtolower(substr($systemLang->lang_code, 0, strlen($browserLang)))) + { + return $systemLang->lang_code; + } + elseif ($primary_browserLang == substr($systemLang->lang_code, 0, 2)) + { + $primaryDetectedLang = $systemLang->lang_code; + } + } + } + + if (isset($primaryDetectedLang)) + { + return $primaryDetectedLang; + } + } + } + + return null; + } + + /** + * Get available languages + * + * @param string $key Array key + * @return array An array of published languages + */ + public function available($key = 'default') + { + static $languages; + + if (empty($languages)) + { + // Installation uses available languages + if (\App::get('client')->id == 2) + { + $languages[$key] = array(); + $knownLangs = self::getKnownLanguages(PATH_APP . DS . 'bootstrap' . DS . $this->client); + foreach ($knownLangs as $metadata) + { + // Take off 3 letters iso code languages as they can't match browsers' languages and default them to en + $languages[$key][] = new Object(array('lang_code' => $metadata['tag'])); + } + } + else + { + $cache = \App::get('cache.store'); + if (!$languages = $cache->get('com_languages.languages')) + { + $db = \App::get('db'); + $query = $db->getQuery() + ->select('*') + ->from('#__languages') + ->whereEquals('published', 1) + ->order('ordering', 'asc'); + $db->setQuery($query->toString()); + + $languages['default'] = $db->loadObjectList(); + $languages['sef'] = array(); + $languages['lang_code'] = array(); + + if (isset($languages['default'][0])) + { + foreach ($languages['default'] as $lang) + { + $languages['sef'][$lang->sef] = $lang; + $languages['lang_code'][$lang->lang_code] = $lang; + } + } + + $cache->put('com_languages.languages', $languages, \App::get('config')->get('cachetime', 15)); + } + } + } + + return $languages[$key]; + } + + /** + * Builds a list of the system languages which can be used in a select option + * + * @param string $actualLanguage Client key for the area + * @param string $basePath Base path to use + * @param boolean $caching True if caching is used + * @param array $installed An array of arrays (text, value, selected) + * @param integer $client Client ID + * @return array List of system languages + */ + public static function getList($actualLanguage, $basePath = PATH_APP, $caching = false, $installed = false, $client = null) + { + $list = array(); + + $langs = self::getKnownLanguages($basePath); + + if ($installed) + { + $db = \App::get('db'); + $query = $db->getQuery() + ->select('element') + ->from('#__extensions') + ->whereEquals('type', 'language') + ->whereEquals('state', 0) + ->whereEquals('enabled', 1) + ->whereEquals('client_id', (is_null($client) ? \App::get('client')->id : (int)$client)); + $db->setQuery($query->toString()); + $installed_languages = $db->loadObjectList('element'); + } + + foreach ($langs as $lang => $metadata) + { + if (!$installed || array_key_exists($lang, $installed_languages)) + { + $option = array(); + $option['text'] = $metadata['name']; + $option['value'] = $lang; + if ($lang == $actualLanguage) + { + $option['selected'] = 'selected="selected"'; + } + + $list[] = $option; + } + } + + return $list; + } + + /** + * Translates a string into the current language. + * + * @param string $string The string to translate. + * @return string The translated string or the key is $script is true + */ + public function txt($string) + { + $args = func_get_args(); + $count = count($args); + + if ($count > 1) + { + if ($count == 2 && is_bool($args[1])) + { + return $this->translate($string, $args[1]); + } + + if ($count == 3 && is_bool($args[1]) && is_bool($args[2])) + { + return $this->translate($string, $args[1], $args[2]); + } + + if (is_array($args[$count - 1])) + { + $args[0] = $this->translate( + $string, array_key_exists('jsSafe', $args[$count - 1]) ? $args[$count - 1]['jsSafe'] : false, + array_key_exists('interpretBackSlashes', $args[$count - 1]) ? $args[$count - 1]['interpretBackSlashes'] : true + ); + } + else + { + $args[0] = $this->translate($string); + } + $args[0] = preg_replace('/\[\[%([0-9]+):[^\]]*\]\]/', '%\1$s', $args[0]); + + return call_user_func_array('sprintf', $args); + } + + return $this->translate($string); + } + + /** + * Translates a string into the current language. + * + * @param string $string The format string. + * @param integer $n The number of items + * @return string The translated string or the key is $script is true + */ + public function txts($string, $n) + { + $args = func_get_args(); + $count = count($args); + + if ($count > 1) + { + // Try the key from the language plural potential suffixes + $found = false; + $suffixes = $this->getPluralSuffixes((int) $n); + array_unshift($suffixes, (int) $n); + foreach ($suffixes as $suffix) + { + $key = $string . '_' . $suffix; + if ($this->hasKey($key)) + { + $found = true; + break; + } + } + if (!$found) + { + // Not found so revert to the original. + $key = $string; + } + if (is_array($args[$count - 1])) + { + $args[0] = $this->translate( + $key, array_key_exists('jsSafe', $args[$count - 1]) ? $args[$count - 1]['jsSafe'] : false, + array_key_exists('interpretBackSlashes', $args[$count - 1]) ? $args[$count - 1]['interpretBackSlashes'] : true + ); + } + else + { + $args[0] = $this->translate($key); + } + return call_user_func_array('sprintf', $args); + } + elseif ($count > 0) + { + + // Default to the normal sprintf handling. + $args[0] = $this->translate($string); + return call_user_func_array('sprintf', $args); + } + + return ''; + } + + /** + * Translates a string into the current language. + * + * Examples: + * it will generate a 'All' string in English but a "Toutes" string in French + * it will generate a 'All' string in English but a "Tous" string in French + * + * @param string $string The string to translate. + * @param string $alt The alternate option for global string + * @param mixed $jsSafe Boolean: Make the result javascript safe. + * @param boolean $interpretBackSlashes To interpret backslashes (\\=\, \n=carriage return, \t=tabulation) + * @return string The translated string or the key if $script is true + */ + public function alt($string, $alt, $jsSafe = false, $interpretBackSlashes = true) + { + if ($this->hasKey($string . '_' . $alt)) + { + $string = $string . '_' . $alt; + } + + return $this->txt($string, $jsSafe, $interpretBackSlashes); + } + + /** + * Method to determine if the language filter plugin is enabled. + * This works for both site and administrator. + * + * @return boolean True if site is supporting multiple languages; false otherwise. + */ + public function isMultilang() + { + // Flag to avoid doing multiple database queries. + static $tested = false; + + // Status of language filter plugin. + static $enabled = false; + + // If being called from the front-end, we can avoid the database query. + if (\App::isSite()) + { + return \App::get('language.filter'); + } + + // If already tested, don't test again. + if (!$tested) + { + // Determine status of language filter plug-in. + $db = \App::get('db'); + $query = $db->getQuery() + ->select('enabled') + ->from('#__extensions') + ->whereEquals('type', 'plugin') + ->whereEquals('folder', 'system') + ->whereEquals('element', 'languagefilter'); + $db->setQuery($query->toString()); + + $enabled = $db->loadResult(); + $tested = true; + } + + return $enabled; + } + + /** + * Translate a string into the current language and stores it in the JavaScript language store. + * + * @param string $string The language key. + * @param boolean $jsSafe Ensure the output is JavaScript safe. + * @param boolean $interpretBackSlashes Interpret \t and \n. + * @return mixed + */ + public function script($string = null, $jsSafe = false, $interpretBackSlashes = true) + { + if (is_array($jsSafe)) + { + if (array_key_exists('interpretBackSlashes', $jsSafe)) + { + $interpretBackSlashes = (boolean) $jsSafe['interpretBackSlashes']; + } + + if (array_key_exists('jsSafe', $jsSafe)) + { + $jsSafe = (boolean) $jsSafe['jsSafe']; + } + else + { + $jsSafe = false; + } + } + + // Add the string to the array if not null. + if ($string !== null) + { + // Normalize the key and translate the string. + self::$jsStrings[strtoupper($string)] = $this->translate($string, $jsSafe, $interpretBackSlashes); + } + + return self::$jsStrings; + } +} diff --git a/core/libraries/Hubzero/Language/Transliterate/Latin.php b/core/libraries/Hubzero/Language/Transliterate/Latin.php new file mode 100644 index 00000000000..6befcb660ab --- /dev/null +++ b/core/libraries/Hubzero/Language/Transliterate/Latin.php @@ -0,0 +1,258 @@ + 'a', + 'ô' => 'o', + 'ď' => 'd', + 'ḟ' => 'f', + 'ë' => 'e', + 'š' => 's', + 'ơ' => 'o', + 'ß' => 'ss', + 'ă' => 'a', + 'ř' => 'r', + 'ț' => 't', + 'ň' => 'n', + 'ā' => 'a', + 'ķ' => 'k', + 'ŝ' => 's', + 'ỳ' => 'y', + 'ņ' => 'n', + 'ĺ' => 'l', + 'ħ' => 'h', + 'ṗ' => 'p', + 'ó' => 'o', + 'ú' => 'u', + 'ě' => 'e', + 'é' => 'e', + 'ç' => 'c', + 'ẁ' => 'w', + 'ċ' => 'c', + 'õ' => 'o', + 'ṡ' => 's', + 'ø' => 'o', + 'ģ' => 'g', + 'ŧ' => 't', + 'ș' => 's', + 'ė' => 'e', + 'ĉ' => 'c', + 'ś' => 's', + 'î' => 'i', + 'ű' => 'u', + 'ć' => 'c', + 'ę' => 'e', + 'ŵ' => 'w', + 'ṫ' => 't', + 'ū' => 'u', + 'č' => 'c', + 'ö' => 'oe', + 'è' => 'e', + 'ŷ' => 'y', + 'ą' => 'a', + 'ł' => 'l', + 'ų' => 'u', + 'ů' => 'u', + 'ş' => 's', + 'ğ' => 'g', + 'ļ' => 'l', + 'ƒ' => 'f', + 'ž' => 'z', + 'ẃ' => 'w', + 'ḃ' => 'b', + 'å' => 'a', + 'ì' => 'i', + 'ï' => 'i', + 'ḋ' => 'd', + 'ť' => 't', + 'ŗ' => 'r', + 'ä' => 'ae', + 'í' => 'i', + 'ŕ' => 'r', + 'ê' => 'e', + 'ü' => 'ue', + 'ò' => 'o', + 'ē' => 'e', + 'ñ' => 'n', + 'ń' => 'n', + 'ĥ' => 'h', + 'ĝ' => 'g', + 'đ' => 'd', + 'ĵ' => 'j', + 'ÿ' => 'y', + 'ũ' => 'u', + 'ŭ' => 'u', + 'ư' => 'u', + 'ţ' => 't', + 'ý' => 'y', + 'ő' => 'o', + 'â' => 'a', + 'ľ' => 'l', + 'ẅ' => 'w', + 'ż' => 'z', + 'ī' => 'i', + 'ã' => 'a', + 'ġ' => 'g', + 'ṁ' => 'm', + 'ō' => 'o', + 'ĩ' => 'i', + 'ù' => 'u', + 'į' => 'i', + 'ź' => 'z', + 'á' => 'a', + 'û' => 'u', + 'þ' => 'th', + 'ð' => 'dh', + 'æ' => 'ae', + 'µ' => 'u', + 'ĕ' => 'e', + 'œ' => 'oe' + ); + } + + $string = str_replace(array_keys($lower), array_values($lower), $string); + } + + if ($case >= 0) + { + if (is_null($upper)) + { + $upper = array( + 'À' => 'A', + 'Ô' => 'O', + 'Ď' => 'D', + 'Ḟ' => 'F', + 'Ë' => 'E', + 'Š' => 'S', + 'Ơ' => 'O', + 'Ă' => 'A', + 'Ř' => 'R', + 'Ț' => 'T', + 'Ň' => 'N', + 'Ā' => 'A', + 'Ķ' => 'K', + 'Ŝ' => 'S', + 'Ỳ' => 'Y', + 'Ņ' => 'N', + 'Ĺ' => 'L', + 'Ħ' => 'H', + 'Ṗ' => 'P', + 'Ó' => 'O', + 'Ú' => 'U', + 'Ě' => 'E', + 'É' => 'E', + 'Ç' => 'C', + 'Ẁ' => 'W', + 'Ċ' => 'C', + 'Õ' => 'O', + 'Ṡ' => 'S', + 'Ø' => 'O', + 'Ģ' => 'G', + 'Ŧ' => 'T', + 'Ș' => 'S', + 'Ė' => 'E', + 'Ĉ' => 'C', + 'Ś' => 'S', + 'Î' => 'I', + 'Ű' => 'U', + 'Ć' => 'C', + 'Ę' => 'E', + 'Ŵ' => 'W', + 'Ṫ' => 'T', + 'Ū' => 'U', + 'Č' => 'C', + 'Ö' => 'Oe', + 'È' => 'E', + 'Ŷ' => 'Y', + 'Ą' => 'A', + 'Ł' => 'L', + 'Ų' => 'U', + 'Ů' => 'U', + 'Ş' => 'S', + 'Ğ' => 'G', + 'Ļ' => 'L', + 'Ƒ' => 'F', + 'Ž' => 'Z', + 'Ẃ' => 'W', + 'Ḃ' => 'B', + 'Å' => 'A', + 'Ì' => 'I', + 'Ï' => 'I', + 'Ḋ' => 'D', + 'Ť' => 'T', + 'Ŗ' => 'R', + 'Ä' => 'Ae', + 'Í' => 'I', + 'Ŕ' => 'R', + 'Ê' => 'E', + 'Ü' => 'Ue', + 'Ò' => 'O', + 'Ē' => 'E', + 'Ñ' => 'N', + 'Ń' => 'N', + 'Ĥ' => 'H', + 'Ĝ' => 'G', + 'Đ' => 'D', + 'Ĵ' => 'J', + 'Ÿ' => 'Y', + 'Ũ' => 'U', + 'Ŭ' => 'U', + 'Ư' => 'U', + 'Ţ' => 'T', + 'Ý' => 'Y', + 'Ő' => 'O', + 'Â' => 'A', + 'Ľ' => 'L', + 'Ẅ' => 'W', + 'Ż' => 'Z', + 'Ī' => 'I', + 'Ã' => 'A', + 'Ġ' => 'G', + 'Ṁ' => 'M', + 'Ō' => 'O', + 'Ĩ' => 'I', + 'Ù' => 'U', + 'Į' => 'I', + 'Ź' => 'Z', + 'Á' => 'A', + 'Û' => 'U', + 'Þ' => 'Th', + 'Ð' => 'Dh', + 'Æ' => 'Ae', + 'Ĕ' => 'E', + 'Œ' => 'Oe' + ); + } + $string = str_replace(array_keys($upper), array_values($upper), $string); + } + + return $string; + } +} diff --git a/core/libraries/Hubzero/Log/Manager.php b/core/libraries/Hubzero/Log/Manager.php new file mode 100644 index 00000000000..0b46614c17d --- /dev/null +++ b/core/libraries/Hubzero/Log/Manager.php @@ -0,0 +1,180 @@ + '', + 'file' => '', + 'format' => '', + 'level' => 'debug', + 'dateFormat' => 'Y-m-d H:i:s', + 'permissions' => 0640, + 'dispatcher' => null + ); + + /** + * Create a new manager instance. + * + * @param string $path + * @return void + */ + public function __construct($path = null) + { + if ($path) + { + $this->defaults['path'] = (string) $path; + } + } + + /** + * Get the default driver name. + * + * @return string + */ + public function getDefaultLog() + { + return 'debug'; + } + + /** + * Get a logger instance. + * + * @param string $name + * @return mixed + */ + public function logger($name = null) + { + $name = $name ?: $this->getDefaultLog(); + + // If the given driver has not been created before, we will create the instances + // here and cache it so we can return it next time very quickly. If there is + // already a driver created by this name, we'll just return that instance. + if (! isset($this->loggers[$name])) + { + $this->loggers[$name] = $this->createLog($name); + } + + return $this->loggers[$name]; + } + + /** + * Check if a logger exists + * + * @param string $name + * @return boolean + */ + public function has($name = null) + { + return isset($this->setup[$name]); + } + + /** + * Create a new log instance. + * + * @param string $name + * @return mixed + * @throws \InvalidArgumentException + */ + protected function createLog($name) + { + if (isset($this->setup[$name])) + { + $config = array_merge($this->defaults, $this->setup[$name]); + + if (!$config['path']) + { + throw new InvalidArgumentException("Log path not specified for [$name]."); + } + + if (!$config['file']) + { + throw new InvalidArgumentException("Log file name not specified for [$name]."); + } + + $log = new Writer( + new Monolog($name), + $config['dispatcher'] + ); + + $log->useFiles( + $config['path'] . DIRECTORY_SEPARATOR . $config['file'], + $config['level'], + $config['format'], + $config['dateFormat'], + $config['permissions'] + ); + + return $log; + } + + throw new InvalidArgumentException("Log [$name] has no configuration values."); + } + + /** + * Register a custom logger. + * + * @param string $name + * @param array $settings + * @return $this + */ + public function register($name, $settings) + { + $this->setup[$name] = (array) $settings; + + return $this; + } + + /** + * Get all of the created "loggers". + * + * @return array + */ + public function getLoggers() + { + return $this->loggers; + } + + /** + * Dynamically call the default log instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return call_user_func_array(array($this->logger(), $method), $parameters); + } +} diff --git a/core/libraries/Hubzero/Log/Writer.php b/core/libraries/Hubzero/Log/Writer.php new file mode 100644 index 00000000000..becb6b2594f --- /dev/null +++ b/core/libraries/Hubzero/Log/Writer.php @@ -0,0 +1,254 @@ +monolog = $monolog; + + if (isset($dispatcher)) + { + $this->dispatcher = $dispatcher; + } + } + + /** + * Call Monolog with the given method and parameters. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + protected function callMonolog($method, $parameters) + { + if (is_array($parameters[0])) + { + $parameters[0] = json_encode($parameters[0]); + } + + return call_user_func_array(array($this->monolog, $method), $parameters); + } + + /** + * Register a file log handler. + * + * @param string $path + * @param string $level + * @param string $format + * @return void + */ + public function useFiles($path, $level = 'debug', $format='', $dateFormat = 'Y-m-d H:i:s', $permissions=null) + { + $level = $this->parseLevel($level); + + $handler = new StreamHandler($path, $level, true, $permissions); + if ($format) + { + $handler->setFormatter(new LineFormatter($format, $dateFormat)); + } + + $this->monolog->pushHandler($handler); + } + + /** + * Register a daily file log handler. + * + * @param string $path + * @param int $days + * @param string $level + * @param string $format + * @return void + */ + public function useDailyFiles($path, $days = 0, $level = 'debug', $format='', $dateFormat = 'Y-m-d H:i:s', $permissions=null) + { + $level = $this->parseLevel($level); + + $handler = new RotatingFileHandler($path, $days, $level, true, $permissions); + if ($format) + { + $handler->setFormatter(new LineFormatter($format, $dateFormat)); + } + + $this->monolog->pushHandler($handler); + } + + /** + * Parse the string level into a Monolog constant. + * + * @param string $level + * @return int + */ + protected function parseLevel($level) + { + switch (strtolower($level)) + { + case 'debug': + return MonologLogger::DEBUG; + + case 'info': + return MonologLogger::INFO; + + case 'notice': + return MonologLogger::NOTICE; + + case 'warning': + return MonologLogger::WARNING; + + case 'error': + return MonologLogger::ERROR; + + case 'critical': + return MonologLogger::CRITICAL; + + case 'alert': + return MonologLogger::ALERT; + + case 'emergency': + return MonologLogger::EMERGENCY; + + default: + throw new \InvalidArgumentException('Invalid log level.'); + } + } + + /** + * Register a new callback handler for when + * a log event is triggered. + * + * @param object $handler Closure + * @return void + */ + public function listen($handler) + { + if (!($this->dispatcher instanceof DispatcherInterface)) + { + throw new \RuntimeException('Event dispatcher has not been set.'); + } + + $this->dispatcher->addListener($handler, 'onLog'); + } + + /** + * Get the underlying Monolog instance. + * + * @return object + */ + public function getMonolog() + { + return $this->monolog; + } + + /** + * Get the event dispatcher instance. + * + * @return mixed object or null + */ + public function getEventDispatcher() + { + return $this->dispatcher; + } + + /** + * Set the event dispatcher instance. + * + * @param object $dispatcher + * @return void + */ + public function setEventDispatcher($dispatcher) + { + $this->dispatcher = $dispatcher; + } + + /** + * Fires a log event. + * + * @param string $level + * @param string $message + * @param array $context + * @return void + */ + protected function triggerLogEvent($level, $message, array $context = array()) + { + // If the event dispatcher is set, we will pass along the parameters to the + // log listeners. These are useful for building profilers or other tools + // that aggregate all of the log messages for a given "request" cycle. + if ($this->dispatcher instanceof DispatcherInterface) + { + $this->dispatcher->trigger('onLog', array($level, $message, $context)); + } + } + + /** + * Dynamically handle error additions. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + if (in_array($method, $this->levels)) + { + call_user_func_array(array($this, 'triggerLogEvent'), array_merge(array($method), $parameters)); + + $method = 'add' . ucfirst($method); + + return $this->callMonolog($method, $parameters); + } + + throw new \BadMethodCallException(sprintf('Method [%s] does not exist.', $method)); + } +} diff --git a/core/libraries/Hubzero/Mail/Message.php b/core/libraries/Hubzero/Mail/Message.php new file mode 100644 index 00000000000..7d47352910f --- /dev/null +++ b/core/libraries/Hubzero/Mail/Message.php @@ -0,0 +1,299 @@ +getHeaders()->addTextHeader($headerFieldNameOrLine, $fieldValue); + return $this; + } + + /** + * Set the priority of this message. + * The value is an integer where 1 is the highest priority and 5 is the lowest. + * + * Modified version to also accept a string $message->setPriority('high'); + * + * @param mixed $priority integer|string + * @return object + */ + public function setPriority($priority) + { + if (is_string($priority)) + { + switch (strtolower($priority)) + { + case 'high': + $priority = 1; + break; + + case 'normal': + $priority = 3; + break; + + case 'low': + $priority = 5; + break; + + default: + $priority = 3; + break; + } + } + return parent::setPriority($priority); + } + + /** + * Send the message + * + * @return object + */ + public function send($transporter='', $options=array()) + { + $transporter = $transporter ? $transporter : \Config::get('mailer'); + + if (is_object($transporter) && ($transporter instanceof \Swift_Transport)) + { + // We were given a valid tranport mechanisms, so just use it + $transport = $transporter; + } + elseif (is_string($transporter) && self::hasTrasporter($transporter)) + { + $transport = self::getTrasporter($transporter); + } + else + { + switch (strtolower($transporter)) + { + case 'smtp': + if (!isset($options['host'])) + { + $options['host'] = \Config::get('smtphost'); + } + if (!isset($options['port'])) + { + $options['port'] = \Config::get('smtpport'); + } + if (!isset($options['username'])) + { + $options['username'] = \Config::get('smtpuser'); + } + if (!isset($options['password'])) + { + $options['password'] = \Config::get('smtppass'); + } + + if (!empty($options)) + { + $transport = \Swift_SmtpTransport::newInstance($options['host'], $options['port']); + $transport->setUsername($options['username']) + ->setPassword($options['password']); + } + break; + + case 'sendmail': + if (!isset($options['command'])) + { + $options['command'] = '/usr/sbin/exim -bs'; + } + $transport = \Swift_SendmailTransport::newInstance($options['command']); + break; + + case 'mail': + default: + $transport = \Swift_MailTransport::newInstance(); + //set mail additional args (mail return path - used for bounces) + //$transport->setExtraParams('-f hubmail-bounces@' . $_SERVER['HTTP_HOST']); + break; + } + + if (!($transport instanceof \Swift_Transport)) + { + throw new \InvalidArgumentException('Invalid transport specified'); + } + } + + $mailer = \Swift_Mailer::newInstance($transport); + $result = $mailer->send($this, $this->_failures); + + if ($result) + { + \Log::info(sprintf('Mail sent to %s', json_encode($this->getTo()))); + } + else + { + \Log::error(sprintf('Failed to mail %s', json_encode($this->getTo()))); + } + + return $result; + } + + /** + * Get the list of failed email addresses + * + * @return array|null + */ + public function getFailures() + { + return $this->_failures; + } + + /** + * Generates email token + * + * @param integer $user_id User ID + * @param integer $object_id Object ID + * @return string + */ + public function buildToken($user_id, $object_id) + { + $encryptor = new Token(); + return $encryptor->buildEmailToken(1, 1, $user_id, $object_id); + } + + /** + * Add an attachment + * + * @param mixed $attachment File path (string) or object (Swift_Mime_MimeEntity) + * @param string $filename Optional filename to set + * @return object + */ + public function addAttachment($attachment, $filename=null) + { + if (!($attachment instanceof Swift_Mime_MimeEntity)) + { + $attachment = \Swift_Attachment::fromPath($attachment); + } + + if ($filename && is_string($filename)) + { + $attachment->setFilename($filename); + } + + return $this->attach($attachment); + } + + /** + * Remove an attachment + * + * @param mixed $attachment File path (string) or object (Swift_Mime_MimeEntity) + * @return object + */ + public function removeAttachment($attachment) + { + if (!($attachment instanceof Swift_Mime_MimeEntity)) + { + $attachment = \Swift_Attachment::fromPath($attachment); + } + + return $this->detach($attachment); + } + + /** + * Get an embed string for an attachment + * + * @param mixed $attachment File path (string) or object (Swift_Image) + * @return object + */ + public function getEmbed($attachment) + { + if (!($attachment instanceof \Swift_Image)) + { + $attachment = \Swift_Image::fromPath($attachment); + } + + return $this->embed($attachment); + } + + /** + * Sets tags on the message + * + * @param array $tags The tags to set + * @return void + */ + public function setTags($tags) + { + $this->_tags = $tags; + } + + /** + * Grabs the message tags + * + * @return array + */ + public function getTags() + { + return $this->_tags; + } + + /** + * Adds a transport mechanisms to the known list + * + * @param string $name the mechanism name + * @param object $transporter the transporter object + * @return void + */ + public static function addTransporter($name, $transporter) + { + self::$_transporters[$name] = $transporter; + } + + /** + * Checks to see if a transporter by the given name exists + * + * @param string $name The transporter name + * @return bool + */ + public static function hasTrasporter($name) + { + return isset(self::$_transporters[$name]); + } + + /** + * Gets the named transporter + * + * @param string $name The transporter name + * @return object + */ + public static function getTrasporter($name) + { + return self::$_transporters[$name]; + } +} diff --git a/core/libraries/Hubzero/Mail/Template.php b/core/libraries/Hubzero/Mail/Template.php new file mode 100644 index 00000000000..63962649621 --- /dev/null +++ b/core/libraries/Hubzero/Mail/Template.php @@ -0,0 +1,120 @@ +setQuery("SELECT s.`template`, e.protected FROM `#__template_styles` AS s INNER JOIN `#__extensions` AS e ON e.`element`=s.`template` WHERE s.`client_id`=0 AND s.`home`=1"); + $result = $db->loadObject(); + } + else + { + $result = App::get('template'); + } + + $params['template'] = $result->template; + if (is_dir(PATH_APP . DS . 'templates' . DS . $result->template)) + { + $params['directory'] = PATH_APP . DS . 'templates'; + } + else + { + $params['directory'] = PATH_CORE . DS . 'templates'; + } + } + + if (!isset($params['file'])) + { + $params['file'] = 'email.php'; + } + + if (!file_exists($params['directory'] . DS . $params['template'] . DS . $params['file'])) + { + $params['template'] = 'system'; + $params['directory'] = PATH_CORE . DS . 'templates'; + } + + $this->_caching = $caching; + + if (!empty($this->_template)) + { + $data = $this->_renderTemplate(); + } + else + { + $this->parse($params); + $data = $this->_renderTemplate(); + } + + if (class_exists('\Pelago\Emogrifier') && $data) + { + $data = str_replace('&#', '{_ANDNUM_}', $data); + $emogrifier = new \Pelago\Emogrifier(); + $emogrifier->preserveEncoding = true; + $emogrifier->setHtml($data); + //$emogrifier->setCss($css); + + $data = $emogrifier->emogrify(); + $data = str_replace('{_ANDNUM_}', '&#', $data); + } + + return $data; + } + + /** + * Load a template file + * + * [!] Overloaded to remove automatic favicon injection + * + * @param string $directory The name of the template + * @param string $filename The actual filename + * @return string The contents of the template + */ + protected function _loadTemplate($directory, $filename) + { + $contents = ''; + + // Check to see if we have a valid template file + if (file_exists($directory . DS . $filename)) + { + // Store the file path + $this->_file = $directory . DS . $filename; + + // Get the file content + ob_start(); + require $directory . DS . $filename; + $contents = ob_get_contents(); + ob_end_clean(); + } + + return $contents; + } +} diff --git a/core/libraries/Hubzero/Mail/Token.php b/core/libraries/Hubzero/Mail/Token.php new file mode 100644 index 00000000000..c35de360500 --- /dev/null +++ b/core/libraries/Hubzero/Mail/Token.php @@ -0,0 +1,180 @@ +_currentVersion = $HubmailConfig1->email_token_current_version; + + if (empty($this->_currentVersion)) + { + throw new RuntimeException('Class HubmailConfig->email_token_current_version not found in config file'); + } + + // Grab the encryption info for that version + $prop = 'email_token_encryption_info_v' . $this->_currentVersion; + $encryption_info = $HubmailConfig1->$prop; + + if (empty($encryption_info)) + { + throw new RuntimeException('Class HubmailConfig->email_token_encryption_info_vX not found for version: ' . $this->_currentVersion); + } + + // Encryption info is comma delimited (key, iv) in this configuraiton value + $keyArray = explode(',', $encryption_info); + + if (count($keyArray) <> 2) + { + throw new RuntimeException(__CLASS__ . '::__construct(); config.email_token_encryption_info_v' . $tokenVersion . ' cannot be split'); + } + + $this->_key = $keyArray[0]; + $this->_iv = $keyArray[1]; + $this->_blocksize = 8; // in bytes + } + + /** + * Build a unique email token + * + * @param int $version + * @param int $action + * @param int $userid + * @param int $id + * @return string Base 16 string representing token + */ + public function buildEmailToken($version, $action, $userid, $id) + { + $rv = ''; + + $binaryString = pack("NNN", $userid, $id, intval(time())); + + // Hash the unencrypted version hex version of the binary string + // Include the unencrypted version and action bytes as well + $hash = sha1(bin2hex(pack("C", $version)) . bin2hex(pack("C", $action)) . bin2hex($binaryString)); + + // We're only using a portion of the hash as a checksum + $hashsub = substr($hash, 0, 4); + + // Append hash to end of binary string, two hex digits stuffed into a single unsigned byte + $binaryString .= pack("n", hexdec($hashsub)); + + // Add PKCS7 style padding before encryption + $pad = $this->_blocksize - (strlen($binaryString) % $this->_blocksize); + $binaryString .= str_repeat(chr($pad), $pad); + + // Do the encryption + $cipher = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', 'cbc', ''); + mcrypt_generic_init($cipher, $this->_key, $this->_iv); + $encrypted = mcrypt_generic($cipher, $binaryString); + mcrypt_generic_deinit($cipher); + + // Prepend an unencrypted version byte and action byte (in base16) + $rv = bin2hex(pack("C", $version)) . bin2hex(pack("C", $action)) . bin2hex($encrypted); + + return $rv; + } + + /** + * Function to decrypt email token + * + * @param string $t Email token + * @return array Email token details + */ + public function decryptEmailToken($t) + { + // returns 3 element array, depending on the context, userid will be first, + // followed by another id (groupid, ticketid, etc) and a timestamp indicating + // the age of the token if you want to consider expiring it after a certain age + + // strip the unencrypted version and action bytes at the beginning of the token + $rawtoken = substr($t, 4); + + // Convert from hex to bin + $encrypted = hex2bin($rawtoken); + + // Do the decryption + $cipher = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', 'cbc', ''); + mcrypt_generic_init($cipher, $this->_key, $this->_iv); + $decrypted = mdecrypt_generic($cipher, $encrypted); + + // unpack the original values, no need to strip padding or hash + // we'll just unpack what we need + $arr = unpack("N3", $decrypted); + return array($arr[1], $arr[2], $arr[3]); + } +} diff --git a/core/libraries/Hubzero/Mail/Transport/Mandrill.php b/core/libraries/Hubzero/Mail/Transport/Mandrill.php new file mode 100644 index 00000000000..bb05259f149 --- /dev/null +++ b/core/libraries/Hubzero/Mail/Transport/Mandrill.php @@ -0,0 +1,200 @@ +apiKey = $apiKey; + } + + /** + * Tests to see if the transporter has been started + * + * @return bool + */ + public function isStarted() + { + return $this->started; + } + + /** + * Starts the transport mechanism + * + * @return void + */ + public function start() + { + $this->mandrill = new MandrillApi($this->apiKey); + } + + /** + * Stop the transport mechanism + * + * @return void + */ + public function stop() + { + $this->mandrill = null; + } + + /** + * Sends the given message + * + * Recipient/sender data will be retrieved from the Message API. + * The return value is the number of recipients who were accepted for delivery. + * + * @param object $message The message to be sent + * @param array $failedRecipients An array of failures + * @return int + */ + public function send(Swift_Mime_Message $message, &$failedRecipients=null) + { + try + { + // Start building the message + $from = $message->getFrom(); + $to = []; + + // Process recipients + foreach ($message->getTo() as $address => $name) + { + $to[] = [ + 'email' => $address, + 'name' => $name, + 'type' => 'to' + ]; + } + + // Check for attachments + $attachments = []; + $html = ''; + $txt = ''; + + foreach ($message->getChildren() as $children) + { + if ($children instanceof Swift_Attachment) + { + $attachments[] = [ + 'type' => $children->getContentType(), + 'name' => $children->getFilename(), + 'content' => base64_encode($children->getBody()) + ]; + } + elseif ($message->getBody() == null) + { + if ($children->getContentType() == 'text/html') + { + $html .= $children->getBody(); + } + else + { + $txt .= $children->getBody(); + } + } + else + { + if ($message->getContentType() == 'text/html') + { + $html .= $message->getBody(); + } + else + { + $txt .= $message->getBody(); + } + } + } + + // Build message + $mail = [ + 'html' => $html, + 'txt' => $txt, + 'subject' => $message->getSubject(), + 'from_email' => array_keys($from)[0], + 'from_name' => reset($from), + 'to' => $to, + 'headers' => array('Reply-To' => $message->getReplyTo()), + 'attachments' => $attachments, + 'tags' => $message->getTags(), + 'preserve_recipients' => false, + ]; + + // @FIXME: could paramertize some of these options + $async = false; + $ip_pool = 'Main Pool'; + $result = $this->mandrill->messages->send($mail, $async, $ip_pool); + + // Check for issues in sending + foreach ($result as $recepient) + { + if (!in_array($recepient['status'], ['queued', 'sent'])) + { + \Log::info(\Lang::txt('Mail to %s failed', $recepient['email'])); + } + } + + return true; + } + catch (Mandrill_Error $e) + { + throw new RuntimeException('A mandrill error occurred: ' . $e->getMessage(), 500); + } + } + + /** + * Registers a plugin on the transporter + * + * @FIXME: not exactly sure how this comes into play (more research needed) + * + * @param object $plugin + * @return void + */ + public function registerPlugin(Swift_Events_EventListener $plugin) + { + return; + } +} diff --git a/core/libraries/Hubzero/Mail/View.php b/core/libraries/Hubzero/Mail/View.php new file mode 100644 index 00000000000..8632077f1d9 --- /dev/null +++ b/core/libraries/Hubzero/Mail/View.php @@ -0,0 +1,82 @@ +_mailTemplate = new Template(); + + // call parent construct + parent::__construct($config); + } + + /** + * Load a template file -- first look in the templates folder for an override + * + * [!] Override to wrap html view in mail template + * + * @param string $tpl The name of the template source file; automatically searches the template paths and compiles as needed. + * @return string The output of the the template script. + */ + public function loadTemplate($tpl = null) + { + // hold reference to template passed in + $template = ($tpl === false) ? null : $tpl; + + // call load template and hold on to content + $content = parent::loadTemplate($template); + + // if we want to wrap in mail template + if ($tpl !== false) + { + $this->_mailTemplate->setBuffer($content, 'component'); + $content = $this->_mailTemplate->render(); + //$this->_mailTemplate->setBuffer(null, array('type' => 'head', 'name' => 'email')); + $this->_mailTemplate->setBuffer(null, 'component'); + $this->_mailTemplate->setBuffer(null, 'head'); + } + + // return content + return $content; + } + + /** + * Include CSS declaration in document head + * + * @param string $css CSS string + * @return void + */ + public function css($css) + { + $this->_mailTemplate->addStyleDeclaration($css); + } +} diff --git a/core/libraries/Hubzero/Menu/Manager.php b/core/libraries/Hubzero/Menu/Manager.php new file mode 100644 index 00000000000..bbfa0d55a85 --- /dev/null +++ b/core/libraries/Hubzero/Menu/Manager.php @@ -0,0 +1,103 @@ +menus = array(); + } + + /** + * Get the default menu name. + * + * @return string + */ + public function getDefaultMenu() + { + return 'base'; + } + + /** + * Get a menu instance. + * + * @param string $menu + * @return mixed + */ + public function menu($menu = null, $options = array()) + { + $menu = $menu ?: $this->getDefaultMenu(); + + // If the given menu has not been created before, we will create the instances + // here and cache it so we can return it next time very quickly. If there is + // already a menu created by this name, we'll just return that instance. + if (!isset($this->menus[$menu])) + { + $this->menus[$menu] = $this->createMenu($menu, $options); + } + + return $this->menus[$menu]; + } + + /** + * Create a new menu instance. + * + * @param string $menu + * @return mixed + * @throws \InvalidArgumentException + */ + protected function createMenu($menu, $options = array()) + { + $cls = __NAMESPACE__ . '\\Type\\' . ucfirst($menu); + + if (class_exists($cls)) + { + return new $cls($options); + } + + throw new \InvalidArgumentException("Menu [$menu] not supported."); + } + + /** + * Get all of the created "menus". + * + * @return array + */ + public function getMenus() + { + return $this->menus; + } + + /** + * Dynamically call the default menu instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return call_user_func_array(array($this->menu(), $method), $parameters); + } +} diff --git a/core/libraries/Hubzero/Menu/Type/Administrator.php b/core/libraries/Hubzero/Menu/Type/Administrator.php new file mode 100644 index 00000000000..151d0422d62 --- /dev/null +++ b/core/libraries/Hubzero/Menu/Type/Administrator.php @@ -0,0 +1,15 @@ +load(); + + foreach ($this->_items as $item) + { + if ($item->home) + { + $this->_default[trim($item->language)] = $item->id; + } + + // Decode the item params + $item->params = new Registry($item->params); + } + } + + /** + * Get menu item by id + * + * @param integer $id The item id + * @return mixed The item object, or null if not found + */ + public function getItem($id) + { + $result = null; + + if (isset($this->_items[$id])) + { + $result =& $this->_items[$id]; + } + + return $result; + } + + /** + * Set the default item by id and language code. + * + * @param integer $id The menu item id. + * @param string $language The language cod (since 1.6). + * @return boolean True, if successful + */ + public function setDefault($id, $language = '') + { + if (isset($this->_items[$id])) + { + $this->_default[$language] = $id; + return true; + } + + return false; + } + + /** + * Get the default item by language code. + * + * @param string $language The language code, default value of * means all. + * @return mixed The item object + */ + public function getDefault($language = '*') + { + if (array_key_exists($language, $this->_default)) + { + return $this->_items[$this->_default[$language]]; + } + + if (array_key_exists('*', $this->_default)) + { + return $this->_items[$this->_default['*']]; + } + + return 0; + } + + /** + * Set the default item by id + * + * @param integer $id The item id + * @return mixed If successful the active item, otherwise null + */ + public function setActive($id) + { + if (isset($this->_items[$id])) + { + $this->_active = $id; + + $result =& $this->_items[$id]; + + return $result; + } + + return null; + } + + /** + * Get menu item by id. + * + * @return object The item object. + */ + public function getActive() + { + if ($this->_active) + { + $item =& $this->_items[$this->_active]; + + return $item; + } + + return null; + } + + /** + * Gets menu items by attribute + * + * @param mixed $attributes The field name(s). + * @param mixed $values The value(s) of the field. If an array, need to match field names + * each attribute may have multiple values to lookup for. + * @param boolean $firstonly If true, only returns the first item found + * @return array + */ + public function getItems($attributes, $values, $firstonly = false) + { + $items = array(); + $attributes = (array) $attributes; + $values = (array) $values; + + foreach ($this->_items as $item) + { + if (!is_object($item)) + { + continue; + } + + $test = true; + for ($i = 0, $count = count($attributes); $i < $count; $i++) + { + $c = $attributes[$i]; + if (is_array($values[$i])) + { + if (!in_array($item->$c, $values[$i])) + { + $test = false; + break; + } + } + else + { + if ($item->$c != $values[$i]) + { + $test = false; + break; + } + } + } + + if ($test) + { + if ($firstonly) + { + return $item; + } + + $items[] = $item; + } + } + + return $items; + } + + /** + * Gets the parameter object for a certain menu item + * + * @param integer $id The item id + * @return object A Registry object + */ + public function getParams($id) + { + if ($menu = $this->getItem($id)) + { + return $menu->params; + } + + return new Registry; + } + + /** + * Getter for the menu array + * + * @return array + */ + public function getMenu() + { + return $this->_items; + } + + /** + * Method to check object authorization against an access control + * object and optionally an access extension object + * + * @param integer $id The menu id + * @return boolean True if authorised + */ + public function authorise($id) + { + $menu = $this->getItem($id); + + if ($menu) + { + return in_array((int) $menu->access, $this->get('access', array(0))); + } + + return true; + } + + /** + * Loads the menu items + * + * @return void + */ + public function load() + { + } +} diff --git a/core/libraries/Hubzero/Menu/Type/Site.php b/core/libraries/Hubzero/Menu/Type/Site.php new file mode 100644 index 00000000000..9ae34880b4b --- /dev/null +++ b/core/libraries/Hubzero/Menu/Type/Site.php @@ -0,0 +1,146 @@ +get('db') instanceof \Hubzero\Database\Driver)) + { + return; + } + + // Initialise variables. + $db = $this->get('db'); + + $query = $db->getQuery() + ->select('m.id') + ->select('m.menutype') + ->select('m.title') + ->select('m.alias') + ->select('m.note') + ->select('m.path', 'route') + ->select('m.link') + ->select('m.type') + ->select('m.level') + ->select('m.language') + ->select('m.browserNav') + ->select('m.access') + ->select('m.params') + ->select('m.home') + ->select('m.img') + ->select('m.template_style_id') + ->select('m.component_id') + ->select('m.parent_id') + ->select('e.element', 'component') + ->from('#__menu', 'm') + ->join('#__extensions AS e', 'e.extension_id', 'm.component_id', 'left') + ->whereEquals('m.published', 1) + ->where('m.parent_id', '>', 0) + ->whereEquals('m.client_id', 0) + ->order('m.lft', 'asc'); + + // Set the query + $db->setQuery($query->toString()); + + $this->_items = $db->loadObjectList('id'); + + foreach ($this->_items as &$item) + { + // Get parent information. + $parent_tree = array(); + if (isset($this->_items[$item->parent_id])) + { + $parent_tree = $this->_items[$item->parent_id]->tree; + } + + // Create tree. + $parent_tree[] = $item->id; + $item->tree = $parent_tree; + + // Create the query array. + $url = str_replace('index.php?', '', $item->link); + $url = str_replace('&', '&', $url); + + parse_str($url, $item->query); + } + } + + /** + * Gets menu items by attribute + * + * @param string $attributes The field name + * @param string $values The value of the field + * @param boolean $firstonly If true, only returns the first item found + * @return array + */ + public function getItems($attributes, $values, $firstonly = false) + { + $attributes = (array) $attributes; + $values = (array) $values; + + // Filter by language if not set + if (($key = array_search('language', $attributes)) === false) + { + if ($this->get('language_filter')) + { + $attributes[] = 'language'; + $values[] = array($this->get('language'), '*'); + } + } + elseif ($values[$key] === null) + { + unset($attributes[$key]); + unset($values[$key]); + } + + // Filter by access level if not set + if (($key = array_search('access', $attributes)) === false) + { + $attributes[] = 'access'; + $values[] = $this->get('access'); + } + elseif ($values[$key] === null) + { + unset($attributes[$key]); + unset($values[$key]); + } + + return parent::getItems($attributes, $values, $firstonly); + } + + /** + * Get menu item by id + * + * @param string $language The language code. + * @return mixed The item object + */ + public function getDefault($language = '*') + { + if (array_key_exists($language, $this->_default) && $this->get('language_filter')) + { + return $this->_items[$this->_default[$language]]; + } + + if (array_key_exists('*', $this->_default)) + { + return $this->_items[$this->_default['*']]; + } + + return 0; + } +} diff --git a/core/libraries/Hubzero/Message/Action.php b/core/libraries/Hubzero/Message/Action.php new file mode 100644 index 00000000000..19e6b728247 --- /dev/null +++ b/core/libraries/Hubzero/Message/Action.php @@ -0,0 +1,85 @@ + 'notempty' + ); + + /** + * Get records for specific type, element, component, and user + * + * @param string $type Action type + * @param string $component Component name + * @param integer $element ID of element that needs action + * @param integer $uid User ID + * @return object + */ + public static function getActionItems($type, $component, $element, $uid) + { + $entries = self::all(); + + $a = $entries->getTableName(); + $m = Message::blank()->getTableName(); + $r = Recipient::blank()->getTableName(); + + return $entries + ->select($m . '.id') + ->join($r, $r . '.actionid', $a . '.id', 'inner') + ->join($m, $m . '.id', $r . '.mid', 'inner') + ->whereEquals($m . '.type', $type) + ->whereEquals($r . '.uid', $uid) + ->whereEquals($a . '.class', $component) + ->whereEquals($a . '.element', $element) + ->rows(); + } +} diff --git a/core/libraries/Hubzero/Message/Component.php b/core/libraries/Hubzero/Message/Component.php new file mode 100644 index 00000000000..40c888751ac --- /dev/null +++ b/core/libraries/Hubzero/Message/Component.php @@ -0,0 +1,121 @@ + 'notempty', + 'action' => 'notempty' + ); + + /** + * Sets up additional custom rules + * + * @return void + */ + public function setup() + { + $this->addRule('component', function($data) + { + self::$connection->setQuery("SELECT element FROM `#__extensions` AS e WHERE e.type = 'component' ORDER BY e.name ASC"); + $extensions = self::$connection->loadColumn(); + if (!in_array($data['component'], $extensions)) + { + return Lang::txt('Component does not exist.'); + } + return false; + }); + } + + /** + * Defines a belongs to one relationship between newsletter and story + * + * @return object + */ + public function getRecords($filters = array()) + { + $entries = self::all(); + + $c = $entries->getTableName(); + $e = '#__extensions'; + + $entries + ->select($c . '.*,' . $e . '.name') + ->join($e, $e . '.element', $c . '.component', 'inner') + ->whereEquals($e . '.type', 'component'); + + if (isset($filters['component']) && $filters['component']) + { + $entries->whereEquals($e . '.element', $filters['component']); + } + + return $entries + ->ordered($c . '.component', 'asc') + ->rows(); + } + + /** + * Get all records + * + * @return array + */ + public function getComponents() + { + return self::all() + ->deselect() + ->select('component') + ->order('component', 'asc') + ->group('component') + ->rows(); + } +} diff --git a/core/libraries/Hubzero/Message/Helper.php b/core/libraries/Hubzero/Message/Helper.php new file mode 100644 index 00000000000..4310bea4f84 --- /dev/null +++ b/core/libraries/Hubzero/Message/Helper.php @@ -0,0 +1,167 @@ + 0) + { + // Loop through each ID + foreach ($uids as $uid) + { + // Find any actions the user needs to take for this $component and $element + $action = Action::blank(); + $mids = $action->getActionItems($component, $element, $uid, $type); + + // Check if the user has any action items + if (count($mids) > 0) + { + $recipient = Recipient::blank(); + if (!$recipient->setState(1, $mids)) + { + $this->setError(Lang::txt('Unable to update recipient records %s for user %s', implode(',', $mids), $uid)); + } + } + } + } + + return true; + } + + /** + * Send a message to one or more users + * + * @param string $type Message type (maps to #__xmessage_component table) + * @param string $subject Message subject + * @param string $message Message to send + * @param array $from Message 'from' data (e.g., name, address) + * @param array $to List of user IDs + * @param string $component Component name + * @param integer $element ID of object that needs an action item + * @param string $description Action item description + * @param integer $group_id Parameter description (if any) ... + * @return mixed True if no errors else error message + */ + public function sendMessage($type, $subject, $message, $from=array(), $to=array(), $component='', $element=null, $description='', $group_id=0) + { + // Do we have a message? + if (!$message) + { + return false; + } + + // Do we have a subject line? If not, create it from the message + if (!$subject && $message) + { + $subject = substr($message, 0, 70); + if (strlen($subject) >= 70) + { + $subject .= '...'; + } + } + + // Create the message object and store it in the database + $xmessage = Message::blank(); + $xmessage->set('subject', $subject); + $xmessage->set('message', $message); + $xmessage->set('created', Date::toSql()); + $xmessage->set('created_by', User::get('id')); + $xmessage->set('component', $component); + $xmessage->set('type', $type); + $xmessage->set('group_id', $group_id); + + if (!$xmessage->save()) + { + return $xmessage->getError(); + } + + // Does this message require an action? + // **DEPRECATED** + /*$action = new Action($database); + if ($element || $description) + { + $action->class = $component; + $action->element = $element; + $action->description = $description; + if (!$action->store()) + { + return $action->getError(); + } + }*/ + + // Do we have any recipients? + if (count($to) > 0) + { + // Loop through each recipient + foreach ($to as $uid) + { + // Create a recipient object that ties a user to a message + $recipient = Recipient::blank(); + $recipient->set('uid', $uid); + $recipient->set('mid', $xmessage->get('id')); + $recipient->set('created', Date::toSql()); + $recipient->set('expires', Date::of(time() + (168 * 24 * 60 * 60))->toSql()); + $recipient->set('actionid', 0); //$action->id + if (!$recipient->save()) + { + return $recipient->getError(); + } + + // Get the user's methods for being notified + $notify = Notify::blank(); + $methods = $notify->getRecords($uid, $type); + + $user = User::getInstance($uid); + + // Do we have any methods? + if ($methods) + { + // Loop through each method + foreach ($methods as $method) + { + $action = strtolower($method->method); + + if (!Event::trigger('xmessage.onMessage', array($from, $xmessage, $user, $action))) + { + $this->setError(Lang::txt('Unable to message user %s with method %s', $uid, $action)); + } + } + } + } + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Message/Message.php b/core/libraries/Hubzero/Message/Message.php new file mode 100644 index 00000000000..ced96747956 --- /dev/null +++ b/core/libraries/Hubzero/Message/Message.php @@ -0,0 +1,223 @@ + 'notempty', + 'created_by' => 'positive|nonzero' + ); + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function creator() + { + return $this->belongsToOne('Hubzero\User\User', 'created_by'); + } + + /** + * Get a record count based on filters passed + * + * @param array $filters Filters to build query from + * @return integer + */ + public function getCount($filters=array()) + { + return self::all() + ->total(); + } + + /** + * Get records based on filters passed + * + * @param array $filters Filters to build query from + * @return array + */ + public function getRecords($filters=array()) + { + return self::all() + ->rows(); + } + + /** + * Builds a query string based on filters passed + * + * @param array $filters Filters to build query from + * @return string SQL + */ + private function buildQuery($filters=array()) + { + $entries = self::all(); + + $m = $entries->getTableName(); + $u = '#__users'; + + if (isset($filters['group_id']) && $filters['group_id'] != 0) + { + $entries + ->select($m . '.*,' . $u . '.name') + ->join($u, $u . '.id', $m . '.created_by', 'inner'); + } + else + { + $r = Recipient::blank()->getTableName(); + + $entries + ->select($m . '.*,' . $u . '.name') + ->join($r, $r . '.mid', $m . '.id', 'inner') + ->join($u, $u . '.id', $r . '.uid', 'inner'); + } + + if (isset($filters['created_by']) && $filters['created_by'] != 0) + { + $entries->whereEquals('created_by', $filters['created_by']); + } + if (isset($filters['daily_limit']) && $filters['daily_limit'] != 0) + { + $start = date('Y-m-d', mktime(0, 0, 0, date('m'), date('d'), date('Y'))) . " 00:00:00"; + $end = date('Y-m-d', mktime(0, 0, 0, date('m'), date('d'), date('Y'))) . " 23:59:59"; + + $entries->where('created', '>=', $start); + $entries->where('created', '<=', $end); + } + if (isset($filters['group_id']) && $filters['group_id'] != 0) + { + $entries->whereEquals('group_id', (int)$filters['group_id']); + } + + return $entries; + } + + /** + * Get sent messages + * + * @param array $filters Filters to build query from + * @return integer + */ + public function getSentMessages($filters=array()) + { + $entries = $this->buildQuery($filters); + $entries->order($entries->getTableName() . '.created', 'desc'); + + if (isset($filters['limit']) && $filters['limit'] != 0) + { + $entries->limit($filters['limit']) + ->start($filters['start']); + } + + return $entries->rows(); + } + + /** + * Get a record count of messages sent + * + * @param array $filters Filters to build query from + * @return integer + */ + public function getSentMessagesCount($filters=array()) + { + $entries = $this->buildQuery($filters); + + return $entries->total(); + } + + /** + * Transform and prepare content + * + * @return string + */ + public function transformMessage() + { + $UrlPtrn = "[^=\"\'](https?:|mailto:|ftp:|gopher:|news:|file:)" . "([^ |\\/\"\']*\\/)*([^ |\\t\\n\\/\"\']*[A-Za-z0-9\\/?=&~_])"; + + $message = str_replace("\n", "\n ", stripslashes($this->get('message'))); + $message = preg_replace_callback("/$UrlPtrn/", array($this,'autolink'), $message); + $message = nl2br($message); + $message = str_replace("\t", '    ', $message); + + return $message; + } + + /** + * Auto-link mailto, ftp, and http strings in text + * + * @param array $matches Text to autolink + * @return string + */ + protected function autolink($matches) + { + $href = $matches[0]; + + if (substr($href, 0, 1) == '!') + { + return substr($href, 1); + } + + $href = str_replace('"', '', $href); + $href = str_replace("'", '', $href); + $href = str_replace('”', '', $href); + + $h = array('h', 'm', 'f', 'g', 'n'); + if (!in_array(substr($href, 0, 1), $h)) + { + $href = substr($href, 1); + } + $name = trim($href); + if (substr($name, 0, 7) == 'mailto:') + { + $name = substr($name, 7, strlen($name)); + $name = Str::obfuscate($name); + + $href = 'mailto:' . $name; + } + $l = sprintf( + ' %s', $href, $name + ); + return $l; + } +} diff --git a/core/libraries/Hubzero/Message/Notify.php b/core/libraries/Hubzero/Message/Notify.php new file mode 100644 index 00000000000..f79d68195a1 --- /dev/null +++ b/core/libraries/Hubzero/Message/Notify.php @@ -0,0 +1,115 @@ + 'positive|nonzero' + ); + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function user() + { + return $this->belongsToOne('Hubzero\User\User', 'uid'); + } + + /** + * Get records for a user + * + * @param integer $uid User ID + * @param string $type Record type + * @return mixed False if errors, array on success + */ + public function getRecords($uid, $type=null) + { + $entries = self::all() + ->whereEquals('uid', $uid); + + if ($type) + { + $entries->whereEquals('type', $type); + } + + return $entries + ->order('priority', 'asc') + ->rows(); + } + + /** + * Clear all entries for a user + * + * @param integer $uid User ID + * @return boolean True on success + */ + public function deleteByUser($uid) + { + return $this->delete($this->getTableName()) + ->whereEquals('uid', $uid) + ->execute(); + } + + /** + * Delete notifications for action + * + * @param string $type + * @return boolean True on success, False on error + */ + public function deleteByType($type) + { + return $this->delete($this->getTableName()) + ->whereEquals('type', $type) + ->execute(); + } +} diff --git a/core/libraries/Hubzero/Message/Recipient.php b/core/libraries/Hubzero/Message/Recipient.php new file mode 100644 index 00000000000..391571ef180 --- /dev/null +++ b/core/libraries/Hubzero/Message/Recipient.php @@ -0,0 +1,340 @@ + 'positive|nonzero' + ); + + /** + * Defines a belongs to one relationship between entry and message + * + * @return object + */ + public function message() + { + return $this->belongsToOne('Message', 'mid'); + } + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function user() + { + return $this->belongsToOne('Hubzero\User\User', 'uid'); + } + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function action() + { + return $this->belongsToOne('Action', 'actionid'); + } + + /** + * Load a record by message ID and user ID + * + * @param integer $mid Message ID + * @param integer $uid User ID + * @return boolean True on success + */ + public static function oneByMessageAndUser($mid, $uid) + { + return self::all() + ->whereEquals('mid', $mid) + ->whereEquals('uid', $uid) + ->row(); + } + + /** + * Builds a query string based on filters passed + * + * @param array $filters Filters to build query from + * @return string SQL + */ + private function buildQuery($uid, $filters=array()) + { + $r = $this->getTableName(); + $m = Message::blank()->getTableName(); + $s = Seen::blank()->getTableName(); + + $entries = self::all() + ->join($m, $m . '.id', $r . '.mid', 'inner') + //->join($s, $s . '.mid', $m . '.id', 'left') + ->joinRaw($s, $s . '.mid=' . $m . '.id AND ' . $s . '.uid=' . $uid, 'left') + ->whereEquals($r . '.uid', $uid); + + if (isset($filters['state'])) + { + $entries->whereEquals($r . '.state', $filters['state']); + } + if (isset($filters['filter']) && $filters['filter'] != '') + { + $entries->whereEquals($m . '.component', $filters['filter']); + } + + return $entries; + } + + /** + * Get records for a user based on filters passed + * + * @param integer $uid User ID + * @param array $filters Filters to build query from + * @return mixed False if errors, array on success + */ + public function getMessages($uid=null, $filters=array()) + { + $uid = $uid ?: $this->uid; + + if (!$uid) + { + return array(); + } + + $r = $this->getTableName(); + $m = Message::blank()->getTableName(); + $s = Seen::blank()->getTableName(); + + $entries = $this->buildQuery($uid, $filters); + + return $entries + ->select($m . '.*,' . $r . '.expires,' . $r . '.actionid,' . $r . '.state,' . $s . '.whenseen') + ->order($r . '.created', 'desc') + ->limit($filters['limit']) + ->start($filters['start']) + ->rows(); + } + + /** + * Get a record count for a user based on filters passed + * + * @param integer $uid User ID + * @param array $filters Filters to build query from + * @return mixed False if errors, integer on success + */ + public function getMessagesCount($uid, $filters=array()) + { + if (!$uid) + { + return 0; + } + + $entries = $this->buildQuery($uid, $filters); + + return $entries->total(); + } + + /** + * Get a list of unread messages for a user + * + * @param integer $uid User ID + * @param integer $limit Number of records to return + * @return mixed False if errors, array on success + */ + public function getUnreadMessages($uid, $limit=null) + { + if (!$uid) + { + return array(); + } + + $r = $this->getTableName(); + $m = Message::blank()->getTableName(); + $s = Seen::blank()->getTableName(); + + $entries = Message::all() + ->select($m . '.*,' . $r . '.expires,' . $r . '.actionid') + //->join($m, $m . '.id', $r . '.mid', 'inner') + ->join($r, $m . '.id', $r . '.mid', 'inner') + ->whereEquals($r . '.uid', $uid) + ->where($r . '.state', '!=', 2) + ->whereRaw($m . ".id NOT IN (SELECT s.mid FROM `" . $s . "` AS s WHERE s.uid=" . $uid . ")") + ->order($r . '.created', 'desc'); + + if ($limit) + { + $entries->limit($limit); + } + + return $entries->rows(); + } + + /** + * Get a count of unread messages for a user + * + * @param integer $uid User ID + * @return integer + */ + public function getUnreadMessagesCount($uid) + { + if (!$uid) + { + return 0; + } + + $r = $this->getTableName(); + $m = Message::blank()->getTableName(); + $s = Seen::blank()->getTableName(); + + $entries = Message::blank() + ->join($r, $m . '.id', $r . '.mid', 'inner') + ->whereEquals($r . '.uid', $uid) + ->where($r . '.state', '!=', 2) + ->whereRaw($m . ".id NOT IN (SELECT s.mid FROM `" . $s . "` AS s WHERE s.uid=" . $uid . ")"); + + return $entries->total(); + } + + /** + * Delete all messages marked as trash for a user + * + * @param integer $uid User ID + * @return boolean True on success + */ + public function deleteTrash($uid) + { + return $this->delete() + ->whereEquals('uid', $uid) + ->whereEquals('state', 2) + ->execute(); + } + + /** + * Set the state of multiple messages + * + * @param integer $state State to set + * @param array $ids List of message IDs + * @return boolean True on success + */ + public function setState($state=0, $ids=array()) + { + if (count($ids) <= 0) + { + return false; + } + + $ids = array_map('intval', $ids); + + return $this->update() + ->set(array('state' => $state)) + ->whereIn('id', $ids) + ->execute(); + } + + /** + * Mark a message as being read by the recipient + * + * @return boolean True on success + */ + public function markAsRead() + { + if (!$this->get('id')) + { + $this->addError('Recipient record not found'); + return false; + } + + $xseen = Seen::oneByMessageAndUser($this->get('mid'), $this->get('uid')); + + if ($xseen->get('whenseen') == '' + || $xseen->get('whenseen') == '0000-00-00 00:00:00' + || $xseen->get('whenseen') == null) + { + $dt = new Date('now'); + + $xseen->set('mid', $this->get('mid')); + $xseen->set('uid', $this->get('uid')); + $xseen->set('whenseen', $dt->toSql()); + if (!$xseen->save()) + { + $this->addError($xseen->getError()); + return false; + } + } + + return true; + } + + /** + * Mark a message as not being read by the recipient + * + * @return boolean True on success + */ + public function markAsUnread() + { + if (!$this->get('id')) + { + $this->addError('Recipient record not found'); + return false; + } + + $xseen = Seen::oneByMessageAndUser($this->get('mid'), $this->get('uid')); + + if ($xseen->get('id')) + { + if (!$xseen->destroy()) + { + $this->addError($xseen->getError()); + return false; + } + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Message/Seen.php b/core/libraries/Hubzero/Message/Seen.php new file mode 100644 index 00000000000..55a424cab79 --- /dev/null +++ b/core/libraries/Hubzero/Message/Seen.php @@ -0,0 +1,93 @@ + 'positive|nonzero', + 'uid' => 'positive|nonzero' + ); + + /** + * Defines a belongs to one relationship between entry and message + * + * @return object + */ + public function message() + { + return $this->belongsToOne('Message', 'mid'); + } + + /** + * Defines a belongs to one relationship between entry and user + * + * @return object + */ + public function user() + { + return $this->belongsToOne('Hubzero\User\User', 'uid'); + } + + /** + * Load a record by message ID and user ID and bind to $this + * + * @param integer $mid Message ID + * @param integer $uid User ID + * @return object + */ + public static function oneByMessageAndUser($mid, $uid) + { + return self::all() + ->whereEquals('mid', $mid) + ->whereEquals('uid', $uid) + ->row(); + } +} diff --git a/core/libraries/Hubzero/Module/Helper.php b/core/libraries/Hubzero/Module/Helper.php new file mode 100644 index 00000000000..836044ce15a --- /dev/null +++ b/core/libraries/Hubzero/Module/Helper.php @@ -0,0 +1,86 @@ +app = $app; + $this->profiler = $profiler; + } + + /** + * Count the modules based on the given condition + * + * @param string $condition The condition to use + * @return integer Number of modules found + */ + public function count($condition) + { + $words = explode(' ', $condition); + for ($i = 0; $i < count($words); $i+=2) + { + // odd parts (modules) + $name = strtolower($words[$i]); + $words[$i] = count($this->byPosition($name)); + } + + $str = 'return ' . implode(' ', $words) . ';'; + + return eval($str); + } + + /** + * Get module by name (real, eg 'Breadcrumbs' or folder, eg 'mod_breadcrumbs') + * + * @param string $name The name of the module + * @param string $title The title of the module, optional + * @return object The Module object + */ + public function byName($name, $title = null) + { + $result = null; + + $name = $this->canonical($name); + $modules = $this->all(); + $total = count($modules); + + for ($i = 0; $i < $total; $i++) + { + // Match the name of the module + if ($modules[$i]->name == $name || $modules[$i]->module == $name) + { + // Match the title if we're looking for a specific instance of the module + if (!$title || $modules[$i]->title == $title) + { + // Found it + $result =& $modules[$i]; + break; + } + } + } + + // If we didn't find it, and the name is mod_something, create a dummy object + if (is_null($result) && substr($name, 0, 4) == 'mod_') + { + $result = new \stdClass; + $result->id = 0; + $result->title = ''; + $result->module = $name; + $result->position = ''; + $result->content = ''; + $result->showtitle = 0; + $result->control = ''; + $result->params = ''; + $result->user = 0; + } + + return $result; + } + + /** + * Get modules by position + * + * @param string $position The position of the module + * @return array An array of module objects + */ + public function byPosition($position) + { + $position = strtolower($position); + $result = array(); + + $modules = $this->all(); + + $total = count($modules); + for ($i = 0; $i < $total; $i++) + { + if ($modules[$i]->position == $position) + { + $result[] =& $modules[$i]; + } + } + + if (count($result) == 0) + { + if ($this->outline()) + { + $result[0] = $this->byName('mod_' . $position); + $result[0]->title = $position; + $result[0]->content = $position; + $result[0]->position = $position; + } + } + + return $result; + } + + /** + * Checks if a module is enabled + * + * @param string $module The module name + * @return boolean + */ + public function isEnabled($module) + { + $result = $this->byName($module); + + return (is_object($result) && $result->id); + } + + /** + * Render modules for a position + * + * @param string $position Position to render modules for + * @param string $style Module style (deprecated?) + * @return string HTML + */ + public function position($position, $style='none') + { + if (!is_array($style)) + { + $style = array('style' => $style); + } + + $contents = ''; + foreach ($this->byPosition($position) as $mod) + { + $contents .= $this->render($mod, $style); + } + + return $contents; + } + + /** + * Render module by name + * + * @param string $name Module name + * @param string $style Module style (deprecated?) + * @return string HTML + */ + public function name($name, $style='none') + { + if (!is_array($style)) + { + $style = array('style' => $style); + } + + return $this->render( + $this->byName($name), + $style + ); + } + + /** + * Determine if module position outlining is enabled + * + * @return boolean + */ + protected function outline() + { + if ($this->app['request']->getBool('tp') + && $this->app['component']->params('com_templates')->get('template_positions_display')) + { + return true; + } + + return false; + } + + /** + * Render the module. + * + * @param object $module A module object. + * @param array $attribs An array of attributes for the module (probably from the XML). + * @return string The HTML content of the module output. + */ + public function render($module, $attribs = array()) + { + static $chrome; + + if (null !== $this->profiler) + { + $this->profiler->mark('beforeRenderModule ' . $module->module . ' (' . $module->title . ')'); + } + + // Record the scope. + $scope = $this->app->has('scope') ? $this->app->get('scope') : null; + + // Set scope to component name + $this->app->set('scope', $module->module); + + // Get module parameters + $params = new Registry($module->params); + + if (isset($attribs['params'])) + { + $customparams = new Registry(html_entity_decode($attribs['params'], ENT_COMPAT, 'UTF-8')); + + $params->merge($customparams); + + $module->params = $params->toString(); + } + + // Get module path + $module->module = $this->canonical($module->module); + + $path = $this->path($module->module); + + // Load the module + // $module->user is a check for 1.0 custom modules and is deprecated refactoring + if (file_exists($path)) + { + $this->app['language']->load($module->module, PATH_APP . DS . 'bootstrap' . DS . $this->app['client']->name, null, false, true) || + $this->app['language']->load($module->module, dirname($path), null, false, true); + + $module->path = $path; + + $content = ''; + ob_start(); + include $path; + $module->content = ob_get_contents() . $content; + ob_end_clean(); + } + + // Load the module chrome functions + if (!$chrome) + { + $chrome = array(); + } + + include_once PATH_CORE . DS . 'templates' . DS . 'system' . DS . 'html' . DS . 'modules.php'; + $chromePath = $this->app['template']->path . DS . 'html' . DS . 'modules.php'; + + if (!isset($chrome[$chromePath])) + { + if (file_exists($chromePath)) + { + include_once $chromePath; + } + + $chrome[$chromePath] = true; + } + + // Make sure a style is set + if (!isset($attribs['style'])) + { + $attribs['style'] = 'none'; + } + + // Dynamically add outline style + if ($this->outline()) + { + $attribs['style'] .= ' outline'; + } + + foreach (explode(' ', $attribs['style']) as $style) + { + $chromeMethod = 'modChrome_' . $style; + + // Apply chrome and render module + if (function_exists($chromeMethod)) + { + $module->style = $attribs['style']; + + ob_start(); + $chromeMethod($module, $params, $attribs); + $module->content = ob_get_contents(); + ob_end_clean(); + } + } + + // Revert the scope + $this->app->forget('scope'); + $this->app->set('scope', $scope); + + if (null !== $this->profiler) + { + $this->profiler->mark('afterRenderModule ' . $module->module . ' (' . $module->title . ')'); + } + + return $module->content; + } + + /** + * Get the path to a layout for a module + * + * @param string $module The name of the module + * @param string $layout The name of the module layout. If alternative layout, in the form template:filename. + * @return string The path to the module layout + */ + public function getLayoutPath($module, $layout = 'default') + { + $template = $this->app['template']->template; + $path = dirname($this->app['template']->path); + $default = $layout; + + if (strpos($layout, ':') !== false) + { + // Get the template and file name from the string + $temp = explode(':', $layout); + + $template = ($temp[0] == '_') ? $template : $temp[0]; + $layout = $temp[1]; + $default = ($temp[1]) ? $temp[1] : 'default'; + } + + // Build the template and base path for the layout + $tPath = $path . '/' . $template . '/html/' . $module . '/' . $layout . '.php'; + + $base = dirname($this->path($module)); + + $bPath = $base . '/tmpl/' . $default . '.php'; + $dPath = $base . '/tmpl/default.php'; + + // If the template has a layout override use it + if (file_exists($tPath)) + { + return $tPath; + } + elseif (file_exists($bPath)) + { + return $bPath; + } + + return $dPath; + } + + /** + * Load published modules. + * + * @return array + */ + public function all() + { + static $clean; + + if (isset($clean)) + { + return $clean; + } + + $Itemid = $this->app['request']->getInt('Itemid'); + + $user = $this->app['user']->getInstance(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $lang = $this->app['language']->getTag(); + $clientId = (int) $this->app['client']->id; + + if (!$this->app->has('cache.store') || !($cache = $this->app['cache.store'])) + { + $cache = new \Hubzero\Cache\Storage\None(); + } + $cacheid = 'com_modules.' . md5(serialize(array($Itemid, $groups, $clientId, $lang))); + + if (!($clean = $cache->get($cacheid))) + { + $db = $this->app['db']; + + $query = $db->getQuery(); + + $query + ->select('m.id') + ->select('m.title') + ->select('m.module') + ->select('m.position') + ->select('m.content') + ->select('m.showtitle') + ->select('m.params') + ->select('mm.menuid') + ->select('e.protected') + ->from('#__modules', 'm') + ->join('#__modules_menu AS mm', 'mm.moduleid', 'm.id', 'left') + ->whereEquals('m.published', 1); + + $query + ->joinRaw('#__extensions AS e', 'e.element = m.module AND e.client_id = m.client_id', 'left') + ->whereEquals('e.enabled', 1); + + $now = with(new Date('now'))->toSql(); + + $query + ->where('m.publish_up', 'IS', null, 'and', 1)->orWhere('m.publish_up', '<=', $now, 1)->resetDepth() + ->where('m.publish_down', 'IS', null, 'and', 1)->orWhere('m.publish_down', '>=', $now, 1)->resetDepth(); + + $query + ->whereIn('m.access', $user->getAuthorisedViewLevels()) + ->whereEquals('m.client_id', $clientId) + ->whereEquals('mm.menuid', (int) $Itemid, 1)->orWhere('mm.menuid', '<=', '0', 1)->resetDepth(); + + // Filter by language + if ($this->app->isSite() && $this->app->get('language.filter')) + { + $query->whereIn('m.language', array($lang, '*')); + } + + $query + ->order('m.position', 'asc') + ->order('m.ordering', 'asc'); + + // Set the query + $db->setQuery($query->toString()); + $modules = $db->loadObjectList(); + $clean = array(); + + if ($db->getErrorNum()) + { + $this->app['notification']->error( + $this->app['language']->txt('JLIB_APPLICATION_ERROR_MODULE_LOAD', $db->getErrorMsg()) + ); + + return $clean; + } + + // Apply negative selections and eliminate duplicates + $negId = $Itemid ? -(int) $Itemid : false; + $dupes = array(); + for ($i = 0, $n = count($modules); $i < $n; $i++) + { + $module = &$modules[$i]; + + // The module is excluded if there is an explicit prohibition + $negHit = ($negId === (int) $module->menuid); + + if (isset($dupes[$module->id])) + { + // If this item has been excluded, keep the duplicate flag set, + // but remove any item from the cleaned array. + if ($negHit) + { + unset($clean[$module->id]); + } + continue; + } + + $dupes[$module->id] = true; + + // Only accept modules without explicit exclusions. + if (!$negHit) + { + $module->name = substr($module->module, 4); + $module->style = null; + $module->position = strtolower($module->position); + + $clean[$module->id] = $module; + } + } + + unset($dupes); + + // Return to simple indexing that matches the query order. + $clean = array_values($clean); + + $cache->put($cacheid, $clean, $this->app['config']->get('cachetime', 15)); + } + + return $clean; + } + + /** + * Module cache helper + * + * Caching modes: + * To be set in XML: + * 'static' One cache file for all pages with the same module parameters + * 'oldstatic' 1.5 definition of module caching, one cache file for all pages + * with the same module id and user aid, + * 'itemid' Changes on itemid change, to be called from inside the module: + * 'safeuri' Id created from $cacheparams->modeparams array, + * 'id' Module sets own cache id's + * + * @param object $module Module object + * @param object $moduleparams Module parameters + * @param object $cacheparams Module cache parameters - id or url parameters, depending on the module cache mode + * @return string + */ + public function cache($module, $moduleparams, $cacheparams) + { + // [!] Deprecated. Needs to be refactored. + return true; + + if (!isset($cacheparams->modeparams)) + { + $cacheparams->modeparams = null; + } + + if (!isset($cacheparams->cachegroup)) + { + $cacheparams->cachegroup = $module->module; + } + + if (!$this->app->has('cache.store') || !($cache = $this->app['cache.store'])) + { + $cache = new \Hubzero\Cache\Storage\None(); + } + + // Turn cache off for internal callers if parameters are set to off and for all logged in users + if ($moduleparams->get('owncache', null) === '0' || $this->app['config']->get('caching') == 0 || $this->app['user']->getInstance()->get('id')) + { + $cache->setCaching(false); + } + + // module cache is set in seconds, global cache in minutes, setLifeTime works in minutes + $cache->setLifeTime($moduleparams->get('cache_time', $this->app['config']->get('cachetime') * 60) / 60); + + $wrkaroundoptions = array('nopathway' => 1, 'nohead' => 0, 'nomodules' => 1, 'modulemode' => 1, 'mergehead' => 1); + + $wrkarounds = true; + $view_levels = md5(serialize($this->app['user']->getInstance()->getAuthorisedViewLevels())); + + switch ($cacheparams->cachemode) + { + case 'id': + $ret = $cache->get( + array($cacheparams->class, $cacheparams->method), + $cacheparams->methodparams, + $cacheparams->modeparams, + $wrkarounds, + $wrkaroundoptions + ); + break; + + case 'safeuri': + $secureid = null; + if (is_array($cacheparams->modeparams)) + { + $uri = \Request::get(); + $safeuri = new \stdClass; + foreach ($cacheparams->modeparams as $key => $value) + { + // Use int filter for id/catid to clean out spamy slugs + if (isset($uri[$key])) + { + $safeuri->$key = \Request::_cleanVar($uri[$key], 0, $value); + } + } + } + $secureid = md5(serialize(array($safeuri, $cacheparams->method, $moduleparams))); + $ret = $cache->get( + array($cacheparams->class, $cacheparams->method), + $cacheparams->methodparams, + $module->id . $view_levels . $secureid, + $wrkarounds, + $wrkaroundoptions + ); + break; + + case 'static': + $ret = $cache->get( + array($cacheparams->class, $cacheparams->method), + $cacheparams->methodparams, + $module->module . md5(serialize($cacheparams->methodparams)), + $wrkarounds, + $wrkaroundoptions + ); + break; + + case 'oldstatic': // provided for backward compatibility, not really usefull + $ret = $cache->get( + array($cacheparams->class, $cacheparams->method), + $cacheparams->methodparams, + $module->id . $view_levels, + $wrkarounds, + $wrkaroundoptions + ); + break; + + case 'itemid': + default: + $ret = $cache->get( + array($cacheparams->class, $cacheparams->method), + $cacheparams->methodparams, + $module->id . $view_levels . \Request::getInt('Itemid', 0), + $wrkarounds, + $wrkaroundoptions + ); + break; + } + + return $ret; + } + + /** + * Get the parameters for a module + * + * @param mixed $id Module ID + * @return object Hubzero\Config\Registry + */ + public function params($id) + { + $params = ''; + + if ($this->app->has('db')) + { + $db = $this->app['db']; + + $query = $db->getQuery() + ->select('params') + ->from('#__modules') + ->whereEquals('published', 1); + + // Select module params based on name or ID + if (is_numeric($id)) + { + $query->whereEquals('id', (int) $id); + } + else + { + $query->whereEquals('module', $id); + } + + $db->setQuery($query->toString()); + $params = $db->loadResult(); + } + + //return params + return new Registry($params); + } + + /** + * Make sure module name follows naming conventions + * + * @param string $module The element value for the extension + * @return string + */ + public function canonical($module) + { + $module = preg_replace('/[^A-Z0-9_\.-]/i', '', $module); + if (substr($module, 0, strlen('mod_')) != 'mod_') + { + $module = 'mod_' . $module; + } + return $module; + } + + /** + * Get the path to a module + * + * @param string $module Module name + * @return string + */ + public function path($module) + { + $module = $this->canonical($module); + $prefixed = $module; + $unprefixed = substr($module, 4); + + $paths = array( + PATH_APP . DS . 'modules' . DS . $unprefixed . DS . $unprefixed . '.php', + PATH_APP . DS . 'modules' . DS . $prefixed . DS . $prefixed . '.php', + PATH_CORE . DS . 'modules' . DS . $unprefixed . DS . $unprefixed . '.php', + PATH_CORE . DS . 'modules' . DS . $prefixed . DS . $prefixed . '.php' + ); + + foreach ($paths as $path) + { + if (file_exists($path)) + { + return $path; + } + } + + return ''; + } +} diff --git a/core/libraries/Hubzero/Module/Module.php b/core/libraries/Hubzero/Module/Module.php new file mode 100644 index 00000000000..83b84a0b849 --- /dev/null +++ b/core/libraries/Hubzero/Module/Module.php @@ -0,0 +1,114 @@ +params = $params; + $this->module = $module; + } + + /** + * Display module + * + * @return void + */ + public function display() + { + require $this->getLayoutPath($this->params->get('layout', 'default')); + } + + /** + * Get the path of a layout for this module + * + * @param string $layout The layout name + * @return string + */ + public function getLayoutPath($layout='default') + { + return App::get('module')->getLayoutPath($this->module->module, $layout); + } + + /** + * Get the cached contents of a module + * caching it, if it doesn't already exist + * + * @return string + */ + public function getCacheContent() + { + $content = ''; + + if (!App::has('cache.store') || !$this->params->get('cache')) + { + return $content; + } + + $debug = App::get('config')->get('debug'); + $key = 'modules.' . $this->module->id; + $ttl = intval($this->params->get('cache_time', 0)); + + if ($debug || !$ttl) + { + return $content; + } + + if (!($content = App::get('cache.store')->get($key))) + { + ob_start(); + $this->run(); + $content = ob_get_contents(); + ob_end_clean(); + + $content .= ''; + + // Module time is in seconds, cache time is in minutes + // Some module times may have been set in minutes so we + // need to account for that. + $ttl = $ttl <= 120 ? $ttl : ($ttl / 60); + + App::get('cache.store')->put($key, $content, $ttl); + } + + return $content; + } +} diff --git a/core/libraries/Hubzero/Notification/Handler.php b/core/libraries/Hubzero/Notification/Handler.php new file mode 100644 index 00000000000..fa76f2b07f1 --- /dev/null +++ b/core/libraries/Hubzero/Notification/Handler.php @@ -0,0 +1,209 @@ +storage = $storage; + } + + /** + * Flash an information message. + * + * @param string $message + * @param string $domain + * @return object + */ + public function info($message, $domain = null) + { + $this->message($message, 'info', $domain); + + return $this; + } + + /** + * Flash a success message. + * + * @param string $message + * @param string $domain + * @return object + */ + public function success($message, $domain = null) + { + $this->message($message, 'success', $domain); + + return $this; + } + + /** + * Flash an error message. + * + * @param string $message + * @param string $domain + * @return object + */ + public function error($message, $domain = null) + { + $this->message($message, 'error', $domain); + + return $this; + } + + /** + * Flash a warning message. + * + * @param string $message + * @param string $domain + * @return object + */ + public function warning($message, $domain = null) + { + $this->message($message, 'warning', $domain); + + return $this; + } + + /** + * Flash a general message. + * + * @param string $message + * @param string $type + * @param string $domain + * @return $this + */ + public function message($message, $type = 'info', $domain = null) + { + $messages = $this->storage->retrieve($domain); + + $duplicate = false; + + foreach ($messages as $m) + { + // If all the data is the same, + // it's a duplicate message. Skip. + if ($m['message'] == $message + && $m['type'] == $type) + { + $duplicate = true; + } + + $this->storage->store($m, $domain); + } + + if (!$duplicate) + { + $this->storage->store( + array( + 'message' => $message, + 'type' => $type + ), + $domain + ); + } + + return $this; + } + + /** + * Check if there are any messages + * + * @param string $domain + * @return boolean + */ + public function isEmpty($domain = null) + { + return !$this->any($domain); + } + + /** + * Check if there are any messages + * + * @param string $domain + * @return boolean + */ + public function any($domain = null) + { + return ($this->storage->total($domain) > 0); + } + + /** + * Get all messages + * + * @param string $domain + * @return array + */ + public function messages($domain = null) + { + return $this->storage->retrieve($domain); + } + + /** + * Clear all messages + * + * @param string $domain + * @return object + */ + public function clear($domain = null) + { + $this->storage->retrieve($domain); + + return $this; + } + + /** + * Get the instance as an array. + * + * @param string $domain + * @return array + */ + public function toArray($domain = null) + { + return $this->messages($domain); + } + + /** + * Convert the object to its JSON representation. + * + * @param integer $options + * @param string $domain + * @return string + */ + public function toJson($options = 0, $domain = null) + { + return json_encode($this->toArray($domain), $options); + } + + /** + * Convert the message bag to its string representation. + * + * @return string + */ + public function __toString() + { + return $this->toJson(); + } +} diff --git a/core/libraries/Hubzero/Notification/MessageStore.php b/core/libraries/Hubzero/Notification/MessageStore.php new file mode 100644 index 00000000000..9658ac4d71d --- /dev/null +++ b/core/libraries/Hubzero/Notification/MessageStore.php @@ -0,0 +1,48 @@ +lifetime = $lifetime; + } + + /** + * Store a message + * + * @param array $data + * @param string $domain + * @return void + */ + public function store($data, $domain) + { + $messages = (array) $this->retrieve($domain); + $messages[] = $data; + + Monster::bake($this->key($domain), $this->expires($this->lifetime), $messages); + } + + /** + * Return a list of messages + * + * @param array $data + * @param string $domain + * @return array + */ + public function retrieve($domain) + { + if (!($messages = Monster::eat($this->key($domain)))) + { + $messages = array(); + } + + if (count($messages)) + { + $this->clear($domain); + } + + return $messages; + } + + /** + * Clear all messages + * + * @param string $domain + * @return void + */ + public function clear($domain) + { + Monster::bake($this->key($domain), $this->expires(0), array()); + } + + /** + * Return a count of messages + * + * @param string $domain + * @return integer + */ + public function total($domain) + { + return count($this->retrieve($domain)); + } + + /** + * Get the storage key + * + * @param string $domain + * @return string + */ + private function key($domain) + { + $domain = (!$domain ? '' : $domain . '.'); + + return md5($domain . 'application.queue'); + } + + /** + * Get the expiration time a # of minutes from now + * + * @param itneger $minutes + * @return integer + */ + private function expires($minutes) + { + return time() + 60 * $minutes; + } +} diff --git a/core/libraries/Hubzero/Notification/Storage/Memory.php b/core/libraries/Hubzero/Notification/Storage/Memory.php new file mode 100644 index 00000000000..1a43032bfec --- /dev/null +++ b/core/libraries/Hubzero/Notification/Storage/Memory.php @@ -0,0 +1,110 @@ +messages = array(); + } + + /** + * Store a message + * + * @param array $data + * @param string $domain + * @return void + */ + public function store($data, $domain) + { + $messages = (array) $this->retrieve($domain); + $messages[] = $data; + + $this->messages[$this->key($domain)] = $messages; + } + + /** + * Return a list of messages + * + * @param array $data + * @param string $domain + * @return array + */ + public function retrieve($domain) + { + $key = $this->key($domain); + + $messages = isset($this->messages[$key]) ? $this->messages[$key] : array(); + + if (count($messages)) + { + $this->clear($domain); + } + + return $messages; + } + + /** + * Clear all messages + * + * @param string $domain + * @return void + */ + public function clear($domain) + { + $key = $this->key($domain); + + $this->messages[$key] = array(); + } + + /** + * Return a count of messages + * + * @param string $domain + * @return integer + */ + public function total($domain) + { + $key = $this->key($domain); + + $messages = isset($this->messages[$key]) ? $this->messages[$key] : array(); + + return count($messages); + } + + /** + * Get the storage key + * + * @param string $domain + * @return string + */ + private function key($domain) + { + $domain = (!$domain ? '' : $domain . '.'); + + return $domain . 'application.queue'; + } +} diff --git a/core/libraries/Hubzero/Notification/Storage/None.php b/core/libraries/Hubzero/Notification/Storage/None.php new file mode 100644 index 00000000000..d87fb2fef6f --- /dev/null +++ b/core/libraries/Hubzero/Notification/Storage/None.php @@ -0,0 +1,62 @@ +session = $session; + } + + /** + * Store a message + * + * @param array $data + * @param string $domain + * @return void + */ + public function store($data, $domain) + { + $messages = (array) $this->retrieve($domain); + $messages[] = $data; + + $this->session->set($this->key($domain), $messages); + } + + /** + * Return a list of messages + * + * @param array $data + * @param string $domain + * @return array + */ + public function retrieve($domain) + { + $messages = $this->session->get($this->key($domain), array()); + + if (count($messages)) + { + $this->clear($domain); + } + + return $messages; + } + + /** + * Clear all messages + * + * @param string $domain + * @return void + */ + public function clear($domain) + { + $this->session->set($this->key($domain), null); + } + + /** + * Return a count of messages + * + * @param string $domain + * @return integer + */ + public function total($domain) + { + $messages = $this->session->get($this->key($domain), array()); + + return count($messages); + } + + /** + * Get the storage key + * + * @param string $domain + * @return string + */ + private function key($domain) + { + $domain = (!$domain ? '' : $domain . '.'); + + return $domain . 'application.queue'; + } +} diff --git a/core/libraries/Hubzero/Notification/Tests/HandlerTest.php b/core/libraries/Hubzero/Notification/Tests/HandlerTest.php new file mode 100755 index 00000000000..1d63a52af04 --- /dev/null +++ b/core/libraries/Hubzero/Notification/Tests/HandlerTest.php @@ -0,0 +1,328 @@ + 'This is an info message.', + 'type' => 'info', + 'domain' => null + ), + array( + 'message' => 'This is a success message!', + 'type' => 'success', + 'domain' => null + ), + array( + 'message' => 'This is a warning message!', + 'type' => 'warning', + 'domain' => null + ), + array( + 'message' => 'This is an error message.', + 'type' => 'error', + 'domain' => null + ) + ); + + /** + * Test that the lit of messages returned by the handler + * + * @covers \Hubzero\Notification\Handler::message + * @return void + **/ + public function testMessage() + { + $handler = new Handler(new Memory); + + $item = $this->data[0]; + + $this->assertInstanceOf('Hubzero\Notification\Handler', $handler->message($item['message'])); + + $messages = $handler->messages(); + + $this->assertTrue(is_array($messages), 'Getting all messages should return an array'); + $this->assertCount(1, $messages, 'Total messages returned does not equal number added'); + + $handler = new Handler(new Memory); + $handler->message($item['message'], 'info'); + $handler->message($item['message'], 'warning'); + $handler->message($item['message'], 'info'); + + $messages = $handler->messages(); + + $this->assertCount(2, $messages, 'Duplicate message+type combinations should not be added'); + + // We should have only one 'info' and one 'warning' + $info = 0; + $warning = 0; + $msg = 0; + foreach ($messages as $message) + { + if ($message['message'] == $item['message']) + { + $msg++; + } + if ($message['type'] == 'info') + { + $info++; + } + if ($message['type'] == 'warning') + { + $warning++; + } + } + + $this->assertEquals($msg, 2); + $this->assertEquals($info, 1); + $this->assertEquals($warning, 1); + } + + /** + * Test that the lit of messages returned by the handler + * + * @covers \Hubzero\Notification\Handler::messages + * @return void + **/ + public function testMessages() + { + $handler = new Handler(new Memory); + + foreach ($this->data as $item) + { + $handler->message($item['message'], $item['type'], $item['domain']); + } + + $messages = $handler->messages(); + + $this->assertTrue(is_array($messages), 'Getting all messages should return an array'); + $this->assertCount(count($this->data), $messages, 'Total messages returned does not equal number added'); + } + + /** + * Tests clear() empties the message bag + * + * @covers \Hubzero\Notification\Handler::clear + * @return void + **/ + public function testClear() + { + $handler = new Handler(new Memory); + + foreach ($this->data as $item) + { + $handler->message($item['message'], $item['type'], 'one'); + } + + foreach ($this->data as $item) + { + $handler->message($item['message'], $item['type'], 'two'); + } + + $handler->clear('one'); + + $this->assertTrue($handler->isEmpty('one')); + $this->assertFalse($handler->any('one')); + $m = $handler->messages('one'); + $this->assertCount(0, $m, 'Total messages returned does not equal number added'); + + $this->assertFalse($handler->isEmpty('two')); + $this->assertTrue($handler->any('two')); + $m = $handler->messages('two'); + $this->assertCount(count($this->data), $m, 'Total messages returned does not equal number added'); + } + + /** + * Test that messages added with info() are + * assigned the appropriate type. + * + * @covers \Hubzero\Notification\Handler::info + * @return void + **/ + public function testInfo() + { + $handler = new Handler(new Memory); + $handler->info('Lorem ipsum dol.'); + + $m = $handler->messages(); + $message = array_pop($m); + + $this->assertTrue(is_array($message), 'Individual messages should be of type array'); + $this->assertEquals($message['type'], 'info'); + } + + /** + * Test that messages added with success() are + * assigned the appropriate type. + * + * @covers \Hubzero\Notification\Handler::success + * @return void + **/ + public function testSuccess() + { + $handler = new Handler(new Memory); + $handler->success('Lorem ipsum dol.'); + + $m = $handler->messages(); + $message = array_pop($m); + + $this->assertTrue(is_array($message), 'Individual messages should be of type array'); + $this->assertEquals($message['type'], 'success'); + } + + /** + * Test that messages added with warning() are + * assigned the appropriate type. + * + * @covers \Hubzero\Notification\Handler::warning + * @return void + **/ + public function testWarning() + { + $handler = new Handler(new Memory); + $handler->warning('Lorem ipsum dol.'); + + $m = $handler->messages(); + $message = array_pop($m); + + $this->assertTrue(is_array($message), 'Individual messages should be of type array'); + $this->assertEquals($message['type'], 'warning'); + } + + /** + * Test that messages added with error() are + * assigned the appropriate type. + * + * @covers \Hubzero\Notification\Handler::error + * @return void + **/ + public function testError() + { + $handler = new Handler(new Memory); + $handler->error('Lorem ipsum dol.'); + + $m = $handler->messages(); + $message = array_pop($m); + + $this->assertTrue(is_array($message), 'Individual messages should be of type array'); + $this->assertEquals($message['type'], 'error'); + } + + /** + * Test that any() returns FALSE if there are no + * messages and TRUE if there. + * + * @covers \Hubzero\Notification\Handler::any + * @return void + **/ + public function testAny() + { + $handler = new Handler(new Memory); + + $this->assertFalse($handler->any()); + + $handler->error('Lorem ipsum dol.'); + + $this->assertTrue($handler->any()); + } + + /** + * Test that isEmpty() returns TRUE if there are no + * messages and FALSE if there. + * + * @covers \Hubzero\Notification\Handler::isEmpty + * @return void + **/ + public function testIsEmpty() + { + $handler = new Handler(new Memory); + + $this->assertTrue($handler->isEmpty()); + + $handler->error('Lorem ipsum dol.'); + + $this->assertFalse($handler->isEmpty()); + } + + /** + * Test that toArray() returns an array of messages + * + * @covers \Hubzero\Notification\Handler::toArray + * @return void + **/ + public function testToArray() + { + $handler = new Handler(new Memory); + + foreach ($this->data as $item) + { + $handler->message($item['message'], $item['type'], $item['domain']); + } + + $messages = $handler->toArray(); + + $this->assertTrue(is_array($messages), 'Getting all messages should return an array'); + $this->assertCount(count($this->data), $messages, 'Total messages returned does not equal number added'); + } + + /** + * Test that toJson() returns a JSON string + * + * @covers \Hubzero\Notification\Handler::toJson + * @return void + **/ + public function testToJson() + { + $handler = new Handler(new Memory); + + foreach ($this->data as $item) + { + $handler->message($item['message'], $item['type'], $item['domain']); + } + + $messages = $handler->toJson(); + + $this->assertTrue(is_string($messages)); + $this->assertJson($messages); + } + + /** + * Test __toString + * + * @covers \Hubzero\Notification\Handler::__toString + * @return void + **/ + public function testToString() + { + $handler = new Handler(new Memory); + + foreach ($this->data as $item) + { + $handler->message($item['message'], $item['type'], $item['domain']); + } + + $messages = (string) $handler; + + $this->assertTrue(is_string($messages)); + $this->assertJson($messages); + } +} diff --git a/core/libraries/Hubzero/Notification/Tests/Storage/MemoryTest.php b/core/libraries/Hubzero/Notification/Tests/Storage/MemoryTest.php new file mode 100644 index 00000000000..619b83e6b09 --- /dev/null +++ b/core/libraries/Hubzero/Notification/Tests/Storage/MemoryTest.php @@ -0,0 +1,183 @@ + 'This is an info message.', + 'type' => 'info', + 'domain' => 'test' + ), + array( + 'message' => 'This is a success message!', + 'type' => 'success', + 'domain' => 'test' + ), + array( + 'message' => 'This is a warning message!', + 'type' => 'warning', + 'domain' => 'test' + ), + array( + 'message' => 'This is an error message.', + 'type' => 'error', + 'domain' => 'test' + ) + ); + + /** + * Test that the constructor provides an empty message bag + * + * @covers \Hubzero\Notification\Storage\Memory::__construct + * @return void + **/ + public function testConstructor() + { + $memory = new Memory; + + $this->assertInstanceOf('Hubzero\Notification\MessageStore', $memory); + $this->assertEquals(0, $memory->total('test'), 'Total messages returned does not equal number added'); + } + + /** + * Test that the store() method adds to the internal list + * + * @covers \Hubzero\Notification\Storage\Memory::store + * @return void + **/ + public function testStore() + { + $memory = new Memory; + + foreach ($this->data as $item) + { + $memory->store($item, $item['domain']); + } + + $messages = $memory->retrieve('test'); + $this->assertCount(count($this->data), $messages, 'Total messages returned does not equal number added'); + + foreach ($messages as $i => $message) + { + $this->assertEquals($this->data[$i]['message'], $message['message']); + $this->assertEquals($this->data[$i]['type'], $message['type']); + } + } + + /** + * Test that the lit of messages returned by the handler + * + * @covers \Hubzero\Notification\Storage\Memory::retrieve + * @return void + **/ + public function testRetrieve() + { + $memory = new Memory; + + foreach ($this->data as $item) + { + $memory->store($item, $item['domain']); + } + + $messages = $memory->retrieve('test'); + + $this->assertTrue(is_array($messages), 'Getting all messages should return an array'); + $this->assertCount(count($this->data), $messages, 'Total messages returned does not equal number added'); + } + + /** + * Tests clear() empties the message bag + * + * @covers \Hubzero\Notification\Storage\Memory::clear + * @return void + **/ + public function testClear() + { + $memory = new Memory; + + foreach ($this->data as $item) + { + $memory->store($item, 'one'); + } + + foreach ($this->data as $item) + { + $memory->store($item, 'two'); + } + + $memory->clear('one'); + + $messages = $memory->retrieve('one'); + + $this->assertCount(0, $messages, 'Total messages returned does not equal number added'); + + $messages = $memory->retrieve('two'); + + $this->assertCount(count($this->data), $messages, 'Total messages returned does not equal number added'); + } + + /** + * Test that the total() method returns the correct count + * + * @covers \Hubzero\Notification\Storage\Memory::total + * @return void + **/ + public function testTotal() + { + $memory = new Memory; + + foreach ($this->data as $item) + { + $memory->store($item, 'one'); + } + + foreach ($this->data as $item) + { + $memory->store($item, 'two'); + } + + $this->assertEquals(count($this->data), $memory->total('one'), 'Total messages returned does not equal number added'); + } + + /** + * Test that the key() method generates keys correctly + * + * @covers \Hubzero\Notification\Storage\Memory::key + * @return void + **/ + public function testKey() + { + $memory = new Memory; + + $reflection = new \ReflectionClass(get_class($memory)); + $method = $reflection->getMethod('key'); + $method->setAccessible(true); + + $result = $method->invokeArgs($memory, array('one')); + + $this->assertEquals('one.application.queue', $result, 'Key should be of pattern {domain}.application.queue'); + + $result = $method->invokeArgs($memory, array('')); + + $this->assertEquals('application.queue', $result, 'Key should just be application.queue'); + } +} diff --git a/core/libraries/Hubzero/Oauth/GrantType/AuthorizationCode.php b/core/libraries/Hubzero/Oauth/GrantType/AuthorizationCode.php new file mode 100644 index 00000000000..abcdd2e6472 --- /dev/null +++ b/core/libraries/Hubzero/Oauth/GrantType/AuthorizationCode.php @@ -0,0 +1,171 @@ +storage = $storage; + } + + /** + * Define identifier for this type of grant + * + * @return string identifier + */ + public function getQuerystringIdentifier() + { + return 'authorization_code'; + } + + /** + * Validate request via client + * + * @param object $request Request object + * @param object $response Response object + * @return bool Result of auth + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + // make sure we have a code param + if (!$code = $request->request('code')) + { + $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required'); + return false; + } + + // verify code param + if (!$authCode = $this->storage->getAuthorizationCode($code)) + { + $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client'); + return false; + } + + // make sure "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request + if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) + { + if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $authCode['redirect_uri']) + { + $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); + return false; + } + } + + // must have expiration + if (!isset($authCode['expires'])) + { + throw new \Exception('Storage must return authcode with a value for "expires"'); + } + + // checkk code isnt expired + if ($authCode["expires"] < time()) + { + $response->setError(400, 'invalid_grant', "The authorization code has expired"); + return false; + } + + // make sure have the actual auth code + if (!isset($authCode['code'])) + { + $authCode['code'] = $code; // used to expire the code after the access token is granted + } + + // store locally + $this->authCode = $authCode; + return true; + } + + /** + * Get client id + * + * @return null + */ + public function getClientId() + { + return $this->authCode['client_id']; + } + + /** + * Get user id + * + * @return int User identifier + */ + public function getUserId() + { + return isset($this->authCode['uidNumber']) ? $this->authCode['uidNumber'] : null; + } + + /** + * Get scope + * + * @return string Scope + */ + public function getScope() + { + return isset($this->authCode['scope']) ? $this->authCode['scope'] : null; + } + + /** + * Create access token + * + * @param object $accessToken Access token object + * @param string $client_id Authorized client + * @param string $user_id User identifier + * @param string $scope Client application scope + * @return string Access token + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + // create access token + $token = $accessToken->createAccessToken($client_id, $user_id, $scope); + + // expire auth code + $this->storage->expireAuthorizationCode($this->authCode['code']); + + // return token + return $token; + } +} diff --git a/core/libraries/Hubzero/Oauth/GrantType/ClientCredentials.php b/core/libraries/Hubzero/Oauth/GrantType/ClientCredentials.php new file mode 100644 index 00000000000..b071460a2c0 --- /dev/null +++ b/core/libraries/Hubzero/Oauth/GrantType/ClientCredentials.php @@ -0,0 +1,184 @@ +storage = $storage; + $this->config = array_merge(array( + 'allow_credentials_in_request_body' => true, + ), $config); + + // force public clients off + $config['allow_public_clients'] = false; + } + + /** + * Define identifier for this type of grant + * + * @return string identifier + */ + public function getQuerystringIdentifier() + { + return 'client_credentials'; + } + + /** + * Validate request via client + * + * @param object $request Request object + * @param object $response Response object + * @return bool Result of auth + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + // check HTTP basic auth headers for client id/secret + if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) + { + $clientData = array( + 'client_id' => $request->headers('PHP_AUTH_USER'), + 'client_secret' => $request->headers('PHP_AUTH_PW') + ); + } + + // if we allow credentials via request body look there + if ($this->config['allow_credentials_in_request_body']) + { + // check for client id in request + if (!is_null($request->request('client_id'))) + { + $clientData = array( + 'client_id' => $request->request('client_id'), + 'client_secret' => $request->request('client_secret') + ); + } + } + + // must have client id + if (!isset($clientData['client_id']) || $clientData['client_id'] == '') + { + $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : ''; + $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message); + return false; + } + + // check to see if we have client secret + if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') + { + // invalid if we dont have client secret and public clients are off + if (!$this->config['allow_public_clients']) + { + $response->setError(400, 'invalid_client', 'client credentials are required'); + return false; + } + + // check storage if client is public client + if (!$this->storage->isPublicClient($clientData['client_id'])) + { + $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret'); + return false; + } + } + // if we do have a secret lets verify them + elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) + { + $response->setError(400, 'invalid_client', 'The client credentials are invalid'); + return false; + } + + // store data locally + $this->clientData = $clientData; + return true; + } + + /** + * Get client id + * + * @return null + */ + public function getClientId() + { + return $this->clientData['client_id']; + } + + /** + * Get user id + * + * @return int User identifier + */ + public function getUserId() + { + return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null; + } + + /** + * Get scope + * + * @return string Scope + */ + public function getScope() + { + return isset($this->clientData['scope']) ? $this->clientData['scope'] : null; + } + + /** + * Create access token + * + * @param object $accessToken Access token object + * @param string $client_id Authorized client + * @param string $user_id User identifier + * @param string $scope Client application scope + * @return string Access token + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + // create access token + // DONT CREATE REFRESH TOKEN + return $accessToken->createAccessToken($client_id, $user_id, $scope, false); + } +} diff --git a/core/libraries/Hubzero/Oauth/GrantType/RefreshToken.php b/core/libraries/Hubzero/Oauth/GrantType/RefreshToken.php new file mode 100644 index 00000000000..b8f098c0bef --- /dev/null +++ b/core/libraries/Hubzero/Oauth/GrantType/RefreshToken.php @@ -0,0 +1,158 @@ +storage = $storage; + $this->config = array_merge(array( + 'always_issue_new_refresh_token' => false + ), $config); + } + + /** + * Define identifier for this type of grant + * + * @return string identifier + */ + public function getQuerystringIdentifier() + { + return 'refresh_token'; + } + + /** + * Exchange refresh token or new access token + * + * @param object $request Request object + * @param object $response Response object + * @return bool Result of auth + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + // make sure request has a refresh token + if (!$request->request("refresh_token")) + { + $response->setError(400, 'invalid_request', 'Missing parameter: "refresh_token" is required'); + return null; + } + + // load token details + if (!$refreshToken = $this->storage->getRefreshToken($request->request("refresh_token"))) + { + $response->setError(400, 'invalid_grant', 'Invalid refresh token'); + return null; + } + + // make sure token hasnt expired + if ($refreshToken['expires'] > 0 && $refreshToken["expires"] < time()) + { + $response->setError(400, 'invalid_grant', 'Refresh token has expired'); + return null; + } + + // store the refresh token locally so we can delete it when a new refresh token is generated + $this->refreshToken = $refreshToken; + return true; + } + + /** + * Get client id + * + * @return null + */ + public function getClientId() + { + return $this->refreshToken['client_id']; + } + + /** + * Get user id + * + * @return int User identifier + */ + public function getUserId() + { + return isset($this->refreshToken['uidNumber']) ? $this->refreshToken['uidNumber'] : null; + } + + /** + * Get scope + * + * @return string Scope + */ + public function getScope() + { + return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null; + } + + /** + * Create access token + * + * @param object $accessToken Access token object + * @param string $client_id Authorized client + * @param string $user_id User identifier + * @param string $scope Client application scope + * @return string Access token + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + // do we want to issue a new refresh token? + $issueNewRefreshToken = $this->config['always_issue_new_refresh_token']; + + // create access token + $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken); + + // if we issued a new refresh token we must delete the old one + if ($issueNewRefreshToken) + { + $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']); + } + + // return new access token + return $token; + } +} diff --git a/core/libraries/Hubzero/Oauth/GrantType/SessionToken.php b/core/libraries/Hubzero/Oauth/GrantType/SessionToken.php new file mode 100644 index 00000000000..54b09a30a28 --- /dev/null +++ b/core/libraries/Hubzero/Oauth/GrantType/SessionToken.php @@ -0,0 +1,143 @@ +storage = $storage; + } + + /** + * Validate request via session data + * + * This is used for internal requests via ajax + * + * @param object $request Request object + * @param object $response Response object + * @return bool Result of auth + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + // check for session id + if (!$sessionId = $this->storage->getSessionIdFromCookie()) + { + $response->setError(401, 'session_authentication_invalid', 'Unable to find a valid session id.'); + return false; + } + + // get user for session id + if (!$userId = $this->storage->getUserIdFromSessionId($sessionId)) + { + $response->setError(401, 'session_authentication_invalid', 'Unable to authenticate via active session.'); + return false; + } + + // store our session & user id + $this->userInfo = array( + 'user_id' => $userId, + 'session_id' => $sessionId + ); + return true; + } + + /** + * Get client id + * + * @return null + */ + public function getClientId() + { + // load internal request client + $client = $this->storage->getInternalRequestClient(); + + // return client id + return isset($client['client_id']) ? $client['client_id'] : null; + } + + /** + * Get user id + * + * @return int User identifier + */ + public function getUserId() + { + return $this->userInfo['user_id']; + } + + /** + * Get scope + * + * @return string Scope + */ + public function getScope() + { + return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; + } + + /** + * Create access token + * + * @param object $accessToken Access token object + * @param string $client_id Authorized client + * @param string $user_id User identifier + * @param string $scope Client application scope + * @return string Access token + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + return $accessToken->createAccessToken($client_id, $user_id, $scope, false); + } +} diff --git a/core/libraries/Hubzero/Oauth/GrantType/ToolSessionToken.php b/core/libraries/Hubzero/Oauth/GrantType/ToolSessionToken.php new file mode 100644 index 00000000000..bbd810a3b6a --- /dev/null +++ b/core/libraries/Hubzero/Oauth/GrantType/ToolSessionToken.php @@ -0,0 +1,143 @@ +storage = $storage; + } + + /** + * Validate request via session data + * + * This is used for internal requests via ajax + * + * @param object $request Request object + * @param object $response Response object + * @return bool Result of auth + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + // make sure we have tool session data + if (!$toolData = $this->storage->getToolSessionDataFromRequest($request)) + { + $response->setError(401, 'tool_session_authentication_invalid', 'Unable to find valid tool session data.'); + return false; + } + + // validate tool session data + if (!$userId = $this->storage->validateToolSessionData($toolData['toolSessionId'], $toolData['toolSessionToken'])) + { + $response->setError(401, 'tool_session_authentication_invalid', 'Unable to find valid tool session data.'); + return false; + } + + // store user info locally + $this->userInfo = [ + 'user_id' => $userId, + 'scope' => '' + ]; + return true; + } + + /** + * Get client id + * + * @return null + */ + public function getClientId() + { + // load internal request client + $client = $this->storage->getInternalRequestClient(); + + // return client id + return isset($client['client_id']) ? $client['client_id'] : null; + } + + /** + * Get user id + * + * @return int User identifier + */ + public function getUserId() + { + return $this->userInfo['user_id']; + } + + /** + * Get scope + * + * @return string Scope + */ + public function getScope() + { + return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; + } + + /** + * Create access token + * + * @param object $accessToken Access token object + * @param string $client_id Authorized client + * @param string $user_id User identifier + * @param string $scope Client application scope + * @return string Access token + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + return $accessToken->createAccessToken($client_id, $user_id, $scope, false); + } +} diff --git a/core/libraries/Hubzero/Oauth/GrantType/UserCredentials.php b/core/libraries/Hubzero/Oauth/GrantType/UserCredentials.php new file mode 100644 index 00000000000..2b001393bd3 --- /dev/null +++ b/core/libraries/Hubzero/Oauth/GrantType/UserCredentials.php @@ -0,0 +1,148 @@ +storage = $storage; + } + + /** + * Define identifier for this type of grant + * + * @return string identifier + */ + public function getQuerystringIdentifier() + { + return 'password'; + } + + /** + * Validate request via session data + * + * This is used for internal requests via ajax + * + * @param object $request Request object + * @param object $response Response object + * @return bool Result of auth + */ + public function validateRequest(RequestInterface $request, ResponseInterface $response) + { + // ensure we have needed params + if (!$request->request("password") || !$request->request("username")) + { + $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required'); + return null; + } + + // check username/password + if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) + { + $response->setError(401, 'invalid_grant', 'Invalid username and password combination'); + return null; + } + + // get user details by username + $userInfo = $this->storage->getUserDetails($request->request("username")); + + // make sure we got an array of user details + if (empty($userInfo)) + { + $response->setError(400, 'invalid_grant', 'Unable to retrieve user information'); + return null; + } + + // if not set, something went wrong + if (!isset($userInfo['user_id'])) + { + throw new \LogicException("you must set the user_id on the array returned by getUserDetails"); + } + + // set our userinfo for later use + $this->userInfo = $userInfo; + + // return sucess + return true; + } + + /** + * Get client id + * + * @return null + */ + public function getClientId() + { + return null; + } + + /** + * Get user id + * + * @return int User identifier + */ + public function getUserId() + { + return $this->userInfo['user_id']; + } + + /** + * Get scope + * + * @return string Scope + */ + public function getScope() + { + return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; + } + + /** + * Create access token + * + * @param object $accessToken Access token object + * @param string $client_id Authorized client + * @param string $user_id User identifier + * @param string $scope Client application scope + * @return string Access token + */ + public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) + { + return $accessToken->createAccessToken($client_id, $user_id, $scope); + } +} diff --git a/core/libraries/Hubzero/Oauth/Provider.php b/core/libraries/Hubzero/Oauth/Provider.php new file mode 100644 index 00000000000..f6f511a283a --- /dev/null +++ b/core/libraries/Hubzero/Oauth/Provider.php @@ -0,0 +1,453 @@ +_request_token_path = trim($path, '/'); + } + + /** + * Set access token path + * + * @param string $path + * @return void + */ + public function setAccessTokenPath($path) + { + $this->_access_token_path = trim($path, '/'); + } + + /** + * Set authorize path + * + * @param string $path + * @return void + */ + public function setAuthorizePath($path) + { + $this->_authorize_path = trim($path, '/'); + } + + /** + * Constructor + * + * @param array $params + * @return void + */ + public function __construct($params = array()) + { + if (!class_exists('OAuthProvider')) + { + throw new \Exception('OAuthProvider class not found.', 500); + } + + $this->_provider = new OAuthProvider($params); + + $this->_provider->consumerHandler(array($this,'consumerHandler')); + $this->_provider->timestampNonceHandler(array($this,'timestampNonceHandler')); + $this->_provider->tokenHandler(array($this, 'tokenHandler')); + } + + /** + * Validate a request + * + * @param string $uri + * @param string $method + * @return boolean + */ + public function validateRequest($uri = null, $method = null) + { + $endpoint = false; + + if (is_null($uri)) + { + $uri = ""; + } + + if (is_null($method)) + { + $method = $_SERVER['REQUEST_METHOD']; + } + + $parts = parse_url($uri); + + $path = trim($parts['path'], '/'); + + if ($path == $this->_request_token_path) + { + $this->_provider->isRequestTokenEndpoint(true); + } + else if ($path == $this->_access_token_path) + { + $header = ''; + + if (isset($_SERVER['HTTP_AUTHORIZATION'])) + { + $header = $_SERVER['HTTP_AUTHORIZATION']; + } + + // @FIXME: header check is inexact and could give false positives + // @FIXME: pecl oauth provider doesn't handle x_auth in header + // @FIXME: api application should convert xauth variables in + // header to form/query data as workaround + // @FIXME: this code is here for future use if/when pecl oauth + // provider is fixed + + if (isset($_GET['x_auth_mode']) + || isset($_GET['x_auth_username']) + || isset($_GET['x_auth_password']) + || isset($_POST['x_auth_mode']) + || isset($_POST['x_auth_username']) + || isset($_POST['x_auth_password']) + || !strpos($header, 'x_auth_mode') + || !strpos($header, 'x_auth_username') + || !strpos($header, 'x_auth_password')) + { + $this->_provider->is2LeggedEndpoint(true); + //$this->_provider->addRequiredParameter ('x_auth_mode'); + //$this->_provider->addRequiredParameter ('x_auth_username'); + //$this->_provider->addRequiredParameter ('x_auth_password'); + } + } + + try + { + $this->_provider->checkOAuthRequest($uri, $method); + + return true; + } + catch (OAuthException $E) + { + } + + // No attempt was made to sign this, let it pass as such + if (($this->_provider->consumer_key === null) + && ($this->_provider->consumer_secret === null) + && ($this->_provider->nonce === null) + && ($this->_provider->token === null) + && ($this->_provider->token_secret === null) + && ($this->_provider->timestamp === null) + && ($this->_provider->version === null) + && ($this->_provider->signature_method === null) + && ($this->_provider->callback === null) + && (empty($this->_provider->signature)) + ) + { + return true; + } + + // request to authorize path can have token and callback params, but are unsigned + if ($path == $this->_authorize_path) + { + if (($this->_provider->consumer_key === null) + && ($this->_provider->consumer_secret === null) + && ($this->_provider->nonce === null) + && ($this->_provider->token_secret === null) + && ($this->_provider->timestamp === null) + && ($this->_provider->version === null) + && ($this->_provider->signature_method === null) + && (empty($this->_provider->signature)) + ) + { + return true; + } + } + + $message = OAuthProvider::reportProblem($E, false); + + // request signed without token is allowed to pass + if ($message == "oauth_problem=token_rejected") + { + if (($this->_provider->consumer_key !== null) + && ($this->_provider->consumer_secret !== null) + && ($this->_provider->nonce !== null) + && (empty($this->_provider->token)) + && (empty($this->_provider->token_secret)) + && ($this->_provider->timestamp !== null) + && ($this->_provider->version !== null) + && ($this->_provider->signature_method !== null) + && (!empty($this->_provider->signature)) + ) + { + return true; + } + } + + $status = 401; + $reason = 'Unauthorized'; + + if ($message == "oauth_problem=signature_method_rejected") + { + $reason = 'Bad Request'; + $status = 400; + } + else if (strpos($message, "oauth_problem=parameter_absent") !== false) + { + $reason = 'Bad Request'; + $status = 400; + } + else if ($message == "oauth_problem=unknown_problem&code=503") + { + $reason = 'Bad Request'; + $status = 400; + } + + $result['message'] = $message; + $result['status'] = $status; + $result['reason'] = $reason; + + return $result; + } + + /** + * Get token + * + * @return string + */ + public function getToken() + { + return $this->_provider->token; + } + + /** + * Get consumer key + * + * @return string + */ + public function getConsumerKey() + { + return $this->_provider->consumer_key; + } + + /** + * Get consumer data + * + * @return string + */ + public function getConsumerData() + { + return $this->_consumer_data; + } + + /** + * Get token data + * + * @return string + */ + public function getTokenData() + { + return $this->_token_data; + } + + /** + * OAuthProvider consumerHandler Callback + * + * Lookup requested consumer key secret + * + * Result is stored in OAuthProvider instance's consumer_secret property + * Consumer data record is stored in _consumer_data property + * + * @return OAUTH_OK on success + * If consumer_key doesn't exist returns OAUTH_CONSUMER_KEY_UNKNOWN + * If consumer_key is expired or otherwise invalid returns OAUTH_CONSUMER_KEY_REFUSED + * If lookup process failed for some reason returns OAUTH_ERR_INTERNAL_ERROR + */ + public function consumerHandler() + { + $db = \App::get('db'); + + if (!is_object($db)) + { + return OAUTH_ERR_INTERNAL_ERROR; + } + + $db->setQuery("SELECT * FROM `#__oauthp_consumers` WHERE token=" . $db->quote($this->_provider->consumer_key) . " LIMIT 1;"); + + $result = $db->loadObject(); + + if ($result === false) // query failed + { + return OAUTH_ERR_INTERNAL_ERROR; + } + + if (empty($result)) // key not found + { + return OAUTH_CONSUMER_KEY_UNKNOWN; + } + + if ($result->state != 1) // key not in a valid state + { + return OAUTH_CONSUMER_KEY_REFUSED; + } + + $this->_consumer_data = $result; + $this->_provider->consumer_secret = $result->secret; + + return OAUTH_OK; + } + + /** + * OAuthProvider timestampNonceHandler Callback + * + * Validate timestamp and nonce assocaited with OAuthProvider instance + * + * @return OAUTH_OK on success + * If timestamp is invalid (expired) returns OAUTH_BAD_TIMESTAMP + * If nonce has been seen before returns OAUTH_BAD_NONCE + * If lookup process failed for some reason returns OAUTH_ERR_INTERNAL_ERROR + */ + public function timestampNonceHandler() + { + $timediff = abs(time() - $this->_provider->timestamp); + + if ($timediff > 600) + { + return OAUTH_BAD_TIMESTAMP; + } + + $db = \App::get('db'); + + if (!is_object($db)) + { + return OAUTH_ERR_INTERNAL_ERROR; + } + + $db->setQuery( + "INSERT INTO `#__oauthp_nonces` (nonce,stamp,created) " + . " VALUES (" . + $db->quote($this->_provider->nonce) . + "," . + $db->quote($this->_provider->timestamp) . + ", UTC_TIMESTAMP());" + ); + + if (($db->query() === false) && ($db->getErrorNum() != 1062)) // duplicate row error ok (well expected anyway) + { + return OAUTH_ERR_INTERNAL_ERROR; + } + + if ($db->getAffectedRows() < 1) // duplicate row error throws this error instead + { + return OAUTH_BAD_NONCE; + } + + return OAUTH_OK; + } + + /** + * OAuthProvider tokenHandler Callback + * + * Lookup token data associated with OAuthProvider instance + * + * If token is valid stores full token record in _token_data property + * + * @return OAUTH_OK on success + * If token not found returns OAUTH_TOKEN_REJECTED + * If token has expired or is otherwise unusable returns OAUTH_TOKEN_REJECTED + * If request verifier doesn't match token's verifier returns OAUTH_VERIFIER_INVALID + * If lookup process failed for some reason returns OAUTH_ERR_INTERNAL_ERROR + */ + public function tokenHandler() + { + $db = \App::get('db'); + + if (!is_object($db)) + { + return OAUTH_ERR_INTERNAL_ERROR; + } + + $db->setQuery("SELECT * FROM `#__oauthp_tokens` WHERE token=" . $db->quote($this->_provider->token) . " LIMIT 1;"); + + $result = $db->loadObject(); + + if ($result === false) // query failed + { + return OAUTH_ERR_INTERNAL_ERROR; + } + + if (empty($result)) // token not found + { + return OAUTH_TOKEN_REJECTED; + } + + if ($result->state != '1') // token not in a valid state + { + return OAUTH_TOKEN_REJECTED; + } + + if ($result->user_id == '0') // check verifier on request tokens + { + if ($result->verifier != $this->_provider->verifier) + { + return OAUTH_VERIFIER_INVALID; + } + } + + $this->_token_data = $result; + $this->_provider->token_secret = $result->token_secret; + + return OAUTH_OK; + } +} diff --git a/core/libraries/Hubzero/Oauth/Server.php b/core/libraries/Hubzero/Oauth/Server.php new file mode 100644 index 00000000000..de8ce0280af --- /dev/null +++ b/core/libraries/Hubzero/Oauth/Server.php @@ -0,0 +1,106 @@ +config = array_merge(array( + 'enforce_state' => true, + 'access_lifetime' => 3600, + 'refresh_token_lifetime' => 7200, + 'require_exact_redirect_uri' => true, + 'allow_credentials_in_request_body' => true, + 'always_issue_new_refresh_token' => true + ), $options); + + // available grant types + $grantTypes = array( + new UserCredentialsGrantType($storage, $this->config), + new RefreshTokenGrantType($storage, $this->config), + new AuthorizationCodeGrantType($storage, $this->config), + new ClientCredentialsGrantType($storage, $this->config), + new SessionTokenGrantType($storage, $this->config), + new ToolSessionTokenGrantType($storage, $this->config) + ); + + // Pass a storage object or array of storage objects to the OAuth2 server class + $this->server = new OAuth2Server($storage, $this->config, $grantTypes); + } + + /** + * Call All methods on OAuth2 Server + * + * @param string $name Method Name + * @param mixed $args Method Args + * @return mixed Result of calling method of server + */ + public function __call($name, $args) + { + // call method on OAuth2 Server + $response = call_user_func_array(array($this->server, $name), $args); + + // If its an OAuth2\Response object that means it was used for token fetching or autorization + // and we can modify it if its an error, otherwise we want to leave the result alone. + // Also check to see if the response code is a error (4xx or 5xx) + if ($response instanceof \OAuth2\Response + && ($response->isClientError() || $response->isServerError())) + { + // rewrite parameters (response body) to a + // standard error format used throughout the api + $response->setParameters(array( + 'message' => $response->getStatusText(), + 'code' => $response->getStatusCode(), + 'errors' => array( + $response->getParameters() + ) + )); + } + + // return response + return $response; + } + + /** + * Accessor for the server's config + * + * @return array The server instances config + */ + public function getConfig() + { + return $this->config; + } +} diff --git a/core/libraries/Hubzero/Oauth/Storage/Mysql.php b/core/libraries/Hubzero/Oauth/Storage/Mysql.php new file mode 100644 index 00000000000..d159046cefb --- /dev/null +++ b/core/libraries/Hubzero/Oauth/Storage/Mysql.php @@ -0,0 +1,630 @@ +get('id')) + { + return false; + } + + // make sure its a published token + if (!$token->isPublished()) + { + return false; + } + + // get the application's client id + $application = \Components\Developer\Models\Application::oneOrFail($token->get('application_id')); + $token->set('client_id', $application->get('client_id')); + + // format expires to unix timestamp + $token->set('expires', with(new Date($token->get('expires')))->toUnix()); + + // return token + return $token->toArray(true); + } + + /** + * Store access token data + * + * @param string $access_token Access token + * @param string $client_id Client Id + * @param string $user_id User identifier + * @param string $expires Access token expiration date/time + * @param string $scope Access token granted scope + * @return void + */ + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // format date like HUBzero does + $expires = with(new Date($expires))->toSql(); + $created = with(new Date('now'))->toSql(); + + // get the id for th client + $client = $this->getClientDetails($client_id); + + // create access token + $model = new \Components\Developer\Models\Accesstoken(); + $model->set('application_id', $client['id']); + $model->set('access_token', $access_token); + $model->set('uidNumber', $user_id); + $model->set('expires', $expires); + $model->set('created', $created); + return $model->save(); + } + + /** + * Get client details by client id + * + * @param string $client_id Load client details via client id. + * @return void + */ + public function getClientDetails($clientId) + { + // create model + $application = \Components\Developer\Models\Application::oneByClientid($clientId); + + // load application by client id + if (!$application->get('id')) + { + return false; + } + + // make sure its published + if (!$application->isPublished()) + { + return false; + } + + // return as array + return $application->toArray(); + } + + /** + * Get client details by id + * + * @param int $id Client mysql row auto-incrementing id + * @return array + */ + public function getClientDetailsById($id) + { + $database = \App::get('db'); + + $sql = "SELECT * FROM `#__developer_applications` + WHERE `id`=" . $database->quote($id); + $database->setQuery($sql); + return $database->loadAssoc(); + } + + /** + * Get client scope + * + * @param string $client_id Client id + * @return null + */ + public function getClientScope($client_id) + { + return null; + } + + /** + * Check grant type against client id + * + * @param string $client_id Client id + * @param string $grant_type Grant type + * @return bool Result of test + */ + public function checkRestrictedGrantType($client_id, $grant_type) + { + // get client details + $client = $this->getClientDetails($client_id); + + // check to make sure grant type is acceptable for client + if (isset($client['grant_types'])) + { + $grant_types = explode(' ', $client['grant_types']); + return in_array($grant_type, (array) $grant_types); + } + + return true; + } + + /** + * Verify client credentials + * + * @param string $client_id Client id + * @param string $client_secret Client secret + * @return bool Result of test + */ + public function checkClientCredentials($client_id, $client_secret = null) + { + // load client + if (!$client = $this->getClientDetails($client_id)) + { + return false; + } + + // make sure stored secret matches incoming + if ($client['client_secret'] != $client_secret) + { + return false; + } + + //passed + return true; + } + + /** + * Is client public + * + * @param integer $client_id + * @return boolean + */ + public function isPublicClient($client_id) + { + // get client details + $client = $this->getClientDetails($client_id); + + // make sure its an available client (aka not deleted) + return $client && $client['state'] != 2 ? true : false; + } + + /** + * Get authorization code details by code + * + * @param string $code Authorization code + * @return array Code details + */ + public function getAuthorizationCode($code) + { + // auth model + $authorizationCode = \Components\Developer\Models\Authorizationcode::oneByCode($code); + + // fetch by code + if (!$authorizationCode->get('id')) + { + return false; + } + + // get the application's client id + $application = \Components\Developer\Models\Application::oneOrFail($authorizationCode->get('application_id')); + $authorizationCode->set('client_id', $application->get('client_id')); + + // format expires to unix timestamp for authorization code grant type + $authorizationCode->set('expires', with(new Date($authorizationCode->get('expires')))->toUnix()); + + // return code + return $authorizationCode->toArray(); + } + + /** + * Create new authorization code + * + * @param string $code Authorization code + * @param string $client_id Client id + * @param int $user_id User identifier + * @param string $redirect_uri Redirect URI after authorization + * @param string $expires Code expiration + * @param string $scope Code scope + * @return bool + */ + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null) + { + // format date like HUBzero does + $expires = with(new Date($expires))->toSql(); + + // get the id for th client + $client = $this->getClientDetails($client_id); + + // create authorization code + $model = new \Components\Developer\Models\Authorizationcode(); + $model->set('application_id', $client['id']); + $model->set('authorization_code', $code); + $model->set('uidNumber', $user_id); + $model->set('redirect_uri', $redirect_uri); + $model->set('expires', $expires); + return $model->save(); + } + + /** + * Remove invalid authorization code + * + * @param string $code Authorization code + * @return void + */ + public function expireAuthorizationCode($code) + { + // auth model + $authorizationCode = \Components\Developer\Models\Authorizationcode::oneByCode($code); + + // fetch by code + if (!$authorizationCode->get('id')) + { + return false; + } + + return $authorizationCode->destroy(); + } + + /** + * Check user credentials + * + * @param string $username User's username + * @param string $password User's password + * @return bool Result of username/password check + */ + public function checkUserCredentials($username, $password) + { + // allow authentication via email, just like in the hub + if (strpos($username, '@')) + { + $username = $this->getUsernameFromEmail($username); + } + + // use hubzero password library to compare stored password with sent password + $match = Password::passwordMatches($username, $password, true); + + // return if match was found + return (bool) $match; + } + + /** + * Get user information + * + * @param string $username User's username + * @return array User info + */ + public function getUserDetails($username) + { + // load username from email + if (strpos($username, '@')) + { + $username = $this->getUsernameFromEmail($username); + } + + // load profile object, make sure its valid + $profile = \Hubzero\User\User::oneByUsername($username); + + if (!$profile->get('id')) + { + return false; + } + + // return details as associative array + return ['user_id' => $profile->get('id'), 'scope' => null]; + } + + /** + * Get username from email address + * + * @param string $enteredUsername Email address + * @return string User's username + */ + private function getUsernameFromEmail($enteredUsername) + { + // get username from email + $result = \Hubzero\User\User::oneByEmail($enteredUsername); + + // no results or too many + if (!$result || !$result->get('id')) + { + return null; + } + + return $result->get('username'); + } + + /** + * Load refresh token details by token + * + * @param string $refresh_token Refresh token + * @return array Refresh token details + */ + public function getRefreshToken($refresh_token) + { + // create refresh token + $token = \Components\Developer\Models\Refreshtoken::oneByToken($refresh_token); + + // make sure we have a token + if (!$token->get('id')) + { + return false; + } + + // make sure its a published token + if (!$token->isPublished()) + { + return false; + } + + // get the application's client id + $application = \Components\Developer\Models\Application::oneOrFail($token->get('application_id')); + $token->set('client_id', $application->get('client_id')); + + // format expires to unix timestamp + $token->set('expires', with(new Date($token->get('expires')))->toUnix()); + + // return token + return $token->toArray(); + } + + /** + * Create a refresh token + * + * @param string $refresh_token Refresh Token + * @param string $client_id Client id + * @param int $user_id User identifier + * @param string $expires Expires timestamp + * @param string $scope Token scope + * @return void + */ + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + // format date like HUBzero does + $expires = with(new Date($expires))->toSql(); + $created = with(new Date('now'))->toSql(); + + // get the id for th client + $client = $this->getClientDetails($client_id); + + // create refresh token + $model = new \Components\Developer\Models\Refreshtoken(); + $model->set('application_id', $client['id']); + $model->set('refresh_token', $refresh_token); + $model->set('uidNumber', $user_id); + $model->set('expires', $expires); + $model->set('created', $created); + return $model->save(); + } + + /** + * Remove refresh token + * + * @param string $refresh_token Refresh Token + * @return void + */ + public function unsetRefreshToken($refresh_token) + { + // create refresh token + $token = \Components\Developer\Models\RefreshToken::oneByToken($refresh_token); + + // make sure we have a token + if (!$token->get('id')) + { + return false; + } + + // delete token + return $token->destroy(); + } + + /** + * Get session id from cookie + * + * [!] This will determine if the user has an active session via browser + * + * @return mixed Result of test + */ + public function getSessionIdFromCookie() + { + // get session id key name + $client = 'site'; + if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER']) + { + $referrer = $_SERVER['HTTP_REFERER']; + if (\Hubzero\Utility\Uri::isInternal($referrer)) + { + if (substr($referrer, 0, strlen('http')) == 'http') + { + $referrer = parse_url($referrer, PHP_URL_PATH); + } + $referrer = trim($referrer, '/'); + $parts = explode('/', $referrer); + $referrer = array_shift($parts); + if ($referrer == 'administrator') + { + $client = $referrer; + } + } + } + $sessionName = md5(\App::hash($client)); + + // return session id stored in cookie + return (!empty($_COOKIE[$sessionName])) ? $_COOKIE[$sessionName] : null; + } + + /** + * Get user id via session id + * + * @param string $sessionId Session identifier + * @return int User identifier + */ + public function getUserIdFromSessionId($sessionId) + { + $database = \App::get('db'); + + // get session timeout period + $timeout = \App::get('config')->get('timeout'); + + // load user from session table + $sql = "SELECT userid + FROM `#__session` + WHERE `session_id`=" . $database->quote($sessionId) . " + AND time + " . (int) $timeout . " <= NOW();"; + // AND client_id = 0;"; + $database->setQuery($sql); + return $database->loadResult(); + } + + /** + * Get tool data from request + * + * @return bool Result of test + */ + public function getToolSessionDataFromRequest(RequestInterface $request) + { + // get params via post vars + $toolSessionId = $request->request('sessionnum'); + $toolSessionToken = $request->request('sessiontoken'); + + // use headers as backup method to post vars + if (!$toolSessionId && !$toolSessionToken) + { + $toolSessionId = $request->headers('sessionnum'); + $toolSessionToken = $request->headers('sessiontoken'); + } + + // return id & token + return compact('toolSessionId', 'toolSessionToken'); + } + + /** + * Validate tool session data + * + * @param string $toolSessionId Tool session id + * @param string $toolSessionToken Tool session token + * @return bool Result of test + */ + public function validateToolSessionData($toolSessionId, $toolSessionToken) + { + // include neede libs + require_once PATH_CORE . DS . 'components' . DS . 'com_tools' . DS . 'helpers' . DS . 'utils.php'; + + // instantiate middleware database + $mwdb = \Components\Tools\Helpers\Utils::getMWDBO(); + + // attempt to load session from db + $query = "SELECT * + FROM `session` + WHERE `sessnum`= " . $mwdb->quote($toolSessionId) . " + AND `sesstoken`=" . $mwdb->quote($toolSessionToken); + $mwdb->setQuery($query); + + // only continue if a valid session was found + if (!$session = $mwdb->loadObject()) + { + return false; + } + + // return user id + $profile = \Hubzero\User\User::oneByUsername($session->username); + return $profile->get('id'); + } + + /** + * Get internal client + * + * @return mixed + */ + public function getInternalRequestClient() + { + // if we didnt find one lets make one + if (!$client = $this->findInternalClient()) + { + if ($this->createInternalRequestClient()) + { + $client = $this->findInternalClient(); + } + } + + // return client + return ($client) ? $client : null; + } + + /** + * Find Internal Client + * + * In separate function so we can call it multiple times. + * + * @return array Client detials + */ + private function findInternalClient() + { + // create model and fetch applications matching fitlers + $application = \Components\Developer\Models\Application::all() + ->whereEquals('hub_account', 1) + ->row(); + + // make sure we have at least one + // although it should always only be one + if (!$application->get('id')) + { + return false; + } + + // return first as an array + return $application->toArray(); + } + + /** + * Create internal client + * + * @return bool + */ + public function createInternalRequestClient() + { + // client id/secret + $clientId = md5(uniqid(\User::get('id'), true)); + $clientSecret = sha1($clientId); + + // application model + $application = new \Components\Developer\Models\Application(); + $application->set('name', 'Hub Account'); + $application->set('description', 'Hub account for internal requests. DO NOT DELETE.'); + $application->set('redirect_uri', 'https://' . $_SERVER['HTTP_HOST']); + $application->set('client_id', $clientId); + $application->set('client_secret', $clientSecret); + $application->set('grant_types', 'client_credentials session tool'); + $application->set('created', with(new Date('now'))->toSql()); + $application->set('created_by', \User::get('id')); + $application->set('state', 1); + $application->set('hub_account', 1); + $application->save(); + + return true; + } +} diff --git a/core/libraries/Hubzero/Oauth/Storage/SessionTokenInterface.php b/core/libraries/Hubzero/Oauth/Storage/SessionTokenInterface.php new file mode 100644 index 00000000000..a627347aa28 --- /dev/null +++ b/core/libraries/Hubzero/Oauth/Storage/SessionTokenInterface.php @@ -0,0 +1,45 @@ +text = $text; + $this->prefix = $prefix; + $this->base = $base; + $this->link = $link; + } +} diff --git a/core/libraries/Hubzero/Pagination/Paginator.php b/core/libraries/Hubzero/Pagination/Paginator.php new file mode 100644 index 00000000000..85db4f5e10b --- /dev/null +++ b/core/libraries/Hubzero/Pagination/Paginator.php @@ -0,0 +1,429 @@ +total = (int) $total; + $this->limitstart = (int) max($limitstart, 0); + $this->limit = (int) max($limit, 0); + $this->prefix = $prefix; + + if ($this->limit > $this->total) + { + $this->limitstart = 0; + } + + // Set the pagination iteration loop values. + $displayedPages = 10; + + if (!$this->limit) + { + $this->limit = $total; + $this->limitstart = 0; + + // If we are viewing all records set the view all flag to true. + $this->_viewall = true; + } + + // If limitstart is greater than total (i.e. we are asked to display records that don't exist) + // then set limitstart to display the last natural page of results + if ($this->limitstart > $this->total - $this->limit) + { + $this->limitstart = max(0, (int) (ceil($this->total / $this->limit) - 1) * $this->limit); + } + + // Set the total pages and current page values. + if ($this->limit > 0) + { + $this->set('pages.total', ceil($this->total / $this->limit)); + $this->set('pages.current', ceil(($this->limitstart + 1) / $this->limit)); + } + else + { + $this->set('pages.total', 1); + $this->set('pages.current', $this->limitstart + 1); + } + + // Completely rewritten to center active page - zooley (2012-08-10) + $this->set('pages.middle', ceil($displayedPages / 2)); + + $start_loop = $this->get('pages.current') - $this->get('pages.middle') + 1; + $start_loop = ($start_loop < 1 ? 1 : $start_loop); + $stop_loop = $this->get('pages.current') + $displayedPages - $this->get('pages.middle'); + + $i = $start_loop; + if ($stop_loop > $this->get('pages.total')) + { + $i = $i + ($this->get('pages.total') - $stop_loop); + $stop_loop = $this->get('pages.total'); + } + if ($i <= 0) + { + $stop_loop = $stop_loop + (1 - $i); + $i = 1; + } + + $this->set('pages.i', $i); + $this->set('pages.start', $start_loop); + $this->set('pages.stop', $stop_loop); + + $this->_limits = array(); + for ($i = 5; $i <= 30; $i += 5) + { + $this->_limits[] = $i; + } + $this->_limits[] = 50; + $this->_limits[] = 100; + $this->_limits[] = 500; + $this->_limits[] = 1000; + } + + /** + * Method to set an additional URL parameter to be added to all pagination class generated + * links. + * + * @param string $key The name of the URL parameter for which to set a value. + * @param mixed $value The value to set for the URL parameter. + * @return object Paginator + */ + public function setAdditionalUrlParam($key, $value) + { + // Get the old value to return and set the new one for the URL parameter. + $result = isset($this->_additionalUrlParams[$key]) ? $this->_additionalUrlParams[$key] : null; + + // If the passed parameter value is null unset the parameter, otherwise set it to the given value. + if ($value === null) + { + unset($this->_additionalUrlParams[$key]); + } + else + { + $this->_additionalUrlParams[$key] = $value; + } + + return $this; + } + + /** + * Method to get an additional URL parameter (if it exists) to be added to + * all pagination class generated links. + * + * @param string $key The name of the URL parameter for which to get the value. + * @return mixed The value if it exists or null if it does not. + */ + public function getAdditionalUrlParam($key) + { + $result = isset($this->_additionalUrlParams[$key]) ? $this->_additionalUrlParams[$key] : null; + + return $result; + } + + /** + * Return the rationalised offset for a row with a given index. + * + * @param integer $index The row index + * @return integer Rationalised offset for a row with a given index. + */ + public function getRowOffset($index) + { + return $index + 1 + $this->limitstart; + } + + /** + * Return the pagination data object, only creating it if it doesn't already exist. + * + * @return object Pagination data object. + */ + public function getData() + { + static $data; + + if (!is_object($data)) + { + $data = $this->_buildDataObject(); + } + + return $data; + } + + /** + * Set the list of limit options. + * + * @param array $limits A list of limit options + * @return object Paginator + */ + public function setLimits($limits) + { + if (is_array($limits)) + { + $this->_limits = $limits; + } + + return $this; + } + + /** + * Get the list of limit options. + * + * @return array + */ + public function getLimits() + { + return $this->_limits; + } + + /** + * Return the icon to move an item UP. + * + * @param integer $i The row index. + * @param boolean $condition True to show the icon. + * @param string $task The task to fire. + * @param string $alt The image alternative text string. + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * @return string Either the icon to move an item up or a space. + */ + public function orderUpIcon($i, $condition = true, $task = 'orderup', $alt = 'JLIB_HTML_MOVE_UP', $enabled = true, $checkbox = 'cb') + { + if (($i > 0 || ($i + $this->limitstart > 0)) && $condition) + { + return Grid::orderUp($i, $task, '', $alt, $enabled, $checkbox); + } + + return ' '; + } + + /** + * Return the icon to move an item DOWN. + * + * @param integer $i The row index. + * @param integer $n The number of items in the list. + * @param boolean $condition True to show the icon. + * @param string $task The task to fire. + * @param string $alt The image alternative text string. + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * @return string Either the icon to move an item down or a space. + */ + public function orderDownIcon($i, $n, $condition = true, $task = 'orderdown', $alt = 'JLIB_HTML_MOVE_DOWN', $enabled = true, $checkbox = 'cb') + { + if (($i < $n - 1 || $i + $this->limitstart < $this->total - 1) && $condition) + { + return Grid::orderDown($i, $task, '', $alt, $enabled, $checkbox); + } + + return ' '; + } + + /** + * Create and return the pagination data object. + * + * @return object Pagination data object. + */ + protected function _buildDataObject() + { + $this->setAdditionalUrlParam('limit', $this->limit); + + // Initialise variables. + $data = new \stdClass; + + // Build the additional URL parameters string. + $params = ''; + if (!empty($this->_additionalUrlParams)) + { + foreach ($this->_additionalUrlParams as $key => $value) + { + $params .= '&' . $key . '=' . $value; + } + } + + $data->all = new Item(\Lang::txt('JLIB_HTML_VIEW_ALL'), $this->prefix); + if (!$this->_viewall) + { + $data->all->base = '0'; + $data->all->link = \Route::url($params . '&' . $this->prefix . 'limitstart='); + } + + // Set the start and previous data objects. + $data->start = new Item(\Lang::txt('JLIB_HTML_START'), $this->prefix); + $data->previous = new Item(\Lang::txt('JPREV'), $this->prefix); + + if ($this->get('pages.current') > 1) + { + $page = ($this->get('pages.current') - 2) * $this->limit; + + // Set the empty for removal from route + //$page = $page == 0 ? '' : $page; + + $data->start->base = '0'; + $data->start->link = \Route::url($params . '&' . $this->prefix . 'limitstart=0'); + + $data->previous->base = $page; + $data->previous->link = \Route::url($params . '&' . $this->prefix . 'limitstart=' . $page); + } + + // Set the next and end data objects. + $data->next = new Item(\Lang::txt('JNEXT'), $this->prefix); + $data->end = new Item(\Lang::txt('JLIB_HTML_END'), $this->prefix); + + if ($this->get('pages.current') < $this->get('pages.total')) + { + $next = $this->get('pages.current') * $this->limit; + $data->next->base = $next; + $data->next->link = \Route::url($params . '&' . $this->prefix . 'limitstart=' . $next); + + $end = ($this->get('pages.total') - 1) * $this->limit; + $data->end->base = $end; + $data->end->link = \Route::url($params . '&' . $this->prefix . 'limitstart=' . $end); + } + + // Set the pages. + $data->pages = array(); + + for ($i = $this->get('pages.start'); $i <= $this->get('pages.stop'); $i++) + { + $offset = ($i - 1) * $this->limit; + + // Set the empty for removal from route + //$offset = $offset == 0 ? '' : $offset; + + $data->pages[$i] = new Item($i, $this->prefix); + if ($i != $this->get('pages.current')) // || $this->_viewall) + { + $data->pages[$i]->rel = (($i + 1) == $this->get('pages.current')) ? 'prev' : ''; + $data->pages[$i]->rel = (($i - 1) == $this->get('pages.current')) ? 'next' : $data->pages[$i]->rel; + $data->pages[$i]->base = $offset; + $data->pages[$i]->link = \Route::url($params . '&' . $this->prefix . 'limitstart=' . $offset); + } + } + + return $data; + } + + /** + * Return the pagination footer. + * + * @param object $view Optional View object to use + * @return string Pagination footer. + */ + public function render($view = null) + { + $this->set('pages.ellipsis', false); + + // Build the page navigation list. + $data = $this->getData(); + + $data->prefix = $this->prefix; + $data->i = $this->get('pages.i'); + $data->ellipsis = $this->get('pages.ellipsis'); + $data->total = $this->get('pages.total'); + $data->startloop = $this->get('pages.start'); + $data->stoploop = $this->get('pages.stop'); + $data->current = $this->get('pages.current'); + + if (is_array($view)) + { + $view = new View($view); + } + + if (!$view instanceof View) + { + $view = new View(); + } + $view->set('limit', $this->limit) + ->set('start', $this->limitstart) + ->set('total', $this->total) + ->set('pages', $data) + ->set('viewall', $this->_viewall) + ->set('limits', $this->_limits) + ->set('prefix', $this->prefix); + + return $view->loadTemplate(); + } + + /** + * Magic method to convert the object to a string gracefully. + * + * @return string The entire pagination footer + */ + public function __toString() + { + return $this->render(); + } +} diff --git a/core/libraries/Hubzero/Pagination/View.php b/core/libraries/Hubzero/Pagination/View.php new file mode 100644 index 00000000000..3abd175467c --- /dev/null +++ b/core/libraries/Hubzero/Pagination/View.php @@ -0,0 +1,84 @@ +_basePath = $config['base_path']; + + // Set the default template search path + if (!array_key_exists('template_path', $config)) + { + $config['template_path'] = $this->_basePath . DIRECTORY_SEPARATOR . 'Views'; + } + + $this->setPath('template', $config['template_path']); + } + + /** + * Sets an entire array of search paths for templates or resources. + * + * @param string $type The type of path to set, typically 'template'. + * @param mixed $path The new set of search paths. If null or false, resets to the current directory only. + * @return void + */ + protected function setPath($type, $path) + { + $type = strtolower($type); + + // Clear out the prior search dirs + $this->_path[$type] = array(); + + // Actually add the user-specified directories + $this->addPath($type, $path); + + // Always add the fallback directories as last resort + if ($type == 'template' && $this->_overridePath) + { + // Set the alternative template search dir + $path = $this->_overridePath . DIRECTORY_SEPARATOR . 'html' . DIRECTORY_SEPARATOR . $this->getName(); + + $this->addPath($type, $path); + } + } +} diff --git a/core/libraries/Hubzero/Pagination/Views/paginator.php b/core/libraries/Hubzero/Pagination/Views/paginator.php new file mode 100644 index 00000000000..2e678dfc278 --- /dev/null +++ b/core/libraries/Hubzero/Pagination/Views/paginator.php @@ -0,0 +1,140 @@ +limits) +{ + foreach ($this->limits as $val) + { + $limits[] = \Hubzero\Html\Builder\Select::option($val); + } +} + +if (!function_exists('paginator_item_active')) +{ + /** + * Method to create an active pagination link to the item + * + * @param Item $item The object with which to make an active link. + * @return string HTML link + */ + function paginator_item_active($item, $prefix) + { + if (App::isAdmin()) + { + return '' . $item->text . ''; + } + else + { + return 'rel ? 'rel="' . $item->rel . '" ' : '') . 'class="pagenav">' . $item->text . ''; + } + } +} +?> + \ No newline at end of file diff --git a/core/libraries/Hubzero/Password/Blacklist.php b/core/libraries/Hubzero/Password/Blacklist.php new file mode 100644 index 00000000000..3fff300e8a1 --- /dev/null +++ b/core/libraries/Hubzero/Password/Blacklist.php @@ -0,0 +1,465 @@ + 'notempty' + ); + + /** + * Load a record by word + * + * @param string $word + * @return object + */ + public static function oneByWord($word) + { + $word = trim($word); + $word = strtolower($word); + + return self::blank() + ->whereEquals('word', $word) + ->row(); + } + + /** + * Check if a word is in the blacklist + * + * @param string $word + * @return bool + */ + public static function wordInBlacklist($word) + { + $result = self::oneByWord($word); + + return ($result->get('id') > 0); + } + + /** + * Check if a username is in the blacklist + * + * @param string $word + * @return bool + */ + public static function usernameInBlacklist($word, $username) + { + $word = self::normalize($word); + $username = self::normalize($username); + + $words = array(); + $words[] = $username; + $words[] = strrev($username); + + foreach ($words as $w) + { + if ($w == $word) + { + return true; + } + } + + return false; + } + + /** + * Check if a name is in the blacklist + * + * @param string $word + * @return bool + */ + public static function nameInBlacklist($word, $givenName, $middleName, $surname) + { + $word = self::normalize($word); + $givenName = self::normalize($givenName); + $middleName = self::normalize($middleName); + $surname = self::normalize($surname); + + $words = array(); + $words[] = $givenName; + $words[] = strrev($givenName); + $words[] = $middleName; + $words[] = strrev($middleName); + $words[] = $surname; + $words[] = strrev($surname); + $words[] = $givenName . $middleName . $surname; + $words[] = strrev($givenName . $middleName . $surname); + $words[] = $givenName . $surname; + $words[] = strrev($givenName . $surname); + $words[] = $middleName . $surname; + $words[] = strrev($middleName . $surname); + $words[] = $surname . $givenName; + $words[] = strrev($surname . $givenName); + $words[] = $surname . $middleName . $givenName; + $words[] = strrev($surname . $middleName . $givenName); + + foreach ($words as $w) + { + if ($w == $word) + { + return true; + } + } + + return false; + } + + /** + * Check if a word is based on a blacklisted word + * + * @param string $word + * @return bool + */ + public static function basedOnBlacklist($word) + { + $words[] = (string)$word; + $words[] = strtolower($word); + $words[] = strtolower(strrev($word)); + + $len = strlen($word); + $word2 = ''; + + // @FIXME: badly inefficient + for ($i = 0; $i < $len; $i++) + { + if (preg_match('/[a-zA-Z]/', $word[$i])) + { + $word2 .= $word[$i]; + } + } + + $words[] = strtolower($word2); + $words[] = strtolower(strrev($word2)); + $words[] = self::toL33t($word); + $words[] = strrev(self::toL33t($word)); + $words[] = self::toSimpleL33t($word); + $words[] = strrev(self::toSimpleL33t($word)); + + $total = self::all() + ->whereIn('word', $words) + ->total(); + + return ($total > 1); // returns true if char belongs to class + } + + /** + * Turn a word into simple l33t type + * + * @param string $word + * @return string + */ + protected static function toSimpleL33t($word) + { + $subs = array( + '4' => 'A', + '@' => 'A', + '^' => 'A', + '8' => 'B', + '(' => 'C', + '{' => 'C', + '<' => 'C', + ')' => 'D', + '3' => 'E', + '6' => 'G', + '9' => 'G', + '&' => 'G', + '#' => 'H', + '1' => 'I', + '!' => 'I', + //'|' => 'I', + '|' => 'L', + '1' => 'L', + '~' => 'N', + '0' => 'O', + '*' => 'O', + '5' => 'S', + '$' => 'S', + '7' => 'T', + '+' => 'T', + '%' => 'Y', + '2' => 'Z', + ); + + $word2 = str_replace(array_keys($subs), array_values($subs), $word); + + return strtolower($word2); + } + + /** + * Turn a word into l33t type + * + * @param string $word + * @return string + */ + protected static function toL33t($word) + { + $subs = array( + '][\\//][' => 'M', + + //'\\/\//' => 'W', + + '//=\\' => 'A', + '[]-[]' => 'H', + ']]-[[' => 'H', + '[]V[]' => 'M', + '][\][' => 'N', + '[]\[]' => 'N', + '[]_[]' => 'U', + + ';_[]' => 'J', + ';_]]' => 'J', + '/\\/\\' => 'M', + '|\\/|' => 'M', + '[\\/]' => 'M', + '(\\/)' => 'M', + '[[]]' => 'O', + '\'][\'' => 'T', + '\\\\//' => 'V', + '\\/\\/' => 'W', + '|/\\|' => 'W', + '[/\\]' => 'W', + '(/\\)' => 'W', + '1/\\/' => 'W', + '\\/1/' => 'W', + '1/1/' => 'W', + '``//' => 'Y', + + '133' => 'LEE', + '/-\\' => 'A', + ']]3' => 'B', + ']])' => 'D', + ']]=' => 'F', + '(_>' => 'G', + '[[6' => 'G', + '|-|' => 'H', + '(-)' => 'H', + ')-(' => 'H', + '}-{' => 'H', + '{-}' => 'H', + '/-/' => 'H', + '\\-\\' => 'H', + '|~|' => 'H', + '][<' => 'K', + ']]<' => 'K', + '[]<' => 'K', + '[]_' => 'L', + '][_' => 'L', + '/V\\' => 'M', + '\\\\\\' => 'M', + '(T)' => 'M', + '.\\\\' => 'M', + '//.' => 'M', + 'JVL' => 'M', + '/\\/' => 'N', + '|\\|' => 'N', + '(\\)' => 'N', + '/|/' => 'N', + '[\\]' => 'N', + '{\\}' => 'N', + '[]D' => 'P', + '][D' => 'P', + '(,)' => 'Q', + '[]\\' => 'Q', + ']]2' => 'R', + '[]2' => 'R', + '][2' => 'R', + '\']\'' => 'T', + '~|~' => 'T', + '-|-' => 'T', + '\'|\'' => 'T', + '(_)' => 'U', + '|_|' => 'U', + '\\_\\' => 'U', + '/_/' => 'U', + '\\_/' => 'U', + ']_[' => 'U', + '///' => 'W', + '\\^/' => 'W', + '\\|/' => 'Y', + '`/_' => 'Z', + + '/\\' => 'A', + 'F|' => 'FI', + 'f|' => 'FI', + '|7' => 'IT', + '|5' => 'IS', + ']3' => 'B', + ']8' => 'B', + '|3' => 'B', + '|8' => 'B', + '13' => 'B', + '[[' => 'C', + '[}' => 'D', + '|)' => 'D', + '|}' => 'D', + '|>' => 'D', + '[>' => 'D', + 'o|' => 'D', + 'ii' => 'E', + '|=' => 'F', + '(=' => 'F', + 'ph' => 'F', + '}{' => 'H', + '][' => 'I', + //'[]' => 'I', + '_|' => 'J', + 'u|' => 'J', + '|<' => 'K', + '|{' => 'K', + '|_' => 'L', + '^^' => 'M', + '()' => 'O', + '[]' => 'O', + '<>' => 'O', + '|o' => 'P', + '|D' => 'P', + '|*' => 'P', + '|>' => 'P', + '0,' => 'Q', + 'O,' => 'Q', + 'O\\' => 'Q', + '|2' => 'R', + '|?' => 'R', + '|-' => 'R', + '7`' => 'T', + '\\/' => 'V', + 'VV' => 'W', + '><' => 'X', + //'}{' => 'X', + ')(' => 'X', + '}[' => 'X', + '\'/' => 'Y', + '`/' => 'Y', + '\\j' => 'Y', + '-/' => 'Y', + '7_' => 'Z', + 't1' => 'thi', + 'T1' => 'THI', + '4' => 'A', + '@' => 'A', + '^' => 'A', + '8' => 'B', + '(' => 'C', + '{' => 'C', + '<' => 'C', + ')' => 'D', + '3' => 'E', + '6' => 'G', + '9' => 'G', + '&' => 'G', + '#' => 'H', + '1' => 'I', + '!' => 'I', + //'|' => 'I', + '|' => 'L', + '1' => 'L', + '~' => 'N', + '0' => 'O', + '*' => 'O', + '5' => 'S', + '$' => 'S', + '7' => 'T', + '+' => 'T', + '%' => 'Y', + //'j' => 'Y', + '2' => 'Z', + 'z' => 'Z', + ); + + $wordsubs = array( + ' joo ' => ' you ', + ' teh ' => ' the ', + ' wat ' => ' what ', + ' sas ' => ' says ', + ' u ' => ' you ' + ); + + $word2 = str_replace( array_keys($subs), array_values($subs), $word); + $word2 = strtolower($word2); + $word2 = str_replace( array_keys($wordsubs), array_values($wordsubs), $word2); + + return $word2; + } + + /** + * Normalize a word + * + * @param string $word + * @return string + */ + protected static function normalize($word) + { + $nword = ''; + + $len = strlen($word); + + for ($i = 0; $i < $word; $i++) + { + $o = ord($word[$i]); + + if ($o < 97) + { + // convert to lowercase + $o += 32; + } + + if ($o > 122 || $o < 97) + { + // skip anything not a lowercase letter + continue; + } + + $nword .= chr($o); + } + + return $nword; + } +} diff --git a/core/libraries/Hubzero/Password/CharacterClass.php b/core/libraries/Hubzero/Password/CharacterClass.php new file mode 100644 index 00000000000..a05ec00bcf9 --- /dev/null +++ b/core/libraries/Hubzero/Password/CharacterClass.php @@ -0,0 +1,79 @@ + '1', 'name' => 'uppercase', 'regex' => '[A-Z]', 'flag' => '1'); + $classes[] = array('id' => '2', 'name' => 'numeric', 'regex' => '[0-9]', 'flag' => '1'); + $classes[] = array('id' => '3', 'name' => 'lowercase', 'regex' => '[a-z]', 'flag' => '1'); + $classes[] = array('id' => '4', 'name' => 'special', 'regex' => '[!"\'(),-.:;?[`{}#$%&*+<=>@^_|~\]\/\\\]', 'flag' => '1'); + $classes[] = array('id' => '5', 'name' => 'nonalpha', 'regex' => '[!"\'(),\-.:;?[`{}#$%&*+<=>@^_|~\]\/\\\0-9]', 'flag' => '0'); + $classes[] = array('id' => '6', 'name' => 'alpha', 'regex' => '[A-Za-z]', 'flag' => '0'); + + self::$classes = $classes; + } + + /** + * Match character class + * + * @param string $char + * @return object + */ + public static function match($char = '') + { + $result = array(); + + if (empty(self::$classes)) + { + self::init(); + } + + if (empty(self::$classes)) + { + return $result; + } + + if (empty($char)) + { + $char = chr(0); + } + + $char = $char{0}; + + foreach (self::$classes as $class) + { + if (preg_match("/" . $class['regex'] . "/", $char)) + { + $match = new \stdClass(); + $match->name = $class['name']; + $match->flag = $class['flag']; + $result[] = $match; + } + } + + return $result; + } +} diff --git a/core/libraries/Hubzero/Password/Rule.php b/core/libraries/Hubzero/Password/Rule.php new file mode 100644 index 00000000000..618e17e20ab --- /dev/null +++ b/core/libraries/Hubzero/Password/Rule.php @@ -0,0 +1,698 @@ + 'notempty', + 'rule' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + */ + public $initiate = array( + 'ordering' + ); + + /** + * Generates automatic ordering field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticOrdering($data) + { + if (!isset($data['ordering'])) + { + $last = self::all() + ->select('ordering') + ->order('ordering', 'desc') + ->row(); + + $data['ordering'] = (int)$last->get('ordering') + 1; + } + + return $data['ordering']; + } + + /** + * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause. + * Negative numbers move the row up in the sequence and positive numbers move it down. + * + * @param integer $delta The direction and magnitude to move the row in the ordering sequence. + * @param string $where WHERE clause to use for limiting the selection of rows to compact the ordering values. + * @return bool True on success. + */ + public function move($delta, $where = '') + { + // If the change is none, do nothing. + if (empty($delta)) + { + return true; + } + + // Select the primary key and ordering values from the table. + $query = self::all(); + + // If the movement delta is negative move the row up. + if ($delta < 0) + { + $query->where('ordering', '<', (int) $this->get('ordering')); + $query->order('ordering', 'desc'); + } + // If the movement delta is positive move the row down. + elseif ($delta > 0) + { + $query->where('ordering', '>', (int) $this->get('ordering')); + $query->order('ordering', 'asc'); + } + + // Add the custom WHERE clause if set. + if ($where) + { + $query->whereRaw($where); + } + + // Select the first row with the criteria. + $row = $query->ordered()->row(); + + // If a row is found, move the item. + if ($row->get('id')) + { + $prev = $this->get('ordering'); + + // Update the ordering field for this instance to the row's ordering value. + $this->set('ordering', (int) $row->get('ordering')); + + // Check for a database error. + if (!$this->save()) + { + return false; + } + + // Update the ordering field for the row to this instance's ordering value. + $row->set('ordering', (int) $prev); + + // Check for a database error. + if (!$row->save()) + { + return false; + } + } + else + { + // Update the ordering field for this instance. + $this->set('ordering', (int) $this->get('ordering')); + + // Check for a database error. + if (!$this->save()) + { + return false; + } + } + + return true; + } + + /** + * Insert default content + * + * @param integer $restore Whether or not to force restoration of default values (even if other values are present) + * @return void + */ + public static function defaultContent($restore=0) + { + $defaults = array( + array( + 'class' => 'alpha', + 'description' => 'Must contain at least 1 letter', + 'enabled' => '0', + 'failuremsg' => 'Must contain at least 1 letter', + 'grp' => 'hub', + 'ordering' => '1', + 'rule' => 'minClassCharacters', + 'value' => '1' + ), + array( + 'class' => 'nonalpha', + 'description' => 'Must contain at least 1 number or punctuation mark', + 'enabled' => '0', + 'failuremsg' => 'Must contain at least 1 number or punctuation mark', + 'grp' => 'hub', + 'ordering' => '2', + 'rule' => 'minClassCharacters', + 'value' => '1' + ), + array( + 'class' => '', + 'description' => 'Must be at least 8 characters long', + 'enabled' => '0', + 'failuremsg' => 'Must be at least 8 characters long', + 'grp' => 'hub', + 'ordering' => '3', + 'rule' => 'minPasswordLength', + 'value' => '8' + ), + array( + 'class' => '', + 'description' => 'Must be no longer than 16 characters', + 'enabled' => '0', + 'failuremsg' => 'Must be no longer than 16 characters', + 'grp' => 'hub', + 'ordering' => '4', + 'rule' => 'maxPasswordLength', + 'value' => '16' + ), + array( + 'class' => '', + 'description' => 'Must contain more than 4 unique characters', + 'enabled' => '0', + 'failuremsg' => 'Must contain more than 4 unique characters', + 'grp' => 'hub', + 'ordering' => '5', + 'rule' => 'minUniqueCharacters', + 'value' => '5' + ), + array( + 'class' => '', + 'description' => 'Must not contain easily guessed words', + 'enabled' => '0', + 'failuremsg' => 'Must not contain easily guessed words', + 'grp' => 'hub', + 'ordering' => '6', + 'rule' => 'notBlacklisted', + 'value' => '' + ), + array( + 'class' => '', + 'description' => 'Must not contain your name or parts of your name', + 'enabled' => '0', + 'failuremsg' => 'Must not contain your name or parts of your name', + 'grp' => 'hub', + 'ordering' => '7', + 'rule' => 'notNameBased', + 'value' => '' + ), + array( + 'class' => '', + 'description' => 'Must not contain your username', + 'enabled' => '0', + 'failuremsg' => 'Must not contain your username', + 'grp' => 'hub', + 'ordering' => '8', + 'rule' => 'notUsernameBased', + 'value' => '' + ), + array( + 'class' => '', + 'description' => 'Must be different than the previous password (re-use of the same password will not be allowed for one (1) year)', + 'enabled' => '0', + 'failuremsg' => 'Must be different than the previous password (re-use of the same password will not be allowed for one (1) year)', + 'grp' => 'hub', + 'ordering' => '9', + 'rule' => 'notReused', + 'value' => '365' + ), + array( + 'class' => '', + 'description' => 'Must be changed at least every 120 days', + 'enabled' => '0', + 'failuremsg' => 'Must be changed at least every 120 days', + 'grp' => 'hub', + 'ordering' => '10', + 'rule' => 'notStale', + 'value' => '120' + ) + ); + + + if ($restore) + { + // Delete current password rules for manual restore + $rows = self::all()->limit(1000)->rows(); + + foreach ($rows as $row) + { + if (!$row->destroy()) + { + return false; + } + } + } + + // Add default rules + foreach ($defaults as $rule) + { + $row = self::blank()->set($rule); + + if (!$row->save()) + { + return false; + } + } + + return true; + } + + /** + * Analyze a password + * + * @param string $password + * @return array + */ + public static function analyze($password) + { + $stats = array(); + + $len = strlen($password); + + $stats['count'][0] = $len; + $stats['uniqueCharacters'] = 0; + $stats['uniqueClasses'] = 0; + + $classes = array(); + $histogram = array(); + + for ($i = 0; $i < $len; $i++) + { + $c = $password[$i]; + + $cl = CharacterClass::match($c); + + foreach ($cl as $class) + { + if (empty($stats['count'][$class->name])) + { + $stats['count'][$class->name] = 1; + if ($class->flag) + { + $stats['uniqueClasses']++; + } + } + else + { + $stats['count'][$class->name]++; + } + } + + if (empty($histogram[$c])) + { + $histogram[$c] = 1; + $stats['uniqueCharacters']++; + } + else + { + $histogram[$c]++; + } + } + + return $stats; + } + + /** + * Validate a password + * + * @param string $password + * @param array $rules + * @param mixed $user + * @param string $name + * @param bool $isNew + * @return array + */ + public static function verify($password, $rules, $user, $name=null, $isNew=true) + { + if (empty($rules)) + { + return array(); + } + + $fail = array(); + + $stats = self::analyze($password); + + foreach ($rules as $rule) + { + if ($rule['rule'] == 'minCharacterClasses') + { + if ($stats['uniqueClasses'] < $rule['value']) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'maxCharacterClasses') + { + if ($stats['uniqueClasses'] > $rule['value']) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'minPasswordLength') + { + if ($stats['count'][0] < $rule['value']) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'maxPasswordLength') + { + if ($stats['count'][0] > $rule['value']) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'maxClassCharacters') + { + if (empty($rule['class'])) + { + continue; + } + + $class = $rule['class']; + + if (empty($stats['count'][$class])) + { + $stats['count'][$class] = 0; + } + + if ($stats['count'][$class] > $rule['value']) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'minClassCharacters') + { + if (empty($rule['class'])) + { + continue; + } + + $class = $rule['class']; + + if (empty($stats['count'][$class])) + { + $stats['count'][$class] = 0; + } + + if ($stats['count'][$class] < $rule['value']) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'minUniqueCharacters') + { + if ($stats['uniqueCharacters'] < $rule['value']) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'notBlacklisted') + { + if (Blacklist::basedOnBlackList($password)) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'notNameBased') + { + if ($name == null) + { + if (is_numeric($user)) + { + $xuser = User::oneOrNew($user); + } + else + { + $xuser = User::oneByUsername($user); + } + + if (!is_object($xuser)) + { + continue; + } + + $givenName = $xuser->get('givenName'); + $middleName = $xuser->get('middleName'); + $surname = $xuser->get('surname'); + + $name = $givenName; + + if (!empty($middleName)) + { + if (empty($name)) + { + $name = $middleName; + } + else + { + $name .= ' ' . $middleName; + } + } + + if (!empty($surname)) + { + if (empty($name)) + { + $name = $surname; + } + else + { + $name .= ' ' . $surname; + } + } + } + + if (self::isBasedOnName($password, $name)) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'notUsernameBased') + { + if (is_numeric($user)) + { + $xuser = User::oneOrNew($user); + + if (!is_object($xuser)) + { + continue; + } + + $user = $xuser->get('username'); + } + if (self::isBasedOnUsername($password, $user)) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'notReused') + { + $date = new \DateTime('now'); + $date->modify("-" . $rule['value'] . "day"); + + $phist = History::getInstance($user); + if (!is_object($phist)) + { + continue; + } + + if ($phist->exists($password, $date->format("Y-m-d H:i:s"))) + { + $fail[] = $rule['failuremsg']; + } + + $current = Password::getInstance($user); + + // [HUBZERO][#10274] Check the current password too + if ($isNew && Password::passwordMatches($user, $password, true)) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] == 'notRepeat') + { + if (Password::passwordMatches($user, $password, true)) + { + $fail[] = $rule['failuremsg']; + } + } + else if ($rule['rule'] === 'true') + { + } + else if ($rule['rule'] == 'notStale') + { + } + else + { + $fail[] = $rule['failuremsg']; + } + } + + if (empty($fail)) + { + $fail = array(); + } + + return $fail; + } + + /** + * Normalize a word + * + * @param string $word + * @return string + */ + protected static function normalize($word) + { + $nword = ''; + + $len = strlen($word); + + for ($i = 0; $i < $len; $i++) + { + $o = ord( $word[$i] ); + + if ($o < 97) + { + // convert to lowercase + $o += 32; + } + + if ($o > 122 || $o < 97) + { + // skip anything not a lowercase letter + continue; + } + + $nword .= chr($o); + } + + return $nword; + } + + /** + * Check if a word is based on a name + * + * @param string $word + * @param string $name + * @return bool + */ + public static function isBasedOnName($word, $name) + { + $word = self::normalize($word); + + if (empty($word)) + { + return false; + } + + $names = explode(" ", $name); + + $count = count($names); + $words = array(); + + $fullname = self::normalize($name); + + $words[] = $fullname; + $words[] = strrev($fullname); + + foreach ($names as $e) + { + $e = self::normalize($e); + + if (strlen($e) > 3) + { + $words[] = $e; + $words[] = strrev($e); + } + } + + if ($count > 1) + { + $e = self::normalize($names[0] . $names[$count-1]); + + $words[] = $e; + $words[] = strrev($e); + } + + foreach ($words as $w) + { + if (empty($w)) + { + continue; + } + + if (strpos($w, $word) !== false) + { + return true; + } + } + + return false; + } + + /** + * Check if a word is based on a username + * + * @param string $word + * @param string $username + * @return bool + */ + public static function isBasedOnUsername($word, $username) + { + return preg_match("/$username/i", $word); + } +} diff --git a/core/libraries/Hubzero/Pathway/Item.php b/core/libraries/Hubzero/Pathway/Item.php new file mode 100644 index 00000000000..5c7b696470e --- /dev/null +++ b/core/libraries/Hubzero/Pathway/Item.php @@ -0,0 +1,41 @@ +name = (string) $name; + $this->link = (string) $link; + } +} diff --git a/core/libraries/Hubzero/Pathway/Tests/ItemTest.php b/core/libraries/Hubzero/Pathway/Tests/ItemTest.php new file mode 100644 index 00000000000..5f4d4f20403 --- /dev/null +++ b/core/libraries/Hubzero/Pathway/Tests/ItemTest.php @@ -0,0 +1,33 @@ +assertEquals($item->name, $name); + $this->assertEquals($item->link, $link); + } +} diff --git a/core/libraries/Hubzero/Pathway/Tests/TrailTest.php b/core/libraries/Hubzero/Pathway/Tests/TrailTest.php new file mode 100644 index 00000000000..88360146f1d --- /dev/null +++ b/core/libraries/Hubzero/Pathway/Tests/TrailTest.php @@ -0,0 +1,283 @@ +set(0, $crumb1); + $pathway->set(1, $crumb2); + $pathway->offsetSet(2, $crumb3); + + $this->assertTrue($pathway->has(1)); + $this->assertFalse($pathway->has(3)); + + $item = $pathway->get(0); + + $this->assertEquals($crumb1->name, $item->name); + $this->assertEquals($crumb1->link, $item->link); + + $item = $pathway->get(1); + + $this->assertEquals($crumb2->name, $item->name); + $this->assertEquals($crumb2->link, $item->link); + + $item = $pathway->offsetGet(2); + + $this->assertEquals($crumb3->name, $item->name); + $this->assertEquals($crumb3->link, $item->link); + + $pathway->forget(1); + + $this->assertFalse($pathway->has(1)); + + $pathway->offsetUnset(2); + + $this->assertFalse($pathway->offsetExists(2)); + } + + /** + * Tests: + * 1. the append() method is chainable + * 2. append() adds to the items list + * 3. append() adds an Hubzero\Pathway\Item object to the items list + * 4. append() adds to the END of the items list + * + * @covers \Hubzero\Pathway\Trail::append + * @return void + **/ + public function testAppend() + { + $pathway = new Trail(); + + $this->assertInstanceOf('Hubzero\Pathway\Trail', $pathway->append('Crumb 1', 'index.php?option=com_lorem')); + + $this->assertCount(1, $pathway->items(), 'List of crumbs should have returned one Item'); + + $name = 'Crumb 2'; + $link = 'index.php?option=com_ipsum'; + + $pathway->append($name, $link); + + $items = $pathway->items(); + $item = array_pop($items); + + $this->assertInstanceOf('Hubzero\Pathway\Item', $item); + $this->assertEquals($item->name, $name); + $this->assertEquals($item->link, $link); + } + + /** + * Tests: + * 1. the prepend() method is chainable + * 2. prepend() adds to the items list + * 3. prepend() adds an Hubzero\Pathway\Item object to the items list + * 4. prepend() adds to the BEGINNING of the items list + * + * @covers \Hubzero\Pathway\Trail::prepend + * @return void + **/ + public function testPrepend() + { + $pathway = new Trail(); + + $this->assertInstanceOf('Hubzero\Pathway\Trail', $pathway->prepend('Crumb 1', 'index.php?option=com_lorem')); + + $this->assertCount(1, $pathway->items(), 'List of crumbs should have returned one Item'); + + $name = 'Crumb 2'; + $link = 'index.php?option=com_ipsum'; + + $pathway->prepend($name, $link); + + $items = $pathway->items(); + $item = array_shift($items); + + $this->assertInstanceOf('Hubzero\Pathway\Item', $item); + $this->assertEquals($item->name, $name); + $this->assertEquals($item->link, $link); + } + + /** + * Test the count() method returns the number of items added + * + * @covers \Hubzero\Pathway\Trail::count + * @return void + **/ + public function testCount() + { + $pathway = new Trail(); + $pathway->append('Crumb 1', 'index.php?option=com_lorem'); + $pathway->append('Crumb 2', 'index.php?option=com_ipsum'); + + $this->assertEquals(2, $pathway->count()); + $this->assertEquals(2, count($pathway)); + } + + /** + * Tests: + * 1. the names() method returns an array + * 2. the number of items in the array matches the number of items added + * 3. the array returned contains just the names of the items added + * + * @covers \Hubzero\Pathway\Trail::names + * @return void + **/ + public function testNames() + { + $data = [ + 'Crumb 1', + 'Crumb 2' + ]; + + $pathway = new Trail(); + $pathway->append('Crumb 1', 'index.php?option=com_lorem'); + $pathway->append('Crumb 2', 'index.php?option=com_ipsum'); + + $names = $pathway->names(); + + $this->assertTrue(is_array($names), 'names() should return an array'); + $this->assertCount(2, $names, 'names() returned incorrect number of entries'); + $this->assertEquals($names, $data); + } + + /** + * Tests: + * 1. the items() method returns an array + * 2. the number of items in the array matches the number of items added + * 3. the array returned contains a Hubzero\Pathway\Item object for each entry added + * + * @covers \Hubzero\Pathway\Trail::items + * @return void + **/ + public function testItems() + { + $data = [ + new Item('Crumb 1', 'index.php?option=com_lorem'), + new Item('Crumb 2', 'index.php?option=com_ipsum') + ]; + + $pathway = new Trail(); + $pathway->append('Crumb 1', 'index.php?option=com_lorem'); + $pathway->append('Crumb 2', 'index.php?option=com_ipsum'); + + $items = $pathway->items(); + + $this->assertTrue(is_array($items), 'items() should return an array'); + $this->assertCount(2, $items, 'items() should have returned two Items'); + $this->assertEquals($items, $data); + } + + /** + * Tests: + * 1. the names() method returns an array + * 2. the number of items in the array matches the number of items added + * 3. the array returned contains just the names of the items added + * + * @covers \Hubzero\Pathway\Trail::clear + * @return void + **/ + public function testClear() + { + $pathway = new Trail(); + $pathway->append('Crumb 1', 'index.php?option=com_lorem'); + $pathway->append('Crumb 2', 'index.php?option=com_ipsum'); + $pathway->clear(); + + $items = $pathway->items(); + + $this->assertTrue(empty($items), 'items() should return an empty array after calling clear()'); + } + + /** + * Tests array traversing methods + * + * @covers \Hubzero\Pathway\Trail::current + * @covers \Hubzero\Pathway\Trail::key + * @covers \Hubzero\Pathway\Trail::next + * @covers \Hubzero\Pathway\Trail::valid + * @covers \Hubzero\Pathway\Trail::rewind + * @return void + **/ + public function testIterator() + { + $items = array( + new Item('Crumb 1', 'index.php?option=com_lorem'), + new Item('Crumb 2', 'index.php?option=com_ipsum'), + new Item('Crumb 3', 'index.php?option=com_foo'), + new Item('Crumb 4', 'index.php?option=com_bar'), + new Item('Crumb 5', 'index.php?option=com_mollum') + ); + + $pathway = new Trail(); + foreach ($items as $item) + { + $pathway->append($item->name, $item->link); + } + + // both cycles must pass + for ($n = 0; $n < 2; ++$n) + { + $i = 0; + reset($items); + foreach ($pathway as $key => $val) + { + if ($i >= 5) + { + $this->fail('Iterator overflow!'); + } + $this->assertEquals(key($items), $key); + $this->assertEquals(current($items), $val); + next($items); + ++$i; + } + $this->assertEquals(5, $i); + } + + // both cycles must pass + $first = reset($items); + + $i = 0; + foreach ($pathway as $key => $val) + { + if ($i > 3) + { + break; + } + $i++; + } + $this->assertEquals($first, $pathway->rewind()); + } +} diff --git a/core/libraries/Hubzero/Pathway/Trail.php b/core/libraries/Hubzero/Pathway/Trail.php new file mode 100644 index 00000000000..e2bca4b74d1 --- /dev/null +++ b/core/libraries/Hubzero/Pathway/Trail.php @@ -0,0 +1,246 @@ +items[] = new Item($name, $link); + + return $this; + } + + /** + * Create and prepend an item to the pathway. + * + * @param string $name The name of the item. + * @param string $link The link to the item. + * @return object + */ + public function prepend($name, $link = '') + { + $b = new Item($name, $link); + array_unshift($this->items, $b); + + return $this; + } + + /** + * Create and return an array of the crumb names. + * + * @return array + */ + public function names() + { + $names = array(); + + foreach ($this->items as $item) + { + $names[] = $item->name; + } + + return array_values($names); + } + + /** + * Return the list of crumbs + * + * @return array + */ + public function items() + { + return $this->items; + } + + /** + * Set an item in the list + * + * @param integer $offset + * @param object $value + * @return void + */ + public function set($offset, $value) + { + return $this->offsetSet($offset, $value); + } + + /** + * Get an item from the list + * + * @param integer $offset + * @return mixed + */ + public function get($offset) + { + return $this->offsetGet($offset); + } + + /** + * Check if an item exists + * + * @param integer $offset + * @return boolean + */ + public function has($offset) + { + return $this->offsetExists($offset); + } + + /** + * Unset an item + * + * @param integer $offset + * @return void + */ + public function forget($offset) + { + return $this->offsetUnset($offset); + } + + /** + * Clear out the list of items + * + * @return object + */ + public function clear() + { + $this->items = array(); + + return $this; + } + + /** + * Rewind position + * + * @return array + */ + public function rewind() + { + return reset($this->items); + } + + /** + * Return current item + * + * @return object + */ + public function current() + { + return current($this->items); + } + + /** + * Return position key + * + * @return integer + */ + public function key() + { + return key($this->items); + } + + /** + * Return next item + * + * @return object + */ + public function next() + { + return next($this->items); + } + + /** + * Is current position valid? + * + * @return voolean + */ + public function valid() + { + return key($this->items) !== null; + } + + /** + * Check if an item exists + * + * @param integer $offset + * @return boolean + */ + public function offsetExists($offset) + { + return isset($this->items[$offset]); + } + + /** + * Set an item in the list + * + * @param integer $offset + * @param object $value + * @return void + */ + public function offsetSet($offset, $value) + { + $this->items[$offset] = $value; + } + + /** + * Get an item from the list + * + * @param integer $offset + * @return mixed + */ + public function offsetGet($offset) + { + return isset($this->items[$offset]) ? $this->items[$offset] : null; + } + + /** + * Unset an item + * + * @param integer $offset + * @return void + */ + public function offsetUnset($offset) + { + unset($this->items[$offset]); + } + + /** + * Return a count of the number of items + * + * @return integer + */ + public function count() + { + return count($this->items); + } +} diff --git a/core/libraries/Hubzero/Plugin/Loader.php b/core/libraries/Hubzero/Plugin/Loader.php new file mode 100644 index 00000000000..7ca7618de89 --- /dev/null +++ b/core/libraries/Hubzero/Plugin/Loader.php @@ -0,0 +1,334 @@ +byType($type); + + foreach ($plugins as $p) + { + if ($result = $this->init($p)) + { + $results[] = $result; + } + } + + return $results; + } + + /** + * Get the params for a specific plugin + * + * @param string $type The plugin type, relates to the sub-directory in the plugins directory. + * @param string $plugin The plugin name. + * @return object + */ + public function params($type, $plugin) + { + $result = $this->byType($type, $plugin); + + if (!$result || empty($result)) + { + $result = new stdClass; + $result->params = ''; + } + + if (is_string($result->params)) + { + $result->params = new Registry($result->params); + } + + return $result->params; + } + + /** + * Get the plugin data of a specific type if no specific plugin is specified + * otherwise only the specific plugin data is returned. + * + * @param string $type The plugin type, relates to the sub-directory in the plugins directory. + * @param string $plugin The plugin name. + * @return mixed An array of plugin data objects, or a plugin data object. + */ + public function byType($type, $plugin = null) + { + $result = array(); + + foreach ($this->all() as $p) + { + // Is this the right plugin? + if ($p->type == $type) + { + if ($plugin) + { + if ($p->name == $plugin) + { + $result = $p; + break; + } + } + else + { + $result[] = $p; + } + } + } + + return $result; + } + + /** + * Checks if a plugin is enabled. + * + * @param string $type The plugin type, relates to the sub-directory in the plugins directory. + * @param string $plugin The plugin name. + * @return boolean + */ + public function isEnabled($type, $plugin = null) + { + $result = $this->byType($type, $plugin); + + return (!empty($result)); + } + + /** + * Checks if a plugin is enabled. + * + * @param string $type The plugin type, relates to the sub-directory in the plugins directory. + * @param string $plugin The plugin name. + * @return string + */ + public function path($type, $plugin = null) + { + static $paths = array(); + + if (!isset($paths[$type . $plugin])) + { + $paths[$type . $plugin] = ''; + + $p = DS . 'plugins' . DS . $type . ($plugin ? DS . $plugin : ''); + + foreach (array(PATH_APP, PATH_CORE) as $base) + { + if (is_dir($base . $p)) + { + $paths[$type . $plugin] = $base . $p; + break; + } + } + } + + return $paths[$type . $plugin]; + } + + /** + * Loads all the plugin files for a particular type if no specific plugin is specified + * otherwise only the specific plugin is loaded. + * + * @param string $type The plugin type, relates to the sub-directory in the plugins directory. + * @param string $plugin The plugin name. + * @param boolean $autocreate Autocreate the plugin. + * @param object $dispatcher Optionally allows the plugin to use a different dispatcher. + * @return boolean True on success. + */ + public function import($type, $plugin = null, $autocreate = true, $dispatcher = null) + { + static $loaded = array(); + + // check for the default args, if so we can optimise cheaply + $defaults = false; + if (is_null($plugin) && $autocreate == true && is_null($dispatcher)) + { + $defaults = true; + } + + if (!isset($loaded[$type]) || !$defaults) + { + $results = null; + + // Makes sure we have an event dispatcher + if (!($dispatcher instanceof DispatcherInterface)) + { + $dispatcher = App::get('dispatcher'); + } + + // Get the specified plugin(s). + foreach ($this->all() as $plug) + { + if ($plug->type == $type && ($plugin === null || $plug->name == $plugin)) + { + if ($p = $this->init($plug, $autocreate)) //, $dispatcher)) + { + $dispatcher->addListener($p); + } + $results = true; + } + } + + // Bail out early if we're not using default args + if (!$defaults) + { + return $results; + } + $loaded[$type] = $results; + } + + return $loaded[$type]; + } + + /** + * Loads the plugin file. + * + * @param object $plugin The plugin data. + * @return boolean True on success. + */ + protected function init(&$plugin, $autocreate = true, $dispatcher = null) + { + $plugin->type = preg_replace('/[^A-Z0-9_\.-]/i', '', $plugin->type); + $plugin->name = preg_replace('/[^A-Z0-9_\.-]/i', '', $plugin->name); + + $classNameL = 'plg' . $plugin->type . $plugin->name; + $classNameN = 'Plugins\\' . ucfirst($plugin->type) . '\\' . ucfirst($plugin->name); + + // If the class exists, the file was already loaded + if (!class_exists($classNameL) && !class_exists($classNameN)) + { + $path = $this->path($plugin->type, $plugin->name) . DS . $plugin->name . '.php'; + + if (file_exists($path)) + { + require_once $path; + + if ($autocreate) + { + foreach (array($classNameL, $classNameN) as $className) + { + if (!class_exists($className)) + { + continue; + } + + // Makes sure we have an event dispatcher + if (!($dispatcher instanceof DispatcherInterface)) + { + $dispatcher = new Dispatcher(); + } + + // Instantiate and register the plugin. + return new $className($dispatcher, (array) $plugin); + } + } + } + } + + return null; + } + + /** + * Loads the published plugins. + * + * @return array An array of published plugins + */ + public function all() + { + if (self::$plugins !== null) + { + return self::$plugins; + } + + if (!App::has('cache.store') || !($cache = App::get('cache.store'))) + { + $cache = new \Hubzero\Cache\Storage\None(); + } + + $levels = implode(',', User::getAuthorisedViewLevels()); + + if (!(self::$plugins = $cache->get('com_plugins.' . $levels))) + { + $db = App::get('db'); + + $query = $db->getQuery() + ->select('folder', 'type') + ->select('element', 'name') + ->select('protected') + ->select('params') + ->from('#__extensions') + ->where('enabled', '>=', 1) + ->whereEquals('type', 'plugin') + ->where('state', '>=', 0) + ->whereIn('access', User::getAuthorisedViewLevels()) + ->order('ordering', 'asc'); + + self::$plugins = $db->setQuery($query->toString())->loadObjectList(); + + if ($error = $db->getErrorMsg()) + { + throw new Exception($error, 500); + } + + $cache->put('com_plugins.' . $levels, self::$plugins, App::get('config')->get('cachetime', 15)); + } + + return self::$plugins; + } + + /** + * Loads the language file for a plugin + * + * @param string $extension Plugin name + * @param string $basePath Path to load from + * @return boolean + */ + public function language($extension, $basePath = PATH_CORE) + { + return App::get('language')->load(strtolower($extension), $basePath); + } +} diff --git a/core/libraries/Hubzero/Plugin/Loader/Legacy.php b/core/libraries/Hubzero/Plugin/Loader/Legacy.php new file mode 100644 index 00000000000..543941d8bc3 --- /dev/null +++ b/core/libraries/Hubzero/Plugin/Loader/Legacy.php @@ -0,0 +1,37 @@ +isEnabled('com_users')) + { + // If someone is logged in already, then we're linking an account + $task = (\User::isGuest()) ? 'user.login' : 'user.link'; + $option = 'users'; + } + else + { + $task = (\User::isGuest()) ? 'login' : 'link'; + } + } + + $scope = '/index.php?option=com_' . $option . '&task=' . $task . '&authenticator=' . $name; + + return $service . $scope; + } +} diff --git a/core/libraries/Hubzero/Plugin/Params.php b/core/libraries/Hubzero/Plugin/Params.php new file mode 100644 index 00000000000..dc2a649e31f --- /dev/null +++ b/core/libraries/Hubzero/Plugin/Params.php @@ -0,0 +1,125 @@ + 'positive|nonzero', + 'folder' => 'notempty', + 'element' => 'notempty' + ); + + /** + * Load a record and binf to $this + * + * @param integer $oid Object ID (eg, group ID) + * @param string $folder Plugin folder + * @param string $element Plugin name + * @return boolean True on success + */ + public static function oneByPlugin($oid=null, $folder=null, $element=null) + { + return self::all() + ->whereEquals('object_id', (int) $oid) + ->whereEquals('folder', (int) $folder) + ->whereEquals('element', (int) $element) + ->row(); + } + + /** + * Get the custom parameters for a plugin + * + * @param integer $oid Object ID (eg, group ID) + * @param string $folder Plugin folder + * @param string $element Plugin name + * @return object + */ + public static function getCustomParams($oid=null, $folder=null, $element=null) + { + $result = self::oneByPlugin($oid, $folder, $element); + + return new Registry($result->get('params')); + } + + /** + * Get the default parameters for a plugin + * + * @param string $folder Plugin folder + * @param string $element Plugin name + * @return object + */ + public static function getDefaultParams($folder=null, $element=null) + { + $plugin = \Plugin::byType($folder, $element); + + return new Registry($plugin->params); + } + + /** + * Get the parameters for a plugin + * Merges default params and custom params (take precedence) + * + * @param integer $oid Object ID (eg, group ID) + * @param string $folder Plugin folder + * @param string $element Plugin name + * @return object + */ + public static function getParams($oid=null, $folder=null, $element=null) + { + $custom = self::getCustomParams($oid, $folder, $element); + + $params = self::getDefaultParams($folder, $element); + $params->merge($custom); + + return $params; + } +} diff --git a/core/libraries/Hubzero/Plugin/Plugin.php b/core/libraries/Hubzero/Plugin/Plugin.php new file mode 100644 index 00000000000..28157eadba9 --- /dev/null +++ b/core/libraries/Hubzero/Plugin/Plugin.php @@ -0,0 +1,160 @@ +_subject = &$subject; + + // Get the parameters. + if (isset($config['params'])) + { + $this->params = $config['params']; + } + + if (!($this->params instanceof Registry)) + { + $this->params = new Registry($this->params); + } + + // Get the plugin name. + if (isset($config['name'])) + { + $this->_name = $config['name']; + } + + // Get the plugin type. + if (isset($config['type'])) + { + $this->_type = $config['type']; + } + + // @TODO: Remove this + $this->option = (isset($config['type']) ? 'com_' . $config['type'] : 'com_' . $this->_type); + + // Load the language files if needed. + if ($this->_autoloadLanguage) + { + $this->loadLanguage('', PATH_APP . DS . 'bootstrap' . DS . \App::get('client')->name); + } + } + + /** + * Loads the plugin language file + * + * @param string $extension The extension for which a language file should be loaded + * @param string $basePath The basepath to use + * @return boolean True, if the file has successfully loaded. + */ + public function loadLanguage($extension = '', $basePath = PATH_APP) + { + if (empty($extension)) + { + $extension = 'plg_' . $this->_type . '_' . $this->_name; + } + + $lang = \App::get('language'); + return $lang->load(strtolower($extension), $basePath, null, false, true) + || $lang->load(strtolower($extension), PATH_APP . DS . 'plugins' . DS . $this->_type . DS . $this->_name, null, false, true) + || $lang->load(strtolower($extension), PATH_CORE . DS . 'plugins' . DS . $this->_type . DS . $this->_name, null, false, true); + } + + /** + * Method to get plugin params + * + * @param string $name Plugin name + * @param string $folder Plugin folder + * @return object + */ + public static function getParams($name, $folder) + { + $database = \App::get('db'); + + // load the params from databse + $sql = "SELECT params FROM `#__extensions` WHERE folder=" . $database->quote($folder) . " AND element=" . $database->quote($name) . " AND enabled=1"; + $database->setQuery($sql); + $result = $database->loadResult(); + + // params object + $params = new Registry($result); + return $params; + } + + /** + * Create a plugin view and return it + * + * @param string $layout View layout + * @param string $name View name + * @return object + */ + public function view($layout='default', $name='') + { + $view = new View(array( + 'folder' => $this->_type, + 'element' => $this->_name, + 'name' => ($name ?: $this->_name), + 'layout' => ($layout ?: 'default') + )); + return $view; + } +} diff --git a/core/libraries/Hubzero/Plugin/View.php b/core/libraries/Hubzero/Plugin/View.php new file mode 100644 index 00000000000..d8569fa791b --- /dev/null +++ b/core/libraries/Hubzero/Plugin/View.php @@ -0,0 +1,304 @@ +path; + } + } + $this->_overridePath = $config['override_path']; + + // Set the view name + if (!array_key_exists('folder', $config)) + { + $config['folder'] = $this->getFolder(); + } + $this->_folder = $config['folder']; + + // Set the view name + if (!array_key_exists('element', $config)) + { + $config['element'] = $this->getElement(); + } + $this->_element = $config['element']; + + // Set the view name + if (!array_key_exists('name', $config)) + { + $config['name'] = $this->getName(); + } + $this->_name = $config['name']; + + // Set the charset (used by the variable escaping functions) + if (array_key_exists('charset', $config)) + { + $this->_charset = $config['charset']; + } + + // User-defined escaping callback + if (array_key_exists('escape', $config)) + { + $this->setEscape($config['escape']); + } + + // Set a base path for use by the view + if (!array_key_exists('base_path', $config)) + { + if (defined('PATH_APP')) + { + $config['base_path'] = PATH_APP . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $this->_folder . DIRECTORY_SEPARATOR . $this->_element; + + if (!file_exists($config['base_path']) && defined('PATH_CORE')) + { + $config['base_path'] = PATH_CORE . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $this->_folder . DIRECTORY_SEPARATOR . $this->_element; + } + } + } + $this->_basePath = $config['base_path']; + + // Set the default template search path + if (!array_key_exists('template_path', $config)) + { + $config['template_path'] = $this->_basePath . '/views/' . $this->getName() . '/tmpl'; + } + $this->setPath('template', $config['template_path']); + + // Set the default helper search path + if (!array_key_exists('helper_path', $config)) + { + $config['helper_path'] = $this->_basePath . '/helpers'; + } + $this->setPath('helper', $config['helper_path']); + + // Set the layout + if (!array_key_exists('layout', $config)) + { + $config['layout'] = 'default'; + } + $this->setLayout($config['layout']); + + // Set the site's base URL + $this->baseurl = \Request::base(true); + } + + /** + * Method to get the plugin folder + * + * The model name by default parsed using the classname, or it can be set + * by passing a $config['folder'] in the class constructor + * + * @return string The name of the model + */ + public function getFolder() + { + $folder = $this->_folder; + + if (empty($folder)) + { + $r = new ReflectionClass($this); + if ($r->inNamespace()) + { + $bits = explode('\\', __NAMESPACE__); + + // Should match either: + // Plugins\Folder\Element + // Components\Folder\Plugins\Element + $folder = strtolower($bits[1]); + } + else + { + throw new Exception('Cannot get or parse view class name.', 500); + } + } + + return $folder; + } + + /** + * Method to get the plugin folder + * + * The model name by default parsed using the classname, or it can be set + * by passing a $config['folder'] in the class constructor + * + * @return string The name of the model + */ + public function getElement() + { + $element = $this->_element; + + if (empty($element)) + { + $r = new ReflectionClass($this); + if ($r->inNamespace()) + { + $bits = explode('\\', __NAMESPACE__); + + // Should match either: + // Plugins\Folder\Element + // Components\Folder\Plugins\Element + $element = strtolower($bits[2]); + + if (strtolower($bits[0]) == 'components') + { + $element = strtolower($bits[3]); + } + } + else + { + throw new Exception('Cannot get or parse view class name.', 500); + } + } + + return $element; + } + + /** + * Sets an entire array of search paths for templates or resources. + * + * @param string $type The type of path to set, typically 'template'. + * @param string|array $path The new set of search paths. If null or false, resets to the current directory only. + * @return void + */ + protected function setPath($type, $path) + { + $type = strtolower($type); + + // Clear out the prior search dirs + $this->_path[$type] = array(); + + // Actually add the user-specified directories + $this->addPath($type, $path); + + // Always add the fallback directories as last resort + if ($type == 'template' && $this->_overridePath) + { + // Set the alternative template search dir + $option = 'plg_' . $this->_folder . '_' . $this->_element; + $option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option); + + $path = $this->_overridePath . DIRECTORY_SEPARATOR . 'html' . DIRECTORY_SEPARATOR . $option . DIRECTORY_SEPARATOR . $this->getName(); + + $this->addPath($type, $path); + } + } + + /** + * Create a plugin view and return it + * + * @param string $layout View layout + * @param string $name View name + * @return object + */ + public function view($layout, $name=null) + { + // If we were passed only a view model, just render it. + if ($layout instanceof AbstractView) + { + return $layout; + } + + $view = new self(array( + 'folder' => $this->_folder, + 'element' => $this->_element, + 'name' => ($name ? $name : $this->_name), + 'layout' => $layout + )); + $view->set('folder', $this->_folder) + ->set('element', $this->_element); + + return $view; + } + + /** + * Dynamically handle calls to the class. + * + * @param string $method + * @param array $parameters + * @return mixed + * @throws \BadMethodCallException + * @since 1.3.1 + */ + public function __call($method, $parameters) + { + if (!static::hasHelper($method)) + { + foreach ($this->_path['helper'] as $path) + { + $file = $path . DIRECTORY_SEPARATOR . $method . '.php'; + if (file_exists($file)) + { + include_once $file; + break; + } + } + + // Namespaced + $invokable1 = '\\Plugins\\' . ucfirst($this->_folder) . '\\' . ucfirst($this->_element) . '\\Helpers\\' . ucfirst($method); + + // Old naming scheme "PluginFolderElementHelperMethod" + $invokable2 = 'Plugin' . ucfirst($this->_folder) . ucfirst($this->_element) . 'Helper' . ucfirst($method); + + $callback = null; + if (class_exists($invokable1)) + { + $callback = new $invokable1(); + } + else if (class_exists($invokable2)) + { + $callback = new $invokable2(); + } + + if (is_callable($callback)) + { + $callback->setView($this); + + $this->helper($method, $callback); + } + } + + return parent::__call($method, $parameters); + } +} diff --git a/core/libraries/Hubzero/Redis/Database.php b/core/libraries/Hubzero/Redis/Database.php new file mode 100644 index 00000000000..1b904ee1c20 --- /dev/null +++ b/core/libraries/Hubzero/Redis/Database.php @@ -0,0 +1,127 @@ +servers = (array) \Config::get('redis_server', array()); + $this->options = (array) \Config::get('redis_server_options', array()); + + if (isset($this->options['cluster']) && $this->options['cluster']) + { + $this->clients = $this->createAggregateClient(); + } + else + { + $this->clients = $this->createSingleClients(); + } + } + + /** + * Create a new aggregate client supporting sharding. + * + * @return array + */ + protected function createAggregateClient() + { + $servers = array_values($this->servers); + foreach ($servers as $k => $server) + { + $servers[$k] = (array) $server; + } + + return array('default' => new Client($servers, $this->options)); + } + + /** + * Create an array of single connection clients. + * + * @return array + */ + protected function createSingleClients() + { + $clients = array(); + + foreach ($this->servers as $key => $server) + { + $clients[$key] = new Client((array) $server, $this->options); + } + + return $clients; + } + + /** + * Get a specific Redis connection instance. + * + * @param string $name + * @return object + */ + public static function connect($name = 'default') + { + // create new instance of this class + $self = new self; + return $self->clients[$name ?: 'default']; + } + + /** + * Run a command against the Redis database. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function command($method, array $parameters = array()) + { + return call_user_func_array(array($this->clients['default'], $method), $parameters); + } + + /** + * Dynamically make a Redis command. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->command($method, $parameters); + } +} diff --git a/core/libraries/Hubzero/Routing/Manager.php b/core/libraries/Hubzero/Routing/Manager.php new file mode 100644 index 00000000000..12448512020 --- /dev/null +++ b/core/libraries/Hubzero/Routing/Manager.php @@ -0,0 +1,151 @@ +app = $app; + $this->paths = (array)$paths; + } + + /** + * Get the default client name. + * + * @return string + */ + public function getDefaultClient() + { + return $this->app['client']->name; + } + + /** + * Get a client instance. + * + * @param string $client + * @return object + */ + public function client($client = null) + { + $client = $client ?: $this->getDefaultClient(); + + // If the given driver has not been created before, we will create the instances + // here and cache it so we can return it next time very quickly. If there is + // already a driver created by this name, we'll just return that instance. + if (!isset($this->routers[$client])) + { + $this->routers[$client] = $this->createRouter($client); + } + + return $this->routers[$client]; + } + + /** + * Create a new client instance. + * + * @param string $client + * @return object + */ + protected function createRouter($client) + { + $prefix = $this->app['request']->getHttpHost(); + + $router = new Router(array(), $prefix); + + $routes = array(); + + foreach ($this->paths as $path) + { + $routes[] = $path . DIRECTORY_SEPARATOR . 'bootstrap' . DIRECTORY_SEPARATOR . $client; + $routes[] = $path . DIRECTORY_SEPARATOR . 'bootstrap' . DIRECTORY_SEPARATOR . ucfirst($client); + } + + foreach ($routes as $route) + { + $path = $route . DIRECTORY_SEPARATOR . 'routes.php'; + + if (file_exists($path)) + { + require $path; + } + } + + return $router; + } + + /** + * Get all of the created "routers". + * + * @return array + */ + public function getRouters() + { + return $this->routers; + } + + /** + * Dynamically call the router instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return call_user_func_array(array($this->client(), $method), $parameters); + } + + /** + * Get the router for a specific client + * + * @param string $client The name of the application. + * @param string $url Absolute or Relative URI to resource. + * @param boolean $xhtml Replace & by & for XML compilance. + * @param integer $ssl Secure state for the resolved URI. + * 1: Make URI secure using global secure site URI. + * 0: Leave URI in the same secure state as it was passed to the function. + * -1: Make URI unsecure using the global unsecure site URI. + * @return The translated humanly readible URL. + */ + public function urlForClient($client, $url, $xhtml = true, $ssl = null) + { + return $this->client($client)->url($url, $xhtml, $ssl); + } +} diff --git a/core/libraries/Hubzero/Routing/Router.php b/core/libraries/Hubzero/Routing/Router.php new file mode 100644 index 00000000000..92c2cca87f4 --- /dev/null +++ b/core/libraries/Hubzero/Routing/Router.php @@ -0,0 +1,324 @@ + null, + 'parse' => null + ); + + /** + * Class constructor + * + * @param array $options Array of options + * @return void + */ + public function __construct($vars = array(), $prefix = '') + { + $this->flush()->bind($vars); + + $this->prefix = $prefix; + } + + /** + * Translates an internal URL to a humanly readable URL. + * + * @param string $url Absolute or Relative URI to resource. + * @param boolean $xhtml Replace & by & for XML compliance. + * @param integer $ssl Secure state for the resolved URI. + * 1: Make URI secure using global secure site URI. + * 0: Leave URI in the same secure state as it was passed to the function. + * -1: Make URI unsecure using the global unsecure site URI. + * @return string The translated humanly readable URL. + */ + public function url($url, $xhtml = true, $ssl = null) + { + if ((strpos($url, '&') !== 0) + && (strpos($url, 'index.php') !== 0)) + { + return $url; + } + + // Build route. + $uri = $this->build($url); + $url = $uri->toString(array('scheme', 'host', 'port', 'path', 'query', 'fragment')); + + // Replace spaces. + $url = preg_replace('/\s/u', '%20', $url); + + // Get the secure/unsecure URLs. + // + // If the first 5 characters of the BASE are 'https', then we are on an ssl connection over + // https and need to set our secure URL to the current request URL, if not, and the scheme is + // 'http', then we need to do a quick string manipulation to switch schemes. + if ((int) $ssl) + { + /*static $prefix; + + if (!$prefix) + { + $prefix = \App::get('request'); + }*/ + + // Determine which scheme we want. + $scheme = ((int) $ssl === 1) ? 'https' : 'http'; + + // Make sure our URL path begins with a slash. + if (!preg_match('#^/#', $url)) + { + $url = '/' . $url; + } + + // Build the URL. + $url = $scheme . '://' . $this->prefix . $url; + } + + if ($xhtml) + { + $url = htmlspecialchars($url); + } + + return $url; + } + + /** + * Get a set of rules + * + * @param string $type + * @return object + */ + public function rules($type) + { + $type = strtolower(trim($type)); + + if (!isset($this->rules[$type])) + { + throw new InvalidArgumentException(sprintf('Rule type of %s not supported', $type)); + } + + return $this->rules[$type]; + } + + /** + * Function to convert an internal URI to a route + * + * @param string $uri The internal URL + * @return string The absolute search engine friendly URL + */ + public function build($uri) + { + // Create the URI object + $uri = $this->createUri($uri); + + // Process the uri information based on custom defined rules + foreach ($this->rules['build'] as $rule) + { + $uri = $rule($uri); + } + + return $uri; + } + + /** + * Function to convert a route to an internal URI + * + * @param string $uri The request URL + * @return array + */ + public function parse($uri) + { + // Create the URI object + $uri = $this->createUri($uri); + + $this->bind($uri->getQuery(true)); + + // Process the parsed variables based on custom defined rules + foreach ($this->rules['parse'] as $rule) + { + if ($rule($uri)) + { + break; + } + } + + $vars = array_merge($uri->getQuery(true), $this->vars()); + + $this->bind($vars); + + return $this->vars(); + } + + /** + * Clear any set rules + * + * @return object + */ + public function flush() + { + $this->rules = array( + 'build' => new Rules(), + 'parse' => new Rules() + ); + $this->vars = array(); + + return $this; + } + + /** + * Set a router variable, creating it if it doesn't exist + * + * @param string $key The name of the variable + * @param mixed $value The value of the variable + * @param boolean $create If True, the variable will be created if it doesn't exist yet + * @return object + */ + public function set($key, $value, $create = true) + { + if ($create || array_key_exists($key, $this->vars)) + { + $this->vars[$key] = $value; + } + + return $this; + } + + /** + * Set the router variable array + * + * @param array $vars An associative array with variables + * @param boolean $merge If True, the array will be merged instead of overwritten + * @return object + */ + public function bind($vars = array(), $merge = true) + { + if ($merge) + { + $this->vars = array_merge($this->vars, $vars); + } + else + { + $this->vars = $vars; + } + + return $this; + } + + /** + * Get a router variable + * + * @param string $key The name of the variable + * @return mixed Value of the variable + */ + public function get($key) + { + $result = null; + + if (isset($this->vars[$key])) + { + $result = $this->vars[$key]; + } + + return $result; + } + + /** + * Unset a router variable + * + * @param string $key The name of the variable + * @return object + */ + public function forget($key) + { + if (array_key_exists($key, $this->vars)) + { + unset($this->vars[$key]); + } + + return $this; + } + + /** + * Get the router variable array + * + * @return array An associative array of router variables + */ + public function vars() + { + return $this->vars; + } + + /** + * Create a uri based on a full or partial url string + * + * @param string $url The URI + * @return object + */ + protected function createUri($url) + { + if ($url instanceof Uri) + { + return $url; + } + + // Create full URL if we are only appending variables to it + if (substr($url, 0, 1) == '&') + { + $vars = array(); + + if (strpos($url, '&') !== false) + { + $url = str_replace('&', '&', $url); + } + + parse_str($url, $vars); + + $vars = array_merge($this->vars(), $vars); + + foreach ($vars as $key => $var) + { + if ($var == '') + { + unset($vars[$key]); + } + } + + $url = 'index.php?' . urldecode(http_build_query($vars, '', '&')); + } + + return new Uri($url); + } +} diff --git a/core/libraries/Hubzero/Routing/Router/Rules.php b/core/libraries/Hubzero/Routing/Router/Rules.php new file mode 100644 index 00000000000..3b97e4058ad --- /dev/null +++ b/core/libraries/Hubzero/Routing/Router/Rules.php @@ -0,0 +1,318 @@ + $value) + { + $this->data[$key] = $this->close($value); + } + } + + /** + * Wrap value in a Closure + * + * @param mixed $value + * @return object + */ + public function close($value) + { + if (!($value instanceof Closure)) + { + $value = function($uri) use ($value) + { + if (is_callable($value)) + { + return call_user_func_array($value, array($uri)); + } + return $value; + }; + } + + return $value; + } + + /** + * Add item to the end of the array + * + * @param string $key + * @param mixed $value + * @return object + */ + public function append($key, $value) + { + if (!$this->has($key, $value)) + { + $this->data[$key] = $this->close($value); + } + + return $this; + } + + /** + * Add item to the beginning of the array + * + * @param string $key + * @param mixed $value + * @return object + */ + public function prepend($key, $value) + { + if ($this->has($key)) + { + unset($this->data[$key]); + } + + $data = array($key => $this->close($value)); + + $this->data = $data + $this->data; + + return $this; + } + + /** + * Add item to the array before specificed $idx + * + * @param string $idx + * @param string $key + * @param mixed $value + * @return object + */ + public function insertBefore($idx, $key, $value=null) + { + if ($this->has($key)) + { + $value = $value ?: $this->data[$key]; + + unset($this->data[$key]); + } + + $data = array(); + foreach ($this->data as $k => $v) + { + if ($idx == $k) + { + $data[$key] = $this->close($value); + } + $data[$k] = $v; + } + $this->data = $data; + + if (!$this->has($key)) + { + $this->append($key, $value); + } + + return $this; + } + + /** + * Add item to the array after specificed $idx + * + * @param string $idx + * @param string $key + * @param mixed $value + * @return object + */ + public function insertAfter($idx, $key, $value=null) + { + if ($this->has($key)) + { + $value = $value ?: $this->data[$key]; + + unset($this->data[$key]); + } + + $data = array(); + foreach ($this->data as $k => $v) + { + $data[$k] = $v; + if ($idx == $k) + { + $data[$key] = $this->close($value); + } + } + $this->data = $data; + + if (!$this->has($key)) + { + $this->append($key, $value); + } + + return $this; + } + + /** + * Determine if rule exist for a given key + * + * @param string $key + * @return boolean + */ + public function has($key) + { + return isset($this->data[$key]); + } + + /** + * Get a rule for a specific key + * + * @param string $key + * @return mixed + */ + public function get($key) + { + if (array_key_exists($key, $this->data)) + { + return $this->close($this->data[$key]); + } + + return null; + } + + /** + * Get all of the rules from the bag for a given key + * + * @param string $key + * @param mixed $value + * @return mixed + */ + public function set($key, $value) + { + $this->data[$key] = $this->close($value); + + return $this; + } + + /** + * Get all of the rules + * + * @return array + */ + public function all() + { + return $this->data; + } + + /** + * Get the number of rules + * + * @return integer + */ + public function count() + { + return count($this->data); + } + + /** + * Are there any rules? + * + * @return boolean + */ + public function any() + { + return ($this->count() > 0); + } + + /** + * Clear all rules + * + * @return object + */ + public function clear() + { + $this->data = array(); + + return $this; + } + + /** + * Merge a new array of rules into the bag. + * + * @param array $data + * @return object + */ + public function merge(array $data) + { + $this->data = array_merge($this->data, $data); + + return $this; + } + + /** + * Rewind to beginning of array + * + * @return void + */ + public function rewind() + { + reset($this->data); + } + + /** + * Get current item in the array + * + * @return object + */ + public function current() + { + return current($this->data); + } + + /** + * Get the key of the current item + * + * @return string + */ + public function key() + { + return key($this->data); + } + + /** + * Move to next item in the array + * + * @return void + */ + public function next() + { + next($this->data); + } + + /** + * Is array position valid? + * + * @return boolean + */ + public function valid() + { + return key($this->data) !== null; + } +} diff --git a/core/libraries/Hubzero/Search/Adapters/SolrIndexAdapter.php b/core/libraries/Hubzero/Search/Adapters/SolrIndexAdapter.php new file mode 100755 index 00000000000..ca38518601d --- /dev/null +++ b/core/libraries/Hubzero/Search/Adapters/SolrIndexAdapter.php @@ -0,0 +1,297 @@ +get('solr_core'); + $port = $config->get('solr_port'); + $host = $config->get('solr_host'); + $path = $config->get('solr_path'); + + $this->logPath = $config->get('solr_log_path'); + + // Build the Solr config object + $solrConfig = array( 'endpoint' => + array( $core => + array('host' => $host, + 'port' => $port, + 'path' => $path, + 'core' => $core, + ) + ) + ); + + // Create the client + $this->connection = new Solarium\Client($solrConfig); + + // Create the Solr Query object + $this->query = $this->connection->createSelect(); + } + + /** + * getLogs - Returns an array of search engine query log entries + * + * @access public + * @return void + */ + public function getLogs() + { + if (file_exists($this->logPath)) + { + $log = Filesystem::read($this->logPath); + $levels = array(); + $this->logs = explode("\n", $log); + } + else + { + return array(); + } + + return $this->logs; + } + + /** + * lastInsert - Returns the timestamp of the last document indexed + * + * @access public + * @return void + */ + public function lastInsert() + { + $query = $this->connection->createSelect(); + $query->setQuery('*:*'); + $query->setFields(array('timestamp')); + $query->addSort('timestamp', 'DESC'); + $query->setRows(1); + $query->setStart(0); + + $results = $this->connection->execute($query); + foreach ($results as $document) + { + foreach ($document as $field => $value) + { + $result = $value; + return $result; + } + } + } + + /** + * status - Checks whether or not the search engine is responding + * + * @access public + * @return void + */ + public function status() + { + try + { + $pingRequest = $this->connection->createPing(); + $ping = $this->connection->ping($pingRequest); + $pong = $ping->getData(); + $alive = false; + + if (isset($pong['status']) && $pong['status'] === "OK") + { + return true; + } + } + catch (\Solarium\Exception $e) + { + return false; + } + } + + + /** + * index - Stores a document within an index + * + * @param mixed $document + * @access public + * @return void + */ + public function index($document, $overwrite = null, $commitWithin = null, $buffer = 1500) + { + $this->initBufferAdd($overwrite, $commitWithin, $buffer); + $this->addDocument($document); + } + + /** + * optimize - Defragment the index + * + * @access public + * @return Solarium\QueryType\Update\Result + */ + public function optimize() + { + $update = $this->connection->createUpdate(); + $update->addOptimize(); + return $this->connection->update($update); + } + + /** + * Initialize Solarium bufferAdd plugin + * @param boolean $overwrite if true, overwrites existing entries with the same docId + * @param int $commitWithin time in milliseconds that a commit should happen + * @param int $buffer max number of documents to add before flushing + * @return Solarium\Plugin\BufferAdd\BufferAdd + */ + public function initBufferAdd($overwrite = null, $commitWithin = null, $buffer = null) + { + if (!isset($this->bufferAdd)) + { + $this->bufferAdd = $this->connection->getPlugin('bufferedadd'); + $this->commitWithin = $commitWithin; + $this->overwrite = $overwrite; + + // When Solarium updates with the ability to preset commitWithin and Overwrite, + // this buffer increase won't be necessary. + // This prevents the automatically flushing in the event there are more records than the batch size, + // since the automatically flushing doesn't set the commitWithin or overwrite values + // for the records flushed. + + $buffer++; + $this->bufferAdd->setBufferSize($buffer); + } + return $this->bufferAdd; + } + + public function addDocument($document) + { + $update = $this->connection->createUpdate(); + $newDoc = $update->createDocument(); + foreach ($document as $field => $value) + { + if (!is_string($value)) + { + $newDoc->$field = json_decode(json_encode($value), true); + } + else + { + $newDoc->setField($field, $value); + } + } + $this->initBufferAdd()->addDocument($newDoc); + } + + /** + * deleteById - Removes a single document from the search index + * + * @param string $id + * @access public + * @return mixed string if error caught + */ + public function delete($query) + { + $deleteQuery = $this->parseQuery($query); + + if (!empty($deleteQuery)) + { + try + { + $update = $this->connection->createUpdate(); + $update->addDeleteQuery($deleteQuery); + $update->addCommit(); + $response = $this->connection->update($update); + return null; + } + catch (\Solarium\Exception\HttpException $e) + { + $body = json_decode($e->getBody()); + $message = isset($body->error->msg) ? $body->error->msg : $e->getStatusMessage(); + return $message; + } + } + else + { + return false; + } + } + + /** + * updateIndex - Updates a document existing in the search index + * + * @param mixed $document + * @param mixed $id + * @access public + * @return void + */ + public function updateIndex($document, $commitWithin = 3000) + { + $this->index($document, true, $commitWithin); + return $this->finalize(); + } + + /** + * parseQuery - Translates symbols from query string + * + * @param array $query - Passes in the query array + * @access private + * @return void + */ + private function parseQuery($query) + { + $string = ''; + if (is_array($query)) + { + foreach ($query as $index => $value) + { + $string .= !empty($string) ? ' AND ' : ''; + $string .= $index . ':' . $value; + } + } + else + { + $string = 'id:' . $query; + } + return $string; + } + + /** + * Automatically flushes any remaining documents in the buffer + * + * @return mixed string if error caught/ null if successful + */ + public function finalize() + { + try + { + if (isset($this->bufferAdd)) + { + $this->bufferAdd->flush($this->overwrite, $this->commitWithin); + } + return null; + } + catch (\Solarium\Exception\HttpException $e) + { + $body = json_decode($e->getBody()); + $message = isset($body->error->msg) ? $body->error->msg : $e->getStatusMessage(); + return $message; + } + } +} diff --git a/core/libraries/Hubzero/Search/Adapters/SolrQueryAdapter.php b/core/libraries/Hubzero/Search/Adapters/SolrQueryAdapter.php new file mode 100755 index 00000000000..1ac713c6d4a --- /dev/null +++ b/core/libraries/Hubzero/Search/Adapters/SolrQueryAdapter.php @@ -0,0 +1,477 @@ +get('solr_core'); + $port = $config->get('solr_port'); + $host = $config->get('solr_host'); + $path = $config->get('solr_path'); + + $this->logPath = $config->get('solr_log_path'); + + // Build the Solr config object + $solrConfig = array( 'endpoint' => + array( $core => + array('host' => $host, + 'port' => $port, + 'path' => $path, + 'core' => $core, + ) + ) + ); + + // Create the client + $this->connection = new Solarium\Client($solrConfig); + + // Add plugin to accept bigger requests + $this->connection->getPlugin('postbigrequest'); + + // Make config accessible + $this->config = $solrConfig; + + // Create the Solr Query object + $this->query = $this->connection->createSelect(); + } + + /** + * Get MoreLikeThis + * + * @access public + * @return SolariumQuery + */ + public function getMoreLikeThis($terms) + { + // Get morelikethis settings + $mltQuery = $this->connection->createSelect(); + $mltQuery->setQuery($terms) + ->getMoreLikeThis() + ->setFields('text'); + + // Executes the query and returns the result + $resultSet = $this->connection->select($mltQuery); + $mlt = $resultSet->getMoreLikeThis(); + + return $resultSet; + } + + /** + * spellCheck Returns terms suggestions + * + * @param mixed $terms + * @access public + * @return dictionary + */ + public function spellCheck($terms) + { + // Set the spellCheck Query + $scQuery = $this->connection->createSelect(); + $scQuery->setRows(0) + ->getSpellcheck() + ->setQuery($terms) + ->setCount('5'); + // This executes the query and returns the result + $spellcheckResults = $this->connection->select($scQuery)->getSpellcheck(); + + return $spellcheckResults; + } + + /** + * getSuggestions Returns indexed terms + * + * @param mixed $terms + * @access public + * @return array + */ + public function getSuggestions($terms) + { + // Rewrite for easier keyboard typing + $config = $this->config['endpoint']['hubsearch']; + + // Create the base URL + $url = rtrim(Request::Root(), '/\\'); + + // Use the correct port + $url .= ':' . $config['port']; + + // Use the correct core + $url .= '/solr/' . $config['core']; + + // Perform a select operation + $url .= '/select?fl=id'; + + // Derive user permission filters + $this->restrictAccess(); + $userPerms = $this->query->getFilterQuery('userPerms')->getQuery(); + $url .= '&fq=' . $userPerms; + + // Limit rows, not interested in results, just facets + $url .= '&rows=0'; + + // Select all, honestly doesn't matter + $url .= '&q=*:*'; + + // Enable Facets, set the mandatory field + $url .= '&facet=true&facet.field=author_auto&facet.field=tags_auto&facet.field=title_auto'; + + // Set the minimum count, could tweak to only most popular things + $url .= '&facet.mincount=1'; + + // The actual searching part + $url .= '&facet.prefix=' . strtolower($terms); + + // Make it JSON + $url .= '&wt=json'; + + $client = new \GuzzleHttp\Client(); + $res = $client->get($url); + $resultSet = $res->json()['facet_counts']['facet_fields']; + + $suggestions = array(); + + foreach ($resultSet as $results) + { + $x = 0; + foreach ($results as $i => $result) + { + if ($i % 2 == 0) + { + // Prevents too many results from being suggested + if ($x >= 10) + { + break; + } + array_push( $suggestions, $result); + $x++; + } + + } + } + + return $suggestions; + } + + /** + * query + * + * @param mixed $terms + * @access public + * @return void + */ + public function query($terms) + { + $this->query->setQuery($terms); + return $this; + } + + /** + * run + * + * @access public + * @return void + */ + public function run() + { + $this->resultset = $this->connection->select($this->query); + $this->numFound = $this->resultset->getNumFound(); + $this->results = $this->getResults(); + $this->resultsFacetSet = $this->resultset->getFacetSet(); + return $this; + } + + /** + * getNumFound + * + * @access public + * @return void + */ + public function getNumFound() + { + return $this->numFound; + } + + /** + * getFacetCount + * + * @param mixed $name + * @access public + * @return void + */ + public function getFacetCount($name) + { + $count = $this->resultset->getFacetSet()->getFacet($name)->getValue(); + return $count; + } + + /** + * + * + * @param string $name name provided for the multiFacet set. + * @access public + * @return \Solarium\QueryType\Select\Query\Component\Facet\MultiQuery + */ + public function getFacetMultiQuery($name) + { + $facet = $this->query->getFacetSet()->createFacetMultiQuery($name); + return $facet; + } + + /** + * addFacet + * + * @param mixed $name + * @param array $query + * @access public + * @return void + */ + public function addFacet($name, $query = array()) + { + $this->facetSet = $this->query->getFacetSet(); + + $string = $this->makeQueryString($query); + $this->facetSet->createFacetQuery($name)->setQuery($string); + + return $this; + } + + /** + * addFilter + * + * @param mixed $name + * @param mixed $query + * @access public + * @return void + */ + public function addFilter($name, $query = array(), $tag = 'root_type') + { + if (is_array($query)) + { + $string = $this->makeQueryString($query); + } + elseif (is_string($query)) + { + $string = $query; + if ($name == 'BoundingBox') + { + $this->query->setOptions(array('geo'=> true)); + } + } + $filterParams = array(); + $filterParams['key'] = $name; + $filterParams['query'] = $string; + if ($tag) + { + $filterParams['tag'] = $tag; + } + + $this->query->createFilterQuery($filterParams); + return $this; + } + + /** + * fields + * + * @param mixed $fieldArray + * @access public + * @return void + */ + public function fields($fieldArray) + { + $this->query->setFields($fieldArray); + return $this; + } + + /** + * sortBy + * + * @param mixed $field + * @param mixed $direction + * @access public + * @return void + */ + public function sortBy($field, $direction) + { + $this->query->addSort($field, $direction); + return $this; + } + + /** + * limit + * + * @param mixed $limit + * @access public + * @return void + */ + public function limit($limit) + { + $this->query->setRows($limit); + return $this; + } + + /** + * start + * + * @param mixed $offset + * @access public + * @return void + */ + public function start($offset) + { + $this->query->setStart($offset); + return $this; + } + + /** + * restrictAccess + * + * @access public + * @return void + */ + public function restrictAccess() + { + $accessFilter = $this->getAccessString(); + $this->addFilter('userPerms', $accessFilter, 'root_type'); + } + + public function getAccessString() + { + $accessFilter = ''; + if (User::isGuest()) + { + $accessFilter = "(access_level:public)"; + } + else + { + $user = User::get('id'); + $userFilter = 'OR (access_level:private AND owner_type:user AND owner:' . $user . ')'; + $accessFilter = "(access_level:public) OR (access_level:registered) " . $userFilter; + + $userGroups = \Hubzero\User\Helper::getGroups($user); + if (!$userGroups) + { + $userGroups = array(); + } + $userGroups = array_map(function($group){ + return $group->gidNumber; + }, $userGroups); + $userGroups = array_unique($userGroups); + if (!empty($userGroups)) + { + $userGroupString = implode(' OR ', $userGroups); + $groupFilter = 'OR (access_level:private AND owner_type:group AND owner:(' . $userGroupString . '))'; + $accessFilter .= ' ' . $groupFilter; + } + + $addon = Event::trigger('search.onAddPermissionSet'); + foreach ($addon as $add) + { + $accessFilter .= $add; + } + } + return $accessFilter; + } + + /** + * getResults + * + * @access public + * @return void + */ + public function getResults() + { + if (!isset($this->resultset)) + { + $this->run(); + } + + $documents = array(); + foreach ($this->resultset as $document) + { + array_push($documents, $document); + } + + foreach ($documents as &$document) + { + $document = $document->getFields(); + } + + return $documents; + } + + /** + * makeQueryString + * + * @param array $query + * @access private + * @return void + */ + private function makeQueryString($query = array()) + { + $subject = $query[0]; + $operator = $query[1]; + $operand = $query[2]; + + switch ($operator) + { + case '=': + $string = $subject . ':' . $operand; + break; + } + + return $string; + } + + + /** + * lastInsert - Returns the timestamp of the latest indexed document + * + * @access public + * @return void + */ + public function lastInsert() + { + $query = $this->connection->createSelect(); + $query->setQuery('*:*'); + $query->setFields(array('timestamp')); + $query->addSort('timestamp', 'DESC'); + $query->setRows(1); + $query->setStart(0); + + $results = $this->connection->execute($query); + foreach ($results as $document) + { + foreach ($document as $field => $value) + { + $result = $value; + return $result; + } + } + } +} diff --git a/core/libraries/Hubzero/Search/Index.php b/core/libraries/Hubzero/Search/Index.php new file mode 100755 index 00000000000..92264c0f86b --- /dev/null +++ b/core/libraries/Hubzero/Search/Index.php @@ -0,0 +1,116 @@ +get('engine'); + if ($engine != 'hubgraph') + { + $adapter = "\\Hubzero\\Search\\Adapters\\" . ucfirst($engine) . 'IndexAdapter'; + $this->adapter = new $adapter($config); + } + return $this; + } + + /** + * getLogs - Returns an array of search engine query log entries + * + * @access public + * @return void + */ + public function getLogs() + { + $logs = $this->adapter->getLogs(); + return $logs; + } + + /** + * defragment search index + * + * @return void + */ + public function optimize() + { + return $this->adapter->optimize(); + } + + /** + * lastInsert - Returns the timestamp of the last document indexed + * + * @access public + * @return void + */ + public function lastInsert() + { + $lastInsert = $this->adapter->lastInsert(); + return $lastInsert; + } + + /** + * status - Checks whether or not the search engine is responding + * + * @access public + * @return void + */ + public function status() + { + return $this->adapter->status(); + } + + /** + * index - Stores a document within an index + * + * @param mixed $document + * @access public + * @return void + */ + public function index($document, $overwrite = null, $commitWithin = 3000, $buffer = 1500) + { + return $this->adapter->index($document, $overwrite, $commitWithin, $buffer); + } + + /** + * updateIndex - Update existing index item + * + * @param mixed $document + * @access public + * @return void + */ + public function updateIndex($document, $commitWithin = 3000) + { + return $this->adapter->updateIndex($document, $commitWithin); + } + + /** + * delete - Deletes a document from the index + * + * @param string $id + * @access public + * @return void + */ + public function delete($id) + { + return $this->adapter->delete($id); + } +} diff --git a/core/libraries/Hubzero/Search/IndexInterface.php b/core/libraries/Hubzero/Search/IndexInterface.php new file mode 100755 index 00000000000..1ddb1e04cca --- /dev/null +++ b/core/libraries/Hubzero/Search/IndexInterface.php @@ -0,0 +1,56 @@ +get('engine'); + + $adapter = "\\Hubzero\\Search\\Adapters\\" . ucfirst($engine) . 'QueryAdapter'; + + $this->adapter = new $adapter($config); + } + + /** + * Get MoreLikeThis + * + * @param mixed $terms + * @return array + */ + public function getMoreLikeThis($terms) + { + return $this->adapter->getMoreLikeThis($terms); + } + + /** + * Returns terms suggestions + * + * @param mixed $terms + * @return array + */ + public function spellCheck($terms) + { + return $this->adapter->spellCheck($terms); + } + + /** + * Returns indexed terms + * + * @param mixed $terms + * @return array + */ + public function getSuggestions($terms) + { + return $this->adapter->getSuggestions($terms); + } + + /** + * Sets the query string + * + * @param mixed $terms + * @return object + */ + public function query($terms) + { + $this->adapter->query($terms); + return $this; + } + + /** + * Sets the fields to be returned by the query. + * + * @param array $fields + * @return object + */ + public function fields($fields) + { + $this->adapter->fields($fields); + return $this; + } + + /** + * Adds a filter to the query + * + * @param mixed $name + * @param array $query + * @param string $tag + * @return object + */ + public function addFilter($name, $query = array(), $tag = 'root_type') + { + $this->adapter->addFilter($name, $query, $tag); + return $this; + } + + /** + * Adds a facet to the query object. + * + * @param string $name Used to identify facet when result is returned. + * @param array $query The query array with a indexes of name, operator, and value + * @return object + */ + public function addFacet($name, $query = array()) + { + $this->adapter->addFacet($name, $query); + return $this; + } + + /** + * Returns an integer value of a defined facet. + * + * @param mixed $name + * @return int + */ + public function getFacetCount($name) + { + return $this->adapter->getFacetCount($name); + } + + /** + * limit - Set the number of results to be returned + * + * @param int $limit + * @return object + */ + public function limit($limit) + { + $this->adapter->limit($limit); + return $this; + } + + /** + * Executes the query and returns an array of results. + * + * @return array + */ + public function getResults() + { + return $this->adapter->getResults(); + } + + /** + * Returns the total number of matching results, even outside of limit. + * + * @return integer + */ + public function getNumFound() + { + return $this->adapter->getNumFound(); + } + + /** + * Offset of search index results. Warning: non-deterministic. + * + * @param mixed $start + * @return object + */ + public function start($start) + { + $this->adapter->start($start); + return $this; + } + + /** + * Order results by a field in a given direction. + * + * @param mixed $field name of a field + * @param mixed $direction (ASC or DESC) + * @return object + */ + public function sortBy($field, $direction) + { + $this->adapter->sortBy($field, $direction); + return $this; + } + + /** + * Performs the query, does not return results. + * + * @return void + */ + public function run() + { + return $this->adapter->run(); + } + + /** + * Applies CMS permissions for the current user. + * + * @return object + */ + public function restrictAccess() + { + $this->adapter->restrictAccess(); + return $this; + } +} diff --git a/core/libraries/Hubzero/Search/QueryInterface.php b/core/libraries/Hubzero/Search/QueryInterface.php new file mode 100755 index 00000000000..8580b01f6d3 --- /dev/null +++ b/core/libraries/Hubzero/Search/QueryInterface.php @@ -0,0 +1,132 @@ +getStore()->session($id); + } + + /** + * Get Session by User Id + * + * @param integer $id User ID + * @return mixed + */ + public static function getSessionWithUserId($userid) + { + // get list of all sessions + $sessions = \App::get('session')->getStore()->all(array( + 'guest' => 0, + 'distinct' => 1 + )); + + // see if any session matches our userid + foreach ($sessions as $session) + { + if ($session->userid == $userid) + { + return $session; + } + } + + // nothing found + return null; + } + + /** + * Get list of all sessions + * + * @param array $filters Filters to apply + * @return array + */ + public static function getAllSessions($filters = array()) + { + return \App::get('session')->getStore()->all($filters); + } +} diff --git a/core/libraries/Hubzero/Session/Manager.php b/core/libraries/Hubzero/Session/Manager.php new file mode 100644 index 00000000000..2cea4379c67 --- /dev/null +++ b/core/libraries/Hubzero/Session/Manager.php @@ -0,0 +1,908 @@ +store = Store::getInstance($store, $options); + + // Set options + $this->setOptions($options); + + // Pass session id in query string when cookie not available. + // This is used, in particular, to allow QuickTime plugin in Safari on the Mac + // to view private mp4. QuickTime does not pass the browser's cookies to the site + if (!isset($_COOKIE[session_name()]) && isset($_GET['PHPSESSID'])) + { + if ((strlen($_GET['PHPSESSID']) == 32) && ctype_alnum($_GET['PHPSESSID'])) + { + if ($this->store->read($_GET['PHPSESSID']) != '') + { + session_id($_GET['PHPSESSID']); + } + } + } + + $this->setCookieParams(); + + // Load the session + $this->start(); + + // Initialise the session + $this->setCounter(); + $this->setTimers(); + + $this->state = 'active'; + + // Perform security checks + $this->validate(); + } + + /** + * Session object destructor + * + * @return void + */ + public function __destruct() + { + $this->close(); + } + + /** + * Get current state of session + * + * @return string The session state + */ + public function getState() + { + return $this->state; + } + + /** + * Get session store object + * + * @return object The session store object + */ + public function getStore() + { + return $this->store; + } + + /** + * Get expiration time in minutes + * + * @return integer The session expiration time in minutes + */ + public function getExpire() + { + return $this->expire; + } + + /** + * Get a session token, if a token isn't set yet one will be generated. + * + * Tokens are used to secure forms from spamming attacks. Once a token + * has been generated the system will check the post request to see if + * it is present, if not it will invalidate the session. + * + * @param boolean $forceNew If true, force a new token to be created + * @return string The session token + */ + public function getToken($forceNew = false) + { + $token = $this->get('session.token'); + + // Create a token + if ($token === null || $forceNew) + { + $token = $this->createToken(12); + + $this->set('session.token', $token); + } + + return $token; + } + + /** + * Method to determine if a token exists in the session. If not the + * session will be set to expired + * + * @param string $tCheck Hashed token to be verified + * @param boolean $forceExpire If true, expires the session + * @return boolean + */ + public function hasToken($tCheck, $forceExpire = true) + { + // Check if a token exists in the session + $tStored = $this->get('session.token'); + + // Check token + if ($tStored !== $tCheck) + { + if ($forceExpire) + { + $this->state = 'expired'; + } + + return false; + } + + return true; + } + + /** + * Method to determine a hash for anti-spoofing variable names + * + * @param boolean $forceNew If true, force a new token to be created + * @return string Hashed var name + */ + public static function getFormToken($forceNew = false) + { + $hash = \App::hash(\User::get('id', 0) . \App::get('session')->getToken($forceNew)); + + return $hash; + } + + /** + * Checks for a form token in the request. + * + * @param string $method The request method in which to look for the token key. + * @param boolean $capture Return result instead of throwing exception? + * @return boolean True if found and valid, false otherwise. + */ + public static function checkToken($method = 'post', $capture = false) + { + $token = self::getFormToken(); + + $result = false; + + if (is_string($method) && strstr($method, ',')) + { + $method = explode(',', $method); + $method = array_map('trim', $method); + } + $method = (array) $method; + + foreach ($method as $m) + { + if (\App::get('request')->getVar($token, '', $m, 'alnum')) + { + $result = true; + break; + } + + if (\App::get('session')->isNew()) + { + // Redirect to login screen. + \App::redirect(\Route::url('index.php'), \App::get('language')->txt('JLIB_ENVIRONMENT_SESSION_EXPIRED')); + \App::close(); + } + } + + if (!$result) + { + if ($capture) + { + return $result; + } + + \App::abort(403, \App::get('language')->txt('JINVALID_TOKEN')); + } + + return $result; + } + + /** + * Get session name + * + * @return string The session name + */ + public function getName() + { + if ($this->state === 'destroyed') + { + return null; + } + + return session_name(); + } + + /** + * Get session id + * + * @return string The session name + */ + public function getId() + { + if ($this->state === 'destroyed') + { + return null; + } + + return session_id(); + } + + /** + * Get the session handlers + * + * @return array An array of available session handlers + */ + public static function getStores() + { + // Get a list of types, only including php files + $glob = glob(__DIR__ . DIRECTORY_SEPARATOR . 'Storage' . DIRECTORY_SEPARATOR . '*.php'); + + $names = array(); + + if ($glob === false) + { + return $names; + } + + // Loop through the types and find the ones that are available + foreach ($glob as $handler) + { + // Get just the file name + $name = basename($handler); + + // Derive the class name from the type + $class = __NAMESPACE__ . '\\Storage\\' . str_ireplace('.php', '', ucfirst(trim($name))); + + // If the class doesn't exist, these are not the droids you're looking for... + if (!class_exists($class)) + { + continue; + } + + // Our class exists, so now we just need to know if it passes it's test method + if (call_user_func_array(array($class, 'isAvailable'), array())) + { + $names[] = str_ireplace('.php', '', $name); + } + } + + $names = array_map('strtolower', $names); + + return $names; + } + + /** + * Check whether this session is currently created + * + * @return boolean True on success. + */ + public function isNew() + { + if ($this->get('session.counter') === 1) + { + return true; + } + + return false; + } + + /** + * Get data from the session store + * + * @param string $name Name of a variable + * @param mixed $default Default value of a variable if not set + * @param string $namespace Namespace to use, default to 'default' + * @return mixed Value of a variable + */ + public function get($name, $default = null, $namespace = 'default') + { + // Add prefix to namespace to avoid collisions + $namespace = '__' . $namespace; + + if ($this->state !== 'active' && $this->state !== 'expired') + { + // @TODO :: generated error here + $error = null; + + return $error; + } + + if (isset($_SESSION[$namespace][$name])) + { + return $_SESSION[$namespace][$name]; + } + + return $default; + } + + /** + * Set data into the session store. + * + * @param string $name Name of a variable. + * @param mixed $value Value of a variable. + * @param string $namespace Namespace to use, default to 'default'. + * @return mixed Old value of a variable. + */ + public function set($name, $value = null, $namespace = 'default') + { + // Add prefix to namespace to avoid collisions + $namespace = '__' . $namespace; + + if ($this->state !== 'active') + { + // @TODO :: generated error here + return null; + } + + $old = isset($_SESSION[$namespace][$name]) ? $_SESSION[$namespace][$name] : null; + + if (null === $value) + { + unset($_SESSION[$namespace][$name]); + } + else + { + $_SESSION[$namespace][$name] = $value; + } + + return $old; + } + + /** + * Check whether data exists in the session store + * + * @param string $name Name of variable + * @param string $namespace Namespace to use, default to 'default' + * @return boolean True if the variable exists + */ + public function has($name, $namespace = 'default') + { + // Add prefix to namespace to avoid collisions. + $namespace = '__' . $namespace; + + if ($this->state !== 'active') + { + // @TODO :: generated error here + return null; + } + + return isset($_SESSION[$namespace][$name]); + } + + /** + * Unset data from the session store + * + * @param string $name Name of variable + * @param string $namespace Namespace to use, default to 'default' + * @return mixed The value from session or NULL if not set + */ + public function clear($name, $namespace = 'default') + { + // Add prefix to namespace to avoid collisions + $namespace = '__' . $namespace; + + if ($this->state !== 'active') + { + // @TODO :: generated error here + return null; + } + + $value = null; + if (isset($_SESSION[$namespace][$name])) + { + $value = $_SESSION[$namespace][$name]; + unset($_SESSION[$namespace][$name]); + } + + return $value; + } + + /** + * Start a session. + * + * Creates a session (or resumes the current one based on the state of the session) + * + * @return boolean true on success + */ + protected function start() + { + // Start session if not started + if ($this->state == 'restart') + { + session_id($this->createId()); + } + else + { + $session_name = session_name(); + + if (!\Request::getVar($session_name, false, 'COOKIE')) + { + if ($id = \Request::getVar($session_name)) + { + session_id($id); + setcookie($session_name, '', time() - 3600); + } + else + { + session_id($this->createId()); + } + } + } + + session_cache_limiter('none'); + session_start(); + + // Regenerate session id if passed a session id that no longer exists + if ($_SESSION === array()) + { + session_destroy(); + session_id($this->createId()); + session_start(); + } + + return true; + } + + /** + * Frees all session variables and destroys all data registered to a session + * + * This method resets the $_SESSION variable and destroys all of the data associated + * with the current session in its storage (file or DB). It forces new session to be + * started after this method is called. It does not unset the session cookie. + * + * @return boolean True on success + * @see session_destroy() + * @see session_unset() + */ + public function destroy() + { + // Session was already destroyed + if ($this->state === 'destroyed') + { + return true; + } + + // In order to kill the session altogether, such as to log the user out, the session id + // must also be unset. If a cookie is used to propagate the session id (default behavior), + // then the session cookie must be deleted. + if (isset($_COOKIE[session_name()])) + { + $cookie_domain = $this->cookie_domain; + $cookie_path = $this->cookie_path; + + setcookie(session_name(), '', time() - 42000, $cookie_path, $cookie_domain); + } + + session_unset(); + session_destroy(); + + $this->state = 'destroyed'; + + return true; + } + + /** + * Restart an expired or locked session. + * + * @return boolean True on success + * @see destroy + */ + public function restart() + { + $this->destroy(); + + if ($this->state !== 'destroyed') + { + // @TODO :: generated error here + return false; + } + + // Re-register the session handler after a session has been destroyed, to avoid PHP bug + $this->store->register(); + + $this->state = 'restart'; + + // Regenerate session id + $id = $this->createId(); + + session_id($id); + + $this->start(); + $this->state = 'active'; + + $this->validate(); + $this->setCounter(); + + return true; + } + + /** + * Create a new session and copy variables from the old one + * + * @return boolean $result True on success + */ + public function fork() + { + if ($this->state !== 'active') + { + // @TODO :: generated error here + return false; + } + + // Save values + $values = $_SESSION; + + // Keep session config + $trans = ini_get('session.use_trans_sid'); + if ($trans) + { + ini_set('session.use_trans_sid', 0); + } + + $cookie = session_get_cookie_params(); + + // Create new session id + $id = $this->createId(); + + // Kill session + session_destroy(); + + // Re-register the session store after a session has been destroyed, to avoid PHP bug + $this->store->register(); + + // Restore config + ini_set('session.use_trans_sid', $trans); + session_set_cookie_params($cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure']); + + // Restart session with new id + session_id($id); + session_start(); + + return true; + } + + /** + * Writes session data and ends session + * + * Session data is usually stored after your script terminated without the need + * to call Session::close(), but as session data is locked to prevent concurrent + * writes only one script may operate on a session at any time. When using + * framesets together with sessions you will experience the frames loading one + * by one due to this locking. You can reduce the time needed to load all the + * frames by ending the session as soon as all changes to session variables are + * done. + * + * @return void + * @see session_write_close() + */ + public function close() + { + session_write_close(); + } + + /** + * Create a session id + * + * @return string Session ID + */ + protected function createId() + { + $id = 0; + + while (strlen($id) < 32) + { + $id .= mt_rand(0, mt_getrandmax()); + } + + return md5(uniqid($id, true)); + } + + /** + * Set session cookie parameters + * + * @return void + */ + protected function setCookieParams() + { + $cookie = session_get_cookie_params(); + + if ($this->force_ssl) + { + $cookie['secure'] = true; + } + + if ($this->cookie_domain != '') + { + $cookie['domain'] = $this->cookie_domain; + } + + if ($this->cookie_path != '') + { + $cookie['path'] = $this->cookie_path; + } + + session_set_cookie_params($cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure']); + } + + /** + * Create a token-string + * + * @param integer $length Length of string + * @return string Generated token + */ + protected function createToken($length = 32) + { + static $chars = '0123456789abcdef'; + + $max = strlen($chars) - 1; + $token = ''; + $name = session_name(); + + for ($i = 0; $i < $length; ++$i) + { + $token .= $chars[(rand(0, $max))]; + } + + return md5($token . $name); + } + + /** + * Set counter of session usage + * + * @return boolean True on success + */ + protected function setCounter() + { + $counter = $this->get('session.counter', 0); + + ++$counter; + + $this->set('session.counter', $counter); + + return true; + } + + /** + * Set the session timers + * + * @return boolean True on success + * + * @since 11.1 + */ + protected function setTimers() + { + if (!$this->has('session.timer.start')) + { + $start = time(); + + $this->set('session.timer.start', $start); + $this->set('session.timer.last', $start); + $this->set('session.timer.now', $start); + } + + $this->set('session.timer.last', $this->get('session.timer.now')); + $this->set('session.timer.now', time()); + + return true; + } + + /** + * Set additional session options + * + * @param array $options List of parameter + * @return boolean True on success + */ + protected function setOptions($options) + { + // Set name + if (isset($options['name'])) + { + session_name(md5($options['name'])); + } + + // Set id + if (isset($options['id'])) + { + session_id($options['id']); + } + + // Set expire time + if (isset($options['expire'])) + { + $this->expire = $options['expire']; + } + + // Get security options + if (isset($options['security'])) + { + $this->security = explode(',', $options['security']); + } + + if (isset($options['force_ssl'])) + { + $this->force_ssl = (bool) $options['force_ssl']; + } + + if (isset($options['cookie_domain'])) + { + $this->cookie_domain = (string) $options['cookie_domain']; + } + + if (isset($options['cookie_path'])) + { + $this->cookie_path = (string) $options['cookie_path']; + } + + // Sync the session maxlifetime + ini_set('session.gc_maxlifetime', $this->expire); + + return true; + } + + /** + * Do some checks for security reason + * + * - timeout check (expire) + * - ip-fixiation + * - browser-fixiation + * + * If one check failed, session data has to be cleaned. + * + * @param boolean $restart Reactivate session + * @return boolean True on success + * @see http://shiflett.org/articles/the-truth-about-sessions + */ + protected function validate($restart = false) + { + // Allow to restart a session + if ($restart) + { + $this->state = 'active'; + + $this->set('session.client.address', null); + $this->set('session.client.forwarded', null); + $this->set('session.client.browser', null); + $this->set('session.token', null); + } + + // Check if session has expired + if ($this->expire) + { + $curTime = $this->get('session.timer.now', 0); + $maxTime = $this->get('session.timer.last', 0) + $this->expire; + + // Empty session variables + if ($maxTime < $curTime) + { + $this->state = 'expired'; + return false; + } + } + + // Check for client address + if (in_array('fix_adress', $this->security) && isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP) !== false) + { + $ip = $this->get('session.client.address'); + + if ($ip === null) + { + $this->set('session.client.address', $_SERVER['REMOTE_ADDR']); + } + elseif ($_SERVER['REMOTE_ADDR'] !== $ip) + { + $this->state = 'error'; + return false; + } + } + + // Record proxy forwarded for in the session in case we need it later + if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && filter_var($_SERVER['HTTP_X_FORWARDED_FOR'], FILTER_VALIDATE_IP) !== false) + { + $this->set('session.client.forwarded', $_SERVER['HTTP_X_FORWARDED_FOR']); + } + + return true; + } +} diff --git a/core/libraries/Hubzero/Session/Storage/Apc.php b/core/libraries/Hubzero/Session/Storage/Apc.php new file mode 100644 index 00000000000..b0f9745b30c --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/Apc.php @@ -0,0 +1,103 @@ +prefix = $options['prefix']; + } + + parent::__construct($options); + } + + /** + * Read the data for a particular session identifier from the + * SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return string The session data. + */ + public function read($session_id) + { + return (string) apc_fetch($this->key($session_id)); + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + return apc_store($this->key($session_id), $session_data, ini_get("session.gc_maxlifetime")); + } + + /** + * Destroy the data for a particular session identifier in the SessionHandler backend. + * + * @param string $id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + return apc_delete($this->key($session_id)); + } + + /** + * Build the storage key + * + * @param string $id The session identifier. + * @return string + */ + protected function key($id) + { + return $this->prefix . $id; + } + + /** + * Test to see if the SessionHandler is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return extension_loaded('apc'); + } +} diff --git a/core/libraries/Hubzero/Session/Storage/Database.php b/core/libraries/Hubzero/Session/Storage/Database.php new file mode 100644 index 00000000000..ea51b798907 --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/Database.php @@ -0,0 +1,292 @@ +connection = $options['database']; + + if (isset($options['profiler'])) + { + $this->profiler = $options['profiler']; + } + + if (isset($options['skipWrites'])) + { + $this->skipWrites = (bool)$options['skipWrites']; + } + + parent::__construct($options); + } + + /** + * Read the data for a particular session identifier from the SessionHandler backend. + * + * @param string $id The session identifier. + * @return mixed The session data on success, False on failure. + */ + public function read($session_id) + { + // Get the database connection object and verify its connected. + if (!$this->connection->connected()) + { + return false; + } + + try + { + // Get the session data from the database table. + $query = $this->connection->getQuery() + ->select('data') + ->from('#__session') + ->whereEquals('session_id', $session_id); + + $this->connection->setQuery($query->toString()); + + return (string) $this->connection->loadResult(); + } + catch (Exception $e) + { + return false; + } + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + // Skip session write on API and command line calls + if ($this->skipWrites) + { + if ($this->profiler) + { + $this->profiler->mark('sessionStore'); + } + return true; + } + + // Get the database connection object and verify its connected. + if ($this->connection->connected()) + { + try + { + $query = $this->connection->getQuery() + ->update('#__session') + ->set(array( + 'data' => $session_data, + 'time' => (int) time(), + 'ip' => $_SERVER['REMOTE_ADDR'] + )) + ->whereEquals('session_id', $session_id); + + // Try to update the session data in the database table. + $this->connection->setQuery($query->toString()); + + if ($this->connection->execute()) + { + if ($this->profiler) + { + $this->profiler->mark('sessionStore'); + } + return true; + } + + // Since $db->execute did not throw an exception, so the query was successful. + // Either the data changed, or the data was identical. + // In either case we are done. + } + catch (Exception $e) + { + } + } + + if ($this->profiler) + { + $this->profiler->mark('sessionStore'); + } + return false; + } + + /** + * Destroy the data for a particular session identifier in the SessionHandler backend. + * + * @param string $id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + // Get the database connection object and verify its connected. + if (!$this->connection->connected()) + { + return false; + } + + try + { + $query = $this->connection->getQuery() + ->delete('#__session') + ->whereEquals('session_id', $session_id); + + // Remove a session from the database. + $this->connection->setQuery($query->toString()); + + return (boolean) $this->connection->execute(); + } + catch (Exception $e) + { + return false; + } + } + + /** + * Garbage collect stale sessions from the SessionHandler backend. + * + * @param integer $lifetime The maximum age of a session. + * @return boolean True on success, false otherwise. + */ + public function gc($lifetime = 1440) + { + // Get the database connection object and verify its connected. + if (!$this->connection->connected()) + { + return false; + } + + // Determine the timestamp threshold with which to purge old sessions. + $past = time() - $lifetime; + + try + { + $query = $this->connection->getQuery() + ->delete('#__session') + ->where('time', '<', (int) $past); + + // Remove expired sessions from the database. + $this->connection->setQuery($query->toString()); + + return (boolean) $this->connection->execute(); + } + catch (Exception $e) + { + return false; + } + } + + /** + * Get single session data as an object + * + * @param integer $session_id Session Id + * @return object + */ + public function session($session_id) + { + $query = $this->connection->getQuery() + ->select('*') + ->from('#__session') + ->group('userid') + ->group('client_id') + ->order('time', 'desc'); + + $this->connection->setQuery($query->toString()); + return $this->connection->loadObject(); + } + + /** + * Get list of all sessions + * + * @param array $filters + * @return array + */ + public function all($filters = array()) + { + $query = $this->connection->getQuery() + ->select('session_id') + ->select('client_id') + ->select('guest') + ->select('time') + ->select('data') + ->select('userid') + ->select('username') + ->select('ip') + ->from('#__session'); + + $max = ''; + if (isset($filters['distinct']) && $filters['distinct'] == 1) + { + $query->select('MAX(time)', 'time'); + } + + if (isset($filters['guest'])) + { + $query->whereEquals('guest', $filters['guest']); + } + + if (isset($filters['client'])) + { + if (!is_array($filters['client'])) + { + $filters['client'] = array($filters['client']); + } + + $query->whereIn('client_id', $filters['client']); + } + + if (isset($filters['distinct']) && $filters['distinct'] == 1) + { + $query + ->group('session_id') + ->group('userid') + ->group('client_id'); + } + + $query->order('time', 'desc'); + + $this->connection->setQuery($query->toString()); + return $this->connection->loadObjectList(); + } +} diff --git a/core/libraries/Hubzero/Session/Storage/Eaccelerator.php b/core/libraries/Hubzero/Session/Storage/Eaccelerator.php new file mode 100644 index 00000000000..44c50d5981a --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/Eaccelerator.php @@ -0,0 +1,165 @@ +prefix = $options['prefix']; + } + + parent::__construct($options); + } + + /** + * Read the data for a particular session identifier from the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return string The session data. + */ + public function read($session_id) + { + return (string) eaccelerator_get($this->key($session_id)); + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + return eaccelerator_put($this->key($session_id), $session_data, ini_get("session.gc_maxlifetime")); + } + + /** + * Destroy the data for a particular session identifier in the SessionHandler backend. + * + * @param string $id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + return eaccelerator_rm($this->key($session_id)); + } + + /** + * Garbage collect stale sessions from the SessionHandler backend. + * + * @param integer $maxlifetime The maximum age of a session. + * @return boolean True on success, false otherwise. + */ + public function gc($maxlifetime = null) + { + eaccelerator_gc(); + return true; + } + + /** + * Get single session data as an object + * + * @param integer $session_id Session Id + * @return object + */ + public function session($session_id) + { + $session = new Object; + $session->session_id = $session_id; + $session->data = $this->read($session_id); + + return $session; + } + + /** + * Get list of all sessions + * + * @param array $filters + * @return array + */ + public function all($filters = array()) + { + $keys = eaccelerator_list_keys(); + + $data = array(); + + if (is_array($keys)) + { + foreach ($keys as $key) + { + // Trim leading ":" to work around list_keys namespace bug in eAcc. + // This will still work when bug is fixed. + $key['name'] = ltrim($key['name'], ':'); + + if (strpos($key['name'], $this->prefix) === 0) + { + continue; + } + + $session = new Object; + $session->session_id = $file->getName(); + $session->data = (string) eaccelerator_get($key['name']); + + $data[] = $session; + } + } + + return $data; + } + + /** + * Build the storage key + * + * @param string $id The session identifier. + * @return string + */ + protected function key($id) + { + return $this->prefix . $id; + } + + /** + * Test to see if the SessionHandler is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return (extension_loaded('eaccelerator') && function_exists('eaccelerator_get')); + } +} diff --git a/core/libraries/Hubzero/Session/Storage/File.php b/core/libraries/Hubzero/Session/Storage/File.php new file mode 100644 index 00000000000..d412f6f378a --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/File.php @@ -0,0 +1,190 @@ +path = $this->cleanPath($options['session_path']); + $this->files = $options['filesystem']; + + if (!is_dir($this->path) || !is_readable($this->path) || !is_writable($this->path)) + { + throw new Exception('Storage path should be directory with available read/write access.'); + } + + parent::__construct($options); + } + + /** + * Read the data for a particular session identifier from the + * SessionHandler backend. + * + * @param string $id The session identifier. + * @return string The session data. + */ + public function read($session_id) + { + if ($this->files->exists($path = $this->path . DS . $session_id)) + { + return $this->files->get($path); + } + + return ''; + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + $this->files->put($this->path . DS . $session_id, $session_data, true); + } + + /** + * Destroy the data for a particular session identifier in the + * SessionHandler backend. + * + * @param string $id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + $this->files->delete($this->path . DS . $session_id); + } + + /** + * Garbage collect stale sessions from the SessionHandler backend. + * + * @param integer $maxlifetime The maximum age of a session. + * @return boolean True on success, false otherwise. + */ + public function gc($maxlifetime = null) + { + $files = $this->files->files($this->path); + + $tm = time() - $maxlifetime; + + foreach ($files as $file) + { + if (!$file->isFile()) + { + continue; + } + + if ($file->getMTime() <= $tm) + { + $this->files->delete($file->getPathname()); + } + } + } + + /** + * Get single session data as an object + * + * @param integer $session_id Session Id + * @return object + */ + public function session($session_id) + { + $session = new Object; + $session->session_id = $session_id; + $session->data = $this->read($session_id); + + return $session; + } + + /** + * Get list of all sessions + * + * @param array $filters + * @return array + */ + public function all($filters = array()) + { + $files = $this->files->files($this->path); + + $sessions = array(); + + foreach ($files as $file) + { + if (!$file->isFile()) + { + continue; + } + + $session = new Object; + $session->session_id = $file->getName(); + $session->data = $this->files->get($file->getPathname()); + + $sessions[] = $session; + } + + return $sessions; + } + + /** + * Strip additional / or \ in a path name + * + * @param string $path The path to clean + * @param string $ds Directory separator (optional) + * @return string The cleaned path + */ + protected function cleanPath($path, $ds = DIRECTORY_SEPARATOR) + { + $path = trim($path); + + // Remove double slashes and backslahses and convert + // all slashes and backslashes to DIRECTORY_SEPARATOR + return preg_replace('#[/\\\\]+#', $ds, $path); + } +} diff --git a/core/libraries/Hubzero/Session/Storage/Memcache.php b/core/libraries/Hubzero/Session/Storage/Memcache.php new file mode 100644 index 00000000000..42305c1145b --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/Memcache.php @@ -0,0 +1,272 @@ +prefix = $options['prefix']; + } + + parent::__construct($options); + + if (isset($options['compress']) && $options['compress']) + { + $this->compress = MEMCACHE_COMPRESSED; + } + + if (!isset($options['servers']) || empty($options['servers'])) + { + $conf = new \Hubzero\Config\Repository('site'); + + $options['servers'] = array( + array( + 'host' => $config->get('memcache_server_host', 'localhost'), + 'port' => $config->get('memcache_server_port', 11211), + 'weight' => $config->get('memcache_persist', true) + ) + ); + } + + $this->servers = $options['servers']; + } + + /** + * Open the SessionHandler backend. + * + * @param string $save_path The path to the session object. + * @param string $name The name of the session. + * @return boolean True on success, false otherwise. + */ + public function open($save_path, $name) + { + $this->engine = new \Memcache; + + // For each server in the array, we'll just extract the configuration and add + // the server to the Memcached connection. Once we have added all of these + // servers we'll verify the connection is successful and return it back. + foreach ($this->servers as $server) + { + $this->engine->addServer( + $server['host'], $server['port'], $server['weight'] + ); + } + + return true; + } + + /** + * Close the SessionHandler backend. + * + * @return boolean True on success, false otherwise. + */ + public function close() + { + return $this->engine->close(); + } + + /** + * Read the data for a particular session identifier from the SessionHandler backend. + * + * @param string $id The session identifier. + * @return string The session data. + */ + public function read($session_id) + { + $key = $this->key($session_id); + + $this->expiration($key); + + return $this->engine->get($key); + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + $key = $this->key($session_id); + + if ($this->engine->get($key . '_expire')) + { + $this->engine->replace($key . '_expire', time(), 0); + } + else + { + $this->engine->set($key . '_expire', time(), 0); + } + if ($this->engine->get($key)) + { + $this->engine->replace($key, $session_data, $this->compress); + } + else + { + $this->engine->set($key, $session_data, $this->compress); + } + + return; + } + + /** + * Destroy the data for a particular session identifier in the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + $key = $this->key($session_id); + + $this->engine->delete($key . '_expire'); + + return $this->engine->delete($key); + } + + /** + * Get single session data as an object + * + * @param integer $session_id Session Id + * @return object + */ + public function session($session_id) + { + $session = new Object; + $session->session_id = $session_id; + $session->data = $this->read($session_id); + + return $session; + } + + /** + * Get list of all sessions + * + * @param array $filters + * @return array + */ + public function all($filters = array()) + { + // load all session keys + $data = $this->engine->getAllKeys(); + + $sessions = array(); + + // loop through all session keys and get data + foreach ($data as $key => $value) + { + if (strpos($value->name, $this->prefix) === 0) + { + $session = new Object; + $session->session_id = $value->name; + $session->data = $value; + + $sessions[] = $session; + } + } + + // return array of session objects + return $sessions; + } + + /** + * Build the storage key + * + * @param string $id The session identifier. + * @return string + */ + protected function key($id) + { + return $this->prefix . $id; + } + + /** + * Test to see if the SessionHandler is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return (extension_loaded('memcache') && class_exists('Memcache')); + } + + /** + * Set expire time on each call since memcached sets it on cache creation. + * + * @param string $key Cache key to expire. + * @return void + */ + protected function expiration($key) + { + $lifetime = ini_get("session.gc_maxlifetime"); + + $expire = $this->engine->get($key . '_expire'); + + // Set prune period + if ($expire + $lifetime < time()) + { + $this->engine->delete($key); + $this->engine->delete($key . '_expire'); + } + else + { + $this->engine->replace($key . '_expire', time()); + } + } +} diff --git a/core/libraries/Hubzero/Session/Storage/Memcached.php b/core/libraries/Hubzero/Session/Storage/Memcached.php new file mode 100644 index 00000000000..a982e4280dc --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/Memcached.php @@ -0,0 +1,286 @@ +prefix = $options['prefix']; + } + + parent::__construct($options); + + if (isset($options['compress']) && $options['compress']) + { + $this->compress = \Memcached::OPT_COMPRESSION; + } + + if (!isset($options['servers']) || empty($options['servers'])) + { + $conf = new \Hubzero\Config\Repository('site'); + + $options['servers'] = array( + array( + 'host' => $config->get('memcache_server_host', 'localhost'), + 'port' => $config->get('memcache_server_port', 11211), + 'weight' => $config->get('memcache_persist', true) + ) + ); + } + + $this->servers = $options['servers']; + } + + /** + * Open the SessionHandler backend. + * + * @param string $save_path The path to the session object. + * @param string $name The name of the session. + * @return boolean True on success, false otherwise. + */ + public function open($save_path, $name) + { + $this->engine = new \Memcached; + + // For each server in the array, we'll just extract the configuration and add + // the server to the Memcached connection. Once we have added all of these + // servers we'll verify the connection is successful and return it back. + foreach ($this->servers as $server) + { + $this->engine->addServer( + $server['host'], $server['port'], $server['weight'] + ); + } + + return true; + } + + /** + * Close the SessionHandler backend. + * + * @return boolean True on success, false otherwise. + */ + public function close() + { + // $this->engine->close(); + return true; + } + + /** + * Read the data for a particular session identifier from the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return string The session data. + */ + public function read($session_id) + { + $key = $this->key($session_id); + + $this->expiration($key); + + return $this->engine->get($key); + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + $key = $this->key($session_id); + + if ($this->engine->get($key . '_expire')) + { + $this->engine->replace($key . '_expire', time()); + } + else + { + $this->engine->set($key . '_expire', time()); + } + if ($this->engine->get($key)) + { + $this->engine->replace($key, $session_data); + } + else + { + $this->engine->set($key, $session_data); + } + + return; + } + + /** + * Destroy the data for a particular session identifier in the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + $key = $this->key($session_id); + + $this->engine->delete($key . '_expire'); + + return $this->engine->delete($key); + } + + /** + * Garbage collect stale sessions from the SessionHandler backend. + * + * -- Not Applicable in memcached -- + * + * @param integer $maxlifetime The maximum age of a session. + * @return boolean True on success, false otherwise. + */ + public function gc($maxlifetime = null) + { + return true; + } + + /** + * Get single session data as an object + * + * @param integer $session_id Session Id + * @return object + */ + public function session($session_id) + { + $session = new Object; + $session->session_id = $session_id; + $session->data = $this->read($session_id); + + return $session; + } + + /** + * Get list of all sessions + * + * @param array $filters + * @return array + */ + public function all($filters = array()) + { + // load all session keys + $data = $this->engine->getAllKeys(); + + $sessions = array(); + + // loop through all session keys and get data + foreach ($data as $key => $value) + { + if (strpos($value->name, $this->prefix) === 0) + { + $session = new Object; + $session->session_id = $value->name; + $session->data = $value; + + $sessions[] = $session; + } + } + + // return array of session objects + return $sessions; + } + + /** + * Build the storage key + * + * @param string $id The session identifier. + * @return string + */ + protected function key($id) + { + return $this->prefix . $id; + } + + /** + * Test to see if the SessionHandler is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return (extension_loaded('memcached') && class_exists('Memcached')); + } + + /** + * Set expire time on each call since memcached sets it on cache creation. + * + * @param string $key Cache key to expire. + * @return void + */ + protected function expiration($key) + { + $lifetime = ini_get("session.gc_maxlifetime"); + + $expire = $this->engine->get($key . '_expire'); + + // Set prune period + if ($expire + $lifetime < time()) + { + $this->engine->delete($key); + $this->engine->delete($key . '_expire'); + } + else + { + $this->engine->replace($key . '_expire', time()); + } + } +} diff --git a/core/libraries/Hubzero/Session/Storage/None.php b/core/libraries/Hubzero/Session/Storage/None.php new file mode 100644 index 00000000000..b58e26d5372 --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/None.php @@ -0,0 +1,27 @@ +prefix = $prefixes['session']; + } + + parent::__construct($options); + } + + /** + * Open the SessionHandler backend. + * + * @param string $save_path The path to the session object. + * @param string $name The name of the session. + * @return boolean True on success, false otherwise. + */ + public function open($save_path, $name) + { + $this->database = RedisDatabase::connect('default'); + $this->database->connect(); + } + + /** + * Close the SessionHandler backend. + * + * @return boolean True on success, false otherwise. + */ + public function close() + { + $this->database->disconnect(); + } + + /** + * Read session hash for Id + * + * @param string $session_id Session Id + * @return mixed Session Data + */ + public function read($session_id) + { + // get session hash + $session = $this->database->hgetall($this->key($session_id)); + + // return session data + return (isset($session['data'])) ? $session['data'] : null; + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $id The session identifier. + * @param string $data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + $data = array( + 'session_id' => $session_id, + 'client_id' => \App::get('client')->id, + 'guest' => \User::isGuest(), + 'time' => time(), + 'data' => $session_data, + 'userid' => \User::get('id'), + 'username' => \User::get('username'), + 'usertype' => null, + 'ip' => $_SERVER['REMOTE_ADDR'] + ); + + $saved = $this->database->hmset($this->key($session_id), $data); + + return $saved; + } + + /** + * Delete session hash + * + * @param string $session_id Session Id + * @return boolean Destroyed or not + */ + public function destroy($session_id) + { + if (!$this->database->del($this->key($session_id))) + { + return false; + } + return true; + } + + /** + * Garbage collect stale sessions from the SessionHandler backend. + * + * @param integer $maxlifetime The maximum age of a session. + * @return boolean True on success, false otherwise. + */ + public function gc($maxlifetime = null) + { + //'redis gc'; + } + + /** + * Get single session data as an object + * + * @param integer $session_id Session Id + * @return object + */ + public function session($session_id) + { + return (object) $this->database->hgetall($session_id); + } + + /** + * Get list of all sessions + * + * @param array $filters + * @return array + */ + public function all($filters = array()) + { + // load all session keys + $result = $this->database->scan(0, array('MATCH' => $this->prefix . '*')); + $cursor = $result[0]; + $sessions = $result[1]; + + // var to hold distinct sessions + $distinct = array(); + + // loop through all session keys and get data + foreach ($sessions as $k => $v) + { + // get session data for key + $sessions[$k] = $this->database->hgetall($v); + $userid = $sessions[$k]->userid; + $guest = $sessions[$k]->guest; + $client = $sessions[$k]->client_id; + + // guest filter + if (isset($filters['guest']) && $filters['guest'] != $guest) + { + unset($sessions[$k]); + continue; + } + + // client filter + if (isset($filters['client'])) + { + // make sure is array + if (!is_array($filters['client'])) + { + $filters['client'] = array($filters['client']); + } + // check to make sure client is in what we want + if (!in_array($client, $filters['client'])) + { + unset($sessions[$k]); + continue; + } + } + + // distinct filter + if (isset($filters['distinct']) && $filters['distinct'] == 1) + { + if (isset($distinct[$client]) && in_array($userid, array_keys($distinct[$client]))) + { + $key = $distinct[$client][$userid]->key; + $beforeTime = $distinct[$client][$userid]->time; + $currentTime = $sessions[$k]->time; + + // is this sessions time greater then the + // previous one saved for this user for this client? + if ($currentTime < $beforeTime) + { + $key = $k; + } + + unset($sessions[$key]); + continue; + } + else + { + $sessions[$k]->key = $k; + $distinct[$client][$userid] = $sessions[$k]; + } + } + } + + // return array of session objects + return array_values(array_filter($sessions)); + } + + /** + * Build the storage key + * + * @param string $id The session identifier. + * @return string + */ + protected function key($id) + { + return $this->prefix . $id; + } + + /** + * Test to see if Predis Library exists + * + * @return boolean + */ + public static function isAvailable() + { + return new RedisDatabase != null; + } +} diff --git a/core/libraries/Hubzero/Session/Storage/WinCache.php b/core/libraries/Hubzero/Session/Storage/WinCache.php new file mode 100644 index 00000000000..af5d1166cd5 --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/WinCache.php @@ -0,0 +1,102 @@ +prefix = $options['prefix']; + } + + parent::__construct($options); + } + + /** + * Read the data for a particular session identifier from the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return string The session data. + */ + public function read($session_id) + { + return (string) wincache_ucache_get($this->key($session_id)); + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + return wincache_ucache_set($this->key($session_id), $session_data, ini_get("session.gc_maxlifetime")); + } + + /** + * Destroy the data for a particular session identifier in the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + return wincache_ucache_delete($this->key($session_id)); + } + + /** + * Build the storage key + * + * @param string $id The session identifier. + * @return string + */ + protected function key($id) + { + return $this->prefix . $id; + } + + /** + * Test to see if the SessionHandler is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return (extension_loaded('wincache') && function_exists('wincache_ucache_get') && !strcmp(ini_get('wincache.ucenabled'), "1")); + } +} diff --git a/core/libraries/Hubzero/Session/Storage/Xcache.php b/core/libraries/Hubzero/Session/Storage/Xcache.php new file mode 100644 index 00000000000..5a3eda4f16e --- /dev/null +++ b/core/libraries/Hubzero/Session/Storage/Xcache.php @@ -0,0 +1,113 @@ +prefix = $options['prefix']; + } + + parent::__construct($options); + } + + /** + * Read the data for a particular session identifier from the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return string The session data. + */ + public function read($session_id) + { + // Check if id exists + if (!xcache_isset($this->key($session_id))) + { + return; + } + + return (string) xcache_get($this->key($session_id)); + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + return xcache_set($this->key($session_id), $session_data, ini_get("session.gc_maxlifetime")); + } + + /** + * Destroy the data for a particular session identifier in the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + if (!xcache_isset($this->key($session_id))) + { + return true; + } + + return xcache_unset($this->key($session_id)); + } + + /** + * Build the storage key + * + * @param string $id The session identifier. + * @return string + */ + protected function key($id) + { + return $this->prefix . $id; + } + + /** + * Test to see if the SessionHandler is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return (extension_loaded('xcache')); + } +} diff --git a/core/libraries/Hubzero/Session/Store.php b/core/libraries/Hubzero/Session/Store.php new file mode 100644 index 00000000000..ab902a84227 --- /dev/null +++ b/core/libraries/Hubzero/Session/Store.php @@ -0,0 +1,187 @@ +register($options); + } + + /** + * Returns a session storage handler object, only creating it if it doesn't already exist. + * + * @param string $name The session store to instantiate + * @param array $options Array of options + * @return object + */ + public static function getInstance($name = 'none', $options = array()) + { + $name = strtolower(preg_replace('/[^A-Z_]/i', '', (string) $name)); + + if (empty(self::$instances[$name])) + { + $class = __NAMESPACE__ . '\\Storage\\' . ucfirst($name); + + if (!class_exists($class)) + { + // No attempt to die gracefully here, as it tries to close the non-existing session + exit('Unable to load session storage class: ' . $name); + } + + self::$instances[$name] = new $class($options); + } + + return self::$instances[$name]; + } + + /** + * Register the functions of this class with PHP's session handler + * + * @param array $options + * @return void + */ + public function register($options = array()) + { + // Use this object as the session handler + session_set_save_handler( + array($this, 'open'), + array($this, 'close'), + array($this, 'read'), + array($this, 'write'), + array($this, 'destroy'), + array($this, 'gc') + ); + } + + /** + * Open the SessionHandler backend. + * + * @param string $save_path The path to the session object. + * @param string $name The name of the session. + * @return boolean True on success, false otherwise. + */ + public function open($save_path, $name) + { + return true; + } + + /** + * Close the SessionHandler backend. + * + * @return boolean True on success, false otherwise. + */ + public function close() + { + return true; + } + + /** + * Read the data for a particular session identifier from the + * SessionHandler backend. + * + * @param string $id The session identifier. + * @return string The session data. + */ + public function read($session_id) + { + return; + } + + /** + * Write session data to the SessionHandler backend. + * + * @param string $session_id The session identifier. + * @param string $session_data The session data. + * @return boolean True on success, false otherwise. + */ + public function write($session_id, $session_data) + { + return true; + } + + /** + * Destroy the data for a particular session identifier in the + * SessionHandler backend. + * + * @param string $id The session identifier. + * @return boolean True on success, false otherwise. + */ + public function destroy($session_id) + { + return true; + } + + /** + * Garbage collect stale sessions from the SessionHandler backend. + * + * @param integer $maxlifetime The maximum age of a session. + * @return boolean True on success, false otherwise. + */ + public function gc($maxlifetime = null) + { + return true; + } + + /** + * Get single session data as an object + * + * @param integer $session_id Session Id + * @return object + */ + public function session($session_id) + { + $session = new Object; + $session->id = $session_id; + + return $session; + } + + /** + * Get list of all sessions + * + * @param array $filters + * @return array + */ + public function all($filters = array()) + { + return array(); + } + + /** + * Test to see if the SessionHandler is available. + * + * @return boolean True on success, false otherwise. + */ + public static function isAvailable() + { + return true; + } +} diff --git a/core/libraries/Hubzero/Spam/Checker.php b/core/libraries/Hubzero/Spam/Checker.php new file mode 100644 index 00000000000..ab80a6acacd --- /dev/null +++ b/core/libraries/Hubzero/Spam/Checker.php @@ -0,0 +1,251 @@ +setStringProcessor($stringProcessor); + } + } + + /** + * Set string processor + * + * @param object $stringProcessor + * @return void + */ + public function setStringProcessor(StringProcessorInterface $stringProcessor) + { + $this->stringProcessor = $stringProcessor; + } + + /** + * Get string processor + * + * @return object + */ + public function getStringProcessor() + { + return $this->stringProcessor; + } + + /** + * Checks if a string is spam or not + * + * @param string|array $data + * @return object + */ + public function check($data) + { + $failure = 0; + $messages = array(); + + if (is_string($data)) + { + $data = array('text' => $data); + } + + $data = $this->prepareData($data); + + foreach ($this->detectors as $id => $detector) + { + $spam = false; + $msg = null; + + try + { + if ($detector->detect($data)) + { + $spam = true; + + if ($detector->message()) + { + $messages[] = $detector->message(); + } + + $failure++; + } + + $msg = $detector->message(); + } + catch (Exception $e) + { + $this->setError($e->getMessage()); + } + + $this->mark($id, $spam, $msg); + } + + $result = new Result($failure > 0, $messages); + + return $result; + } + + /** + * Registers a Spam Detector + * + * @param object $spamDetector SpamDetectorInterface + * @return object SpamDetector + * @throws RuntimeException + */ + public function registerDetector(DetectorInterface $spamDetector) + { + $detectorId = $this->classSimpleName($spamDetector); + + if (isset($this->detectors[$detectorId])) + { + throw new RuntimeException( + sprintf('Spam Detector [%s] already registered', $detectorId) + ); + } + + $this->detectors[$detectorId] = $spamDetector; + + return $this; + } + + /** + * Gets a detector using its detector ID (Class Simple Name) + * + * @param string $detectorId + * @return mixed False or SpamDetectorInterface + */ + public function getDetector($detectorId) + { + if (!isset($this->detectors[$detectorId])) + { + return false; + } + + return $this->detectors[$detectorId]; + } + + /** + * Gets a list of all spam detectors + * + * @return array + */ + public function getDetectors() + { + return $this->detectors; + } + + /** + * Get IP address + * + * @return string + */ + public function getReport() + { + return $this->report; + } + + /** + * Used to normalize string before passing + * it to detectors + * + * @param array $data + * @return string + */ + protected function prepareData(array $data) + { + $data = array_merge(array( + 'name' => null, + 'email' => null, + 'username' => null, + 'id' => null, + 'text' => null, + 'ip' => null, + 'user_agent' => null + ), $data); + + $data['original_text'] = $data['text']; + + if ($this->stringProcessor) + { + $data['text'] = $this->stringProcessor->prepare($data['text']); + } + + return $data; + } + + /** + * Gets the name of a class (w. Namespaces removed) + * + * @param mixed $class String (class name) or object + * @return string + */ + protected function classSimpleName($class) + { + if (is_object($class)) + { + $class = get_class($class); + } + + return $class; + } + + /** + * Report the results of a spam detector + * + * @param string $name Name of the detector + * @param boolean $value If spam or not + * @param string $message Message set by detector + * @return string + */ + protected function mark($name, $value, $message = null) + { + $this->report[] = array( + 'service' => $name, + 'is_spam' => $value, + 'message' => $message + ); + } +} diff --git a/core/libraries/Hubzero/Spam/Detector/DetectorInterface.php b/core/libraries/Hubzero/Spam/Detector/DetectorInterface.php new file mode 100644 index 00000000000..9b7b294518a --- /dev/null +++ b/core/libraries/Hubzero/Spam/Detector/DetectorInterface.php @@ -0,0 +1,29 @@ +_value; + } + + /** + * Sets the value to be validated and clears the errors arrays + * + * @param mixed $value + * @return void + */ + public function setValue($value) + { + $this->_value = $value; + $this->_errors = array(); + $this->message = ''; + } + + /** + * Run content through spam detection + * + * @param array $data + * @return bool + */ + public function detect($data) + { + return false; + } + + /** + * Train the service + * + * @param string $data + * @param boolean $isSpam + * @return boolean + */ + public function learn($data, $isSpam) + { + if (!$data) + { + return false; + } + + return true; + } + + /** + * Forget a trained value + * + * @param string $data + * @param boolean $isSpam + * @return boolean + */ + public function forget($data, $isSpam) + { + return true; + } + + /** + * Return any message the service may have + * + * @return string + */ + public function message() + { + return $this->message; + } +} diff --git a/core/libraries/Hubzero/Spam/Honeypot.php b/core/libraries/Hubzero/Spam/Honeypot.php new file mode 100644 index 00000000000..3f57d0d54aa --- /dev/null +++ b/core/libraries/Hubzero/Spam/Honeypot.php @@ -0,0 +1,111 @@ +' . "\n" . + 'Leave this field empty:' . "\n" . + Input::input('text', $name . '[p]') . "\n" . + Input::input('text', $name . '[t]', self::getEncrypter()->encrypt(time())) . "\n" . + '' . "\n"; + } + + /** + * Validate honeypot + * + * @param mixed $value + * @param mixed $tme + * @param integer $delay + * @return boolean + */ + public static function isValid($value, $tme, $delay = 3) + { + return (self::validatePot($value) && self::validateTime($tme, $delay)); + } + + /** + * Validate pot is empty + * + * @param mixed $value + * @return boolean + */ + public static function validatePot($value) + { + return $value == ''; + } + + /** + * Validate time was within the time limit + * + * @param mixed $value + * @param integer $delay + * @return boolean + */ + public static function validateTime($value, $delay) + { + // Get the decrypted time + $value = self::getEncrypter()->decrypt($value); + + // The current time should be greater than the time the form was built + the speed option + return (is_numeric($value) && time() > ($value + $delay)); + } + + /** + * Get a unique form name + * + * @return string + */ + public static function getName() + { + return 'hypt' . substr(\App::get('session')->getFormToken(), 0, 7); + } + + /** + * Get the encrypter + * + * @return object + */ + protected static function getEncrypter() + { + static $crypt; + + if (!$crypt) + { + $key = \App::get('session')->getFormToken(); + + $crypt = new Encrypter( + new Simple, + new Key('simple', $key, $key) + ); + } + + return $crypt; + } +} diff --git a/core/libraries/Hubzero/Spam/Result.php b/core/libraries/Hubzero/Spam/Result.php new file mode 100644 index 00000000000..5c45dc89b60 --- /dev/null +++ b/core/libraries/Hubzero/Spam/Result.php @@ -0,0 +1,79 @@ + + */ +class Result +{ + /** + * @var bool + */ + protected $isSpam = false; + + /** + * @var array + */ + protected $messages = array(); + + /** + * Constructor + * + * @param bool $isSpam Result from spam detectors + * @param array $messages Messages to pass along + * @return void + */ + public function __construct($isSpam, array $messages = array()) + { + $this->isSpam = $isSpam; + $this->messages = $messages; + } + + /** + * Alias of SpamResult::failed(); + * + * @return bool + */ + public function isSpam() + { + return $this->failed(); + } + + /** + * Did the content pass? + * + * @return bool + */ + public function passed() + { + return $this->isSpam == false; + } + + /** + * Did the content fail? + * + * @return bool + */ + public function failed() + { + return !$this->passed(); + } + + /** + * Get the list of messages + * + * @return array + */ + public function getMessages() + { + return $this->messages; + } +} diff --git a/core/libraries/Hubzero/Spam/StringProcessor/NativeStringProcessor.php b/core/libraries/Hubzero/Spam/StringProcessor/NativeStringProcessor.php new file mode 100644 index 00000000000..d1b918b6de9 --- /dev/null +++ b/core/libraries/Hubzero/Spam/StringProcessor/NativeStringProcessor.php @@ -0,0 +1,81 @@ + + */ +class NativeStringProcessor implements StringProcessorInterface +{ + /** + * Perform ASCII conversion? + * + * @var bool + */ + protected $asciiConversion = true; + + /** + * Aggressive processing? + * + * @var bool + */ + protected $aggressive = false; + + /** + * Constructor + * + * @param array $options + * @return void + */ + public function __construct(array $options = array()) + { + if (isset($options['ascii_conversion'])) + { + $this->asciiConversion = (bool) $options['ascii_conversion']; + } + + if (isset($options['aggressive'])) + { + $this->aggressive = (bool) $options['aggressive']; + } + } + + /** + * Prepare a string + * + * @param string $string + * @return string + */ + public function prepare($string) + { + if ($this->asciiConversion) + { + setlocale(LC_ALL, 'en_us.UTF8'); + $string = iconv('UTF-8', 'ASCII//TRANSLIT', $string); + } + + if ($this->aggressive) + { + // Convert some characters that 'MAY' be used as alias + $string = str_replace(array('@', '$', '[dot]', '(dot)'), array('at', 's', '.', '.'), $string); + + // Remove special characters + $string = preg_replace("/[^a-zA-Z0-9-\.]/", '', $string); + + // Strip multiple dots (.) to one. eg site......com to site.com + $string = preg_replace("/\.{2,}/", '.', $string); + } + + $string = trim(strtolower($string)); + $string = str_replace(array("\t", "\r\n", "\r", "\n"), '', $string); + + return $string; + } +} diff --git a/core/libraries/Hubzero/Spam/StringProcessor/NoneStringProcessor.php b/core/libraries/Hubzero/Spam/StringProcessor/NoneStringProcessor.php new file mode 100644 index 00000000000..a7ee3ee2f01 --- /dev/null +++ b/core/libraries/Hubzero/Spam/StringProcessor/NoneStringProcessor.php @@ -0,0 +1,25 @@ + + */ +interface StringProcessorInterface +{ + /** + * Prepare a string + * + * @param string $string + * @return string + */ + public function prepare($string); +} diff --git a/core/libraries/Hubzero/Spam/Tests/CheckerTest.php b/core/libraries/Hubzero/Spam/Tests/CheckerTest.php new file mode 100644 index 00000000000..d1fd4988263 --- /dev/null +++ b/core/libraries/Hubzero/Spam/Tests/CheckerTest.php @@ -0,0 +1,161 @@ +assertInstanceOf('Hubzero\Spam\StringProcessor\NoneStringProcessor', $service->getStringProcessor()); + + $service->setStringProcessor(new NativeStringProcessor()); + + $this->assertInstanceOf('Hubzero\Spam\StringProcessor\NativeStringProcessor', $service->getStringProcessor()); + } + + /** + * Test to make sure a detector is registered properly + * and returns $this. + * + * @covers \Hubzero\Spam\Checker::registerDetector + * @return void + **/ + public function testRegisterDetector() + { + $service = new Checker(); + + $this->assertInstanceOf('Hubzero\Spam\Checker', $service->registerDetector(new Detector())); + + $this->setExpectedException('RuntimeException'); + + $service->registerDetector(new Detector()); + } + + /** + * Test to get a registered detector + * + * @covers \Hubzero\Spam\Checker::getDetector + * @covers \Hubzero\Spam\Checker::classSimpleName + * @return void + **/ + public function testGetDetector() + { + $service = new Checker(); + $service->registerDetector(new Detector()); + + $this->assertInstanceOf('Hubzero\Spam\Tests\Mock\Detector', $service->getDetector('Hubzero\Spam\Tests\Mock\Detector')); + $this->assertFalse($service->getDetector('Hubzero\Spam\Tests\Mock\Example')); + } + + /** + * Test that getDetectors returns an array of detectors + * + * @covers \Hubzero\Spam\Checker::getDetectors + * @return void + **/ + public function testGetDetectors() + { + $d = new Detector(); + $k = get_class($d); + + $data = []; + $data[$k] = $d; + + $service = new Checker(); + $service->registerDetector($data[$k]); + + $detectors = $service->getDetectors(); + + $this->assertTrue(is_array($detectors), 'Getting all detectors should return an array'); + $this->assertCount(1, $detectors, 'Get detectors should have returned one detector'); + $this->assertEquals($detectors, $data); + } + + /** + * Test that getReport() returns an array + * + * @covers \Hubzero\Spam\Checker::getReport + * @return void + **/ + public function testGetReport() + { + $service = new Checker(); + $service->registerDetector(new Detector()); + + $report = $service->getReport(); + + $this->assertTrue(is_array($report)); + } + + /** + * Test the check() method + * + * @covers \Hubzero\Spam\Checker::check + * @covers \Hubzero\Spam\Checker::prepareData + * @covers \Hubzero\Spam\Checker::mark + * @return void + **/ + public function testCheck() + { + $service = new Checker(); + $service->registerDetector(new Detector()); + + // This should NOT be caught as spam + $result = $service->check('Maecenas sed diam eget risus varius blandit sit amet non magna.'); + + $this->assertInstanceOf('Hubzero\Spam\Result', $result); + $this->assertFalse($result->isSpam()); + + // This should be caught as spam + $result = $service->check('Maecenas sed diam eget risus varius spam blandit sit amet non magna.'); + + $this->assertInstanceOf('Hubzero\Spam\Result', $result); + $this->assertTrue($result->isSpam()); + + $messages = $result->getMessages(); + $this->assertTrue(is_array($messages)); + $this->assertTrue(in_array('Text contained the word "spam".', $messages)); + + // Make sure string processors do their job + $service->setStringProcessor(new NativeStringProcessor()); + + $result = $service->check("Maecenas sed diam eget risus varius sp\nam blandit sit amet non magna."); + + $this->assertInstanceOf('Hubzero\Spam\Result', $result); + $this->assertTrue($result->isSpam()); + + // Make sure exceptions are caught and passed as error messages + $service->registerDetector(new DetectorException()); + + $result = $service->check('Maecenas sed diam eget risus varius spam blandit sit amet non magna.'); + + $error = $service->getError(); + + $this->assertEquals($error, 'I always throw an exception.'); + } +} diff --git a/core/libraries/Hubzero/Spam/Tests/Mock/Detector.php b/core/libraries/Hubzero/Spam/Tests/Mock/Detector.php new file mode 100644 index 00000000000..e6e82f8ca5c --- /dev/null +++ b/core/libraries/Hubzero/Spam/Tests/Mock/Detector.php @@ -0,0 +1,41 @@ +message = 'Text contained the word "spam".'; + return true; + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Spam/Tests/Mock/DetectorException.php b/core/libraries/Hubzero/Spam/Tests/Mock/DetectorException.php new file mode 100644 index 00000000000..b6b5f5eecb8 --- /dev/null +++ b/core/libraries/Hubzero/Spam/Tests/Mock/DetectorException.php @@ -0,0 +1,37 @@ +assertTrue($result->isSpam()); + + $result = new Result(false); + + $this->assertFalse($result->isSpam()); + } + + /** + * Tests passed() returns correct value depending on if spam or not + * + * @covers \Hubzero\Spam\Result::__construct + * @covers \Hubzero\Spam\Result::passed + * @return void + */ + public function testPassed() + { + $result = new Result(false); + + $this->assertTrue($result->passed()); + + $result = new Result(true); + + $this->assertFalse($result->passed()); + } + + /** + * Tests failed() returns correct value depending on if spam or not + * + * @covers \Hubzero\Spam\Result::__construct + * @covers \Hubzero\Spam\Result::failed + * @return void + */ + public function testFailed() + { + $result = new Result(true); + + $this->assertTrue($result->failed()); + + $result = new Result(false); + + $this->assertFalse($result->failed()); + } + + /** + * Tests getMessages() returns the list of messages passed in the constructor + * + * @covers \Hubzero\Spam\Result::__construct + * @covers \Hubzero\Spam\Result::getMessages + * @return void + */ + public function testGetMessages() + { + $messages = [ + 'Message one', + 'Message two' + ]; + + $result = new Result(true, $messages); + + $this->assertEquals($messages, $result->getMessages()); + } +} diff --git a/core/libraries/Hubzero/Spam/Tests/ServiceTest.php b/core/libraries/Hubzero/Spam/Tests/ServiceTest.php new file mode 100644 index 00000000000..92c588fc3cc --- /dev/null +++ b/core/libraries/Hubzero/Spam/Tests/ServiceTest.php @@ -0,0 +1,99 @@ +getMockForAbstractClass('Hubzero\Spam\Detector\Service'); + } + + /** + * Tests for setting and getting a value + * + * @covers \Hubzero\Spam\Detector\Service::setValue + * @covers \Hubzero\Spam\Detector\Service::getValue + * @return void + **/ + public function testValue() + { + $stub = $this->getStub(); + + $stub->setValue('foo'); + + $this->assertEquals($stub->getValue(), 'foo'); + } + + /** + * Tests detect() returns false + * + * @covers \Hubzero\Spam\Detector\Service::detect + * @return void + **/ + public function testDetect() + { + $stub = $this->getStub(); + + $this->assertFalse($stub->detect('foo')); + } + + /** + * Tests learn() + * + * @covers \Hubzero\Spam\Detector\Service::learn + * @return void + **/ + public function testLearn() + { + $stub = $this->getStub(); + + $isSpam = true; + + $this->assertFalse($stub->learn('', $isSpam)); + $this->assertTrue($stub->learn('foo', $isSpam)); + } + + /** + * Tests forget() + * + * @covers \Hubzero\Spam\Detector\Service::forget + * @return void + **/ + public function testForget() + { + $stub = $this->getStub(); + + $isSpam = true; + + $this->assertTrue($stub->forget('foo', $isSpam)); + } + + /** + * Tests message() returns an empty string + * + * @covers \Hubzero\Spam\Detector\Service::message + * @return void + **/ + public function testMessage() + { + $stub = $this->getStub(); + + $this->assertEquals($stub->message(), ''); + } +} diff --git a/core/libraries/Hubzero/Spam/Tests/StringProcessorTest.php b/core/libraries/Hubzero/Spam/Tests/StringProcessorTest.php new file mode 100644 index 00000000000..033ab084c1f --- /dev/null +++ b/core/libraries/Hubzero/Spam/Tests/StringProcessorTest.php @@ -0,0 +1,69 @@ +prepare($text); + + $this->assertEquals($result, $text); + } + + /** + * Tests for setting and getting a StringProcessor + * + * @covers \Hubzero\Spam\StringProcessor\NativeStringProcessor::__construct + * @covers \Hubzero\Spam\StringProcessor\NativeStringProcessor::prepare + * @return void + **/ + public function testNativeStringProcessor() + { + // Test default preparation + $text = " Curabitur foo @ blandit up......er tempus porttitor[dot]\nLorem ipsum dolor sit \tamet, consectetur & adipiscing elit."; + + $processor = new NativeStringProcessor(); + $result = $processor->prepare($text); + $expected = "curabitur foo @ blandit up......er tempus porttitor[dot]lorem ipsum dolor sit amet, consectetur & adipiscing elit."; + + $this->assertEquals($result, $expected); + + // Test aggressive flag + $processor = new NativeStringProcessor(array('aggressive' => true)); + $result = $processor->prepare($text); + $expected = "curabiturfooatblanditup.ertempusporttitor.loremipsumdolorsitametconsecteturadipiscingelit."; + + $this->assertEquals($result, $expected); + + // Test ASCII conversion flag + $text = " Curabitur foo @ blandit ùp......er tempus porttitor[dot]\nLorem ipsum dölor sit \tamet, cönsectetur & adipiscing élit."; + + $processor = new NativeStringProcessor(array('ascii_conversion' => true)); + $result = $processor->prepare($text); + $expected = "curabitur foo @ blandit up......er tempus porttitor[dot]lorem ipsum dolor sit amet, consectetur & adipiscing elit."; + + $this->assertEquals($result, $expected); + } +} diff --git a/core/libraries/Hubzero/Template/Loader.php b/core/libraries/Hubzero/Template/Loader.php new file mode 100644 index 00000000000..92744aa11ac --- /dev/null +++ b/core/libraries/Hubzero/Template/Loader.php @@ -0,0 +1,297 @@ + null, + 'core' => null + ); + + /** + * Specified style + * + * @var integer + */ + protected $style = 0; + + /** + * Language tag + * + * @var string + */ + protected $lang = ''; + + /** + * Constructor + * + * @param object $app + * @param array $options + * @return void + */ + public function __construct(Container $app, $options = array()) + { + $this->app = $app; + + if (array_key_exists('style', $options)) + { + $this->setStyle($options['style']); + } + + if (array_key_exists('lang', $options)) + { + $this->setLang($options['lang']); + } + + if (array_key_exists('path_app', $options)) + { + $this->setPath('app', $options['path_app']); + } + + if (array_key_exists('path_core', $options)) + { + $this->setPath('core', $options['path_core']); + } + } + + /** + * Set path for a key + * + * @param string $key + * @param string $path + * @return object + */ + public function setPath($key, $path) + { + $this->paths[(string) $key] = (string) $path; + + return $this; + } + + /** + * Get path for key name + * + * @param string $key + * @return string + */ + public function getPath($key) + { + return (isset($this->paths[$key]) ? $this->paths[$key] : ''); + } + + /** + * Set style + * + * @param integer $style + * @return object + */ + public function setStyle($style) + { + $this->style = (int) $style; + + return $this; + } + + /** + * Get style + * + * @return integer + */ + public function getStyle() + { + return $this->style; + } + + /** + * Set language + * + * @param string $lang + * @return object + */ + public function setLang($lang) + { + $this->lang = (string) $lang; + + return $this; + } + + /** + * Get language + * + * @return string + */ + public function getLang() + { + return $this->lang; + } + + /** + * Load a template by client + * + * @param integer $client_id The client to load the tmeplate for + * @return string + */ + public function load($client_id = null) + { + if (!is_null($client_id)) + { + $client = ClientManager::client($client_id, (! is_numeric($client_id))); + } + else + { + $client = $this->app['client']; + } + + if (!$client) + { + throw new \InvalidArgumentException(sprintf('Invalid client type of "%s".', $client_id)); + } + + return $this->getTemplate((int)$client->id, $this->style); + } + + /** + * Get the system template + * + * @return object + */ + public function getSystemTemplate() + { + static $template; + + if (!isset($template)) + { + $template = new stdClass; + $template->id = 0; + $template->home = 0; + $template->template = 'system'; + $template->params = new Registry(); + $template->protected = 1; + $template->path = $this->getPath('core') . DIRECTORY_SEPARATOR . $template->template; + } + + return $template; + } + + /** + * Get a list of templates for the specified client + * + * @param integer $client_id + * @param integer $id + * @return object + */ + public function getTemplate($client_id = 0, $id = 0) + { + if (!$this->app->has('cache.store') || !($cache = $this->app['cache.store'])) + { + $cache = new \Hubzero\Cache\Storage\None(array('hash' => $this->app->hash('template.loader'))); + } + + $templates = $cache->get('com_templates.templates' . $client_id . $this->lang); + + if (!$templates || empty($templates)) + { + try + { + $db = $this->app['db']; + + $s = '#__template_styles'; + $e = '#__extensions'; + + $query = new Query($db); + $query + ->select($s . '.id') + ->select($s . '.home') + ->select($s . '.template') + ->select($s . '.params') + ->select($e . '.protected') + ->from($s) + ->join($e, $e . '.element', $s . '.template') + ->whereEquals($s . '.client_id', (int)$client_id) + ->whereEquals($e . '.enabled', 1) + ->whereEquals($e . '.type', 'template') + ->whereRaw($e . '.`client_id` = `' . $s . '`.`client_id`'); + + $query->order('home', 'desc'); + + $db->setQuery($query->toString()); + $templates = $db->loadObjectList('id'); + + foreach ($templates as $i => $template) + { + $template->params = new Registry($template->params); + + if (substr($template->template, 0, 4) == 'tpl_') + { + $template->template = substr($template->template, 4); + } + + if (is_dir($this->getPath('app') . DIRECTORY_SEPARATOR . $template->template)) + { + $template->path = $this->getPath('app') . DIRECTORY_SEPARATOR . $template->template; + } + else + { + $template->path = $this->getPath('core') . DIRECTORY_SEPARATOR . $template->template; + } + + $templates[$i] = $template; + + // Create home element + if ($template->home && !isset($templates[0])) + { + $templates[0] = clone $template; + } + } + + $cache->put('com_templates.templates' . $client_id . $this->lang, $templates, $this->app['config']->get('cachetime', 15)); + } + catch (Exception $e) + { + $templates = array(); + } + } + + $tmpl = null; + + if (isset($templates[$id])) + { + $tmpl = $templates[$id]; + } + + if ($tmpl && file_exists($tmpl->path . DIRECTORY_SEPARATOR . 'index.php')) + { + return $tmpl; + } + + return $this->getSystemTemplate(); + } +} diff --git a/core/libraries/Hubzero/Template/Tests/Fixtures/seed.xml b/core/libraries/Hubzero/Template/Tests/Fixtures/seed.xml new file mode 100644 index 00000000000..9aaa072d143 --- /dev/null +++ b/core/libraries/Hubzero/Template/Tests/Fixtures/seed.xml @@ -0,0 +1,93 @@ + + + + extension_id + name + type + element + client_id + enabled + access + protected + + 1 + Admin Foo + template + adminfoo + 1 + 1 + 1 + 1 + + + 2 + Admin Bar + template + adminbar + 1 + 1 + 1 + 0 + + + 3 + Site Foo + template + sitefoo + 0 + 1 + 1 + 1 + + + 4 + Site Bar + template + sitebar + 0 + 1 + 1 + 0 + +
    + + id + template + client_id + home + title + params + + 1 + adminfoo + 1 + 1 + Admin Foo + {"one":"lorem","two":"ipsum"} + + + 2 + adminbar + 1 + 0 + Admin Bar + {"one":"nullum","two":"crimia"} + + + 3 + sitefoo + 0 + 1 + Site Foo + {"one":"vestibu","two":"ligula"} + + + 4 + sitebar + 0 + 0 + Site Bar + {"one":"fusce","two":"cursus"} + +
    +
    \ No newline at end of file diff --git a/core/libraries/Hubzero/Template/Tests/Fixtures/test.sqlite3 b/core/libraries/Hubzero/Template/Tests/Fixtures/test.sqlite3 new file mode 100644 index 00000000000..e16248bdeaa Binary files /dev/null and b/core/libraries/Hubzero/Template/Tests/Fixtures/test.sqlite3 differ diff --git a/core/libraries/Hubzero/Template/Tests/Fixtures/testBad.sqlite3 b/core/libraries/Hubzero/Template/Tests/Fixtures/testBad.sqlite3 new file mode 100644 index 00000000000..409c3f41cd5 Binary files /dev/null and b/core/libraries/Hubzero/Template/Tests/Fixtures/testBad.sqlite3 differ diff --git a/core/libraries/Hubzero/Template/Tests/LoaderTest.php b/core/libraries/Hubzero/Template/Tests/LoaderTest.php new file mode 100644 index 00000000000..cc076e59add --- /dev/null +++ b/core/libraries/Hubzero/Template/Tests/LoaderTest.php @@ -0,0 +1,310 @@ +getMockDriver()); + + $app = new Application(); + $app['client'] = new \Hubzero\Base\Client\Site(); + $app['db'] = $this->getMockDriver(); + $app['config'] = \App::get('config'); + + $this->loader = new Loader($app, [ + 'path_app' => __DIR__ . '/Mock/app', + 'path_core' => __DIR__ . '/Mock/core' + ]); + } + + /** + * Test the getPath() method. + * + * @covers \Hubzero\Template\Loader::getPath + * @return void + */ + public function testGetPath() + { + $this->assertEquals($this->loader->getPath('core'), __DIR__ . '/Mock/core'); + $this->assertEquals($this->loader->getPath('app'), __DIR__ . '/Mock/app'); + + $this->assertNotEquals($this->loader->getPath('core'), $this->loader->getPath('app')); + } + + /** + * Test the setPath() method. + * + * @covers \Hubzero\Template\Loader::setPath + * @return void + */ + public function testSetPath() + { + $core = $this->loader->getPath('core'); + $app = $this->loader->getPath('app'); + + $this->assertInstanceOf('Hubzero\Template\Loader', $this->loader->setPath('core', __DIR__ . '/core')); + $this->assertEquals($this->loader->getPath('core'), __DIR__ . '/core'); + + $this->assertInstanceOf('Hubzero\Template\Loader', $this->loader->setPath('app', __DIR__ . '/app')); + $this->assertEquals($this->loader->getPath('app'), __DIR__ . '/app'); + + $this->assertNotEquals($this->loader->getPath('core'), $this->loader->getPath('app')); + + $this->loader->setPath('core', $core); + $this->loader->setPath('app', $app); + } + + /** + * Test that the system template is built and returned properly + * + * @covers \Hubzero\Template\Loader::getSystemTemplate + * @return void + */ + public function testGetSystemTemplate() + { + $template = $this->loader->getSystemTemplate(); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'system'); + $this->assertEquals($template->protected, 1); + $this->assertEquals($template->id, 0); + $this->assertEquals($template->home, 0); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('core') . DIRECTORY_SEPARATOR . $template->template); + } + + /** + * Test the setStyle() and getStyle() methods. + * + * @covers \Hubzero\Template\Loader::setStyle + * @covers \Hubzero\Template\Loader::getStyle + * @return void + */ + public function testSetGetStyle() + { + $this->loader->setStyle(1); + + $this->assertEquals($this->loader->getStyle(), 1); + + $this->loader->setStyle('0012'); + + $this->assertEquals($this->loader->getStyle(), 12); + } + + /** + * Test the setLang() and getLang() methods. + * + * @covers \Hubzero\Template\Loader::setLang + * @covers \Hubzero\Template\Loader::getLang + * @return void + */ + public function testSetGetLang() + { + $this->loader->setLang('de-DE'); + + $this->assertEquals($this->loader->getLang(), 'de-DE'); + + $this->loader->setLang('en-US'); + + $this->assertEquals($this->loader->getLang(), 'en-US'); + } + + /** + * Test the the constructor is properly setting all optional values + * + * @covers \Hubzero\Template\Loader::__construct + * @return void + */ + public function testConstructor() + { + \Hubzero\Database\Relational::setDefaultConnection($this->getMockDriver()); + + $app = new Application(); + $app['client'] = new \Hubzero\Base\Client\Site(); + $app['db'] = $this->getMockDriver(); + $app['config'] = \App::get('config'); + + $loader = new Loader($app, [ + 'path_app' => __DIR__ . '/Mock/app', + 'path_core' => __DIR__ . '/Mock/core', + 'style' => 5, + 'lang' => 'en-US' + ]); + + $this->assertEquals($loader->getPath('core'), __DIR__ . '/Mock/core'); + $this->assertEquals($loader->getPath('app'), __DIR__ . '/Mock/app'); + $this->assertEquals($loader->getStyle(), 5); + $this->assertEquals($loader->getLang(), 'en-US'); + } + + /** + * Test that the system template is built and returned properly + * + * @covers \Hubzero\Template\Loader::getTemplate + * @return void + */ + public function testGetTemplate() + { + $template = $this->loader->getTemplate(0); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'sitefoo'); + $this->assertEquals($template->protected, 1); + $this->assertEquals($template->id, 3); + $this->assertEquals($template->home, 1); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('core') . DIRECTORY_SEPARATOR . $template->template); + + $template = $this->loader->getTemplate(1); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'adminfoo'); + $this->assertEquals($template->protected, 1); + $this->assertEquals($template->id, 1); + $this->assertEquals($template->home, 1); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('core') . DIRECTORY_SEPARATOR . $template->template); + + $template = $this->loader->getTemplate(0, 4); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'sitebar'); + $this->assertEquals($template->protected, 0); + $this->assertEquals($template->id, 4); + $this->assertEquals($template->home, 0); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('app') . DIRECTORY_SEPARATOR . $template->template); + + $template = $this->loader->getTemplate(1, 2); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'adminbar'); + $this->assertEquals($template->protected, 0); + $this->assertEquals($template->id, 2); + $this->assertEquals($template->home, 0); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('app') . DIRECTORY_SEPARATOR . $template->template); + + $template = $this->loader->getTemplate(1, 7); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'system'); + $this->assertEquals($template->protected, 1); + $this->assertEquals($template->id, 0); + $this->assertEquals($template->home, 0); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('core') . DIRECTORY_SEPARATOR . $template->template); + + $template = $this->loader->getTemplate(0, 8); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'system'); + $this->assertEquals($template->protected, 1); + $this->assertEquals($template->id, 0); + $this->assertEquals($template->home, 0); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('core') . DIRECTORY_SEPARATOR . $template->template); + } + + /** + * Test loading a template by client ID + * + * @covers \Hubzero\Template\Loader::load + * @return void + */ + public function testLoad() + { + // Load tmeplate by current client (site) + $template = $this->loader->load(); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'sitefoo'); + $this->assertEquals($template->protected, 1); + $this->assertEquals($template->id, 3); + $this->assertEquals($template->home, 1); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('core') . DIRECTORY_SEPARATOR . $template->template); + + // Load site tmeplate by client ID + $template = $this->loader->load(0); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'sitefoo'); + $this->assertEquals($template->protected, 1); + $this->assertEquals($template->id, 3); + $this->assertEquals($template->home, 1); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('core') . DIRECTORY_SEPARATOR . $template->template); + + // Load admin template by client name + $template = $this->loader->load('administrator'); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'adminfoo'); + $this->assertEquals($template->protected, 1); + $this->assertEquals($template->id, 1); + $this->assertEquals($template->home, 1); + $this->assertInstanceOf('Hubzero\Config\Registry', $template->params); + $this->assertEquals($template->path, $this->loader->getPath('core') . DIRECTORY_SEPARATOR . $template->template); + + $this->setExpectedException('InvalidArgumentException'); + + $template = $this->loader->load('foobar'); + } + + /** + * Test that the system template is returned on a database error + * + * @covers \Hubzero\Template\Loader::load + * @return void + */ + public function testDatabaseError() + { + self::tearDownAfterClass(); + + $this->fixture = 'testBad.sqlite3'; + $this->connection = null; + + $app = new Application(); + $app['client'] = new \Hubzero\Base\Client\Site(); + $app['db'] = $this->getMockDriver(); + $app['config'] = \App::get('config'); + + $this->loader = new Loader($app, [ + 'path_app' => __DIR__ . '/Mock/app', + 'path_core' => __DIR__ . '/Mock/core' + ]); + + // Load admin template by client name + $template = $this->loader->load(); + + $this->assertTrue(is_object($template)); + $this->assertEquals($template->template, 'system'); + } +} diff --git a/core/libraries/Hubzero/Template/Tests/Mock/app/adminbar/index.php b/core/libraries/Hubzero/Template/Tests/Mock/app/adminbar/index.php new file mode 100644 index 00000000000..ae8de865f35 --- /dev/null +++ b/core/libraries/Hubzero/Template/Tests/Mock/app/adminbar/index.php @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/core/libraries/Hubzero/Template/Tests/Mock/app/sitebar/index.php b/core/libraries/Hubzero/Template/Tests/Mock/app/sitebar/index.php new file mode 100644 index 00000000000..ae8de865f35 --- /dev/null +++ b/core/libraries/Hubzero/Template/Tests/Mock/app/sitebar/index.php @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/core/libraries/Hubzero/Template/Tests/Mock/core/adminfoo/index.php b/core/libraries/Hubzero/Template/Tests/Mock/core/adminfoo/index.php new file mode 100644 index 00000000000..ae8de865f35 --- /dev/null +++ b/core/libraries/Hubzero/Template/Tests/Mock/core/adminfoo/index.php @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/core/libraries/Hubzero/Template/Tests/Mock/core/sitefoo/index.php b/core/libraries/Hubzero/Template/Tests/Mock/core/sitefoo/index.php new file mode 100644 index 00000000000..ae8de865f35 --- /dev/null +++ b/core/libraries/Hubzero/Template/Tests/Mock/core/sitefoo/index.php @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/core/libraries/Hubzero/Template/Tests/Mock/core/system/index.php b/core/libraries/Hubzero/Template/Tests/Mock/core/system/index.php new file mode 100644 index 00000000000..ae8de865f35 --- /dev/null +++ b/core/libraries/Hubzero/Template/Tests/Mock/core/system/index.php @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/core/libraries/Hubzero/Test/Basic.php b/core/libraries/Hubzero/Test/Basic.php new file mode 100644 index 00000000000..d60aeeffa98 --- /dev/null +++ b/core/libraries/Hubzero/Test/Basic.php @@ -0,0 +1,30 @@ +test = $expected; + + // exercise + $actual = $systemUnderTest->test; + + // verification + $this->assertEquals($expected, $actual); + } + +} diff --git a/core/libraries/Hubzero/Test/Database.php b/core/libraries/Hubzero/Test/Database.php new file mode 100644 index 00000000000..be8feb558c1 --- /dev/null +++ b/core/libraries/Hubzero/Test/Database.php @@ -0,0 +1,127 @@ +connection)) + { + // Only create the pdo object once per file + if (is_null(self::$pdo)) + { + $filename = with(new \ReflectionClass(get_called_class()))->getFileName(); + + self::$pdo = new \PDO('sqlite:' . dirname($filename) . DS . 'Fixtures' . DS . $this->fixture); + } + + $this->connection = $this->createDefaultDBConnection(self::$pdo, 'main'); + } + + return $this->connection; + } + + /** + * Gets the database seed info (PHPUnit does this for every test) + * + * @return object PHPUnit_Extensions_Database_DataSet_IDataSet + */ + public function getDataSet() + { + $filename = with(new \ReflectionClass(get_called_class()))->getFileName(); + + return $this->createXMLDataSet(dirname($filename) . DS . 'Fixtures' . DS . $this->seed); + } + + /** + * Gets a mock database driver + * + * @return object + */ + public function getMockDriver() + { + if (!isset(self::$dbo)) + { + self::$dbo = $this->getMockBuilder('Hubzero\Database\Driver\Pdo') + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + + $this->getConnection(); + + self::$dbo->setConnection(self::$pdo) + ->setPrefix(''); + } + + return self::$dbo; + } + + /** + * Resets the database handle to ensure it doesn't cause + * problems for subsequent tests + * + * @return void + */ + public static function tearDownAfterClass() + { + self::$dbo = null; + self::$pdo = null; + } +} diff --git a/core/libraries/Hubzero/Trac/Project.php b/core/libraries/Hubzero/Trac/Project.php new file mode 100644 index 00000000000..b86ae3cdcd4 --- /dev/null +++ b/core/libraries/Hubzero/Trac/Project.php @@ -0,0 +1,894 @@ +_updatedkeys = array(); + + foreach ($cvars as $key => $value) + { + if ($key{0} != '_') + { + unset($this->$key); + + if (!in_array($key, $this->_list_keys)) + { + $this->$key = null; + } + else + { + $this->$key = array(); + } + } + } + + $this->_updatedkeys = array(); + + return true; + } + + /** + * Log a debug message + * + * @param string $msg Message to log + * @return void + */ + private function logDebug($msg) + { + $xlog = \App::get('log')->logger('debug'); + $xlog->debug($msg); + } + + /** + * Output data as an array + * + * @return array + */ + public function toArray() + { + $result = array(); + + $cvars = get_class_vars(__CLASS__); + + foreach ($cvars as $key => $value) + { + if ($key{0} == '_') + { + continue; + } + + $current = $this->__get($key); + + $result[$key] = $current; + } + + return $result; + } + + /** + * Find a project + * + * @param string $name Project name + * @return mixed + */ + public function find($name) + { + $hztp = new self(); + + if (is_numeric($name)) + { + $hztp->id = $name; + } + else + { + $hztp->name = $name; + } + + if ($hztp->read() == false) + { + return false; + } + + return $hztp; + } + + /** + * Find a project. Create it if one doesn't exist. + * + * @param string $name Project name + * @return mixed + */ + public static function find_or_create($name) + { + $hztp = new self(); + + if (is_numeric($name)) + { + $hztp->id = $name; + } + else + { + $hztp->name = $name; + } + + if ($hztp->read() == false) + { + if ($hztp->create() == false) + { + return false; + } + } + + return $hztp; + } + + /** + * Create a record + * + * @return boolean + */ + public function create() + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + if (is_numeric($this->id)) + { + $query = "INSERT INTO `#__trac_project` (id,name) VALUES ( " . $db->quote($this->id) . "," . $db->quote($this->name) . ");"; + $db->setQuery($query); + + $result = $db->query(); + + if ($result !== false || $db->getErrorNum() == 1062) + { + return true; + } + } + else + { + $query = "INSERT INTO `#__trac_project` (name) VALUES ( " . $db->quote($this->name) . ");"; + + $db->setQuery($query); + + $result = $db->query(); + + if ($result === false && $db->getErrorNum() == 1062) + { + $query = "SELECT id FROM `#__trac_project` WHERE name=" . $db->quote($this->name) . ";"; + + $db->setQuery($query); + + $result = $db->loadResult(); + + if ($result == null) + { + return false; + } + + $this->id = $result; + return true; + } + else if ($result !== false) + { + $this->id = $db->insertid(); + return true; + } + } + + return false; + } + + /** + * Read a record + * + * @return boolean + */ + public function read() + { + $db = \App::get('db'); + + $lazyloading = false; + + if (empty($db)) + { + return false; + } + + if (is_numeric($this->id)) + { + $query = "SELECT * FROM `#__trac_project` WHERE id = " . $db->quote($this->id) . ";"; + } + else + { + $query = "SELECT * FROM `#__trac_project` WHERE name = " . $db->quote($this->name) . ";"; + } + + $db->setQuery($query); + + $result = $db->loadAssoc(); + + if (empty($result)) + { + return false; + } + + $this->clear(); + + foreach ($result as $key => $value) + { + if (property_exists(__CLASS__, $key) && $key{0} != '_') + { + $this->__set($key, $value); + } + } + + $this->_updatedkeys = array(); + + return true; + } + + /** + * Update a record + * + * @param boolean $all + * @return boolean + */ + public function update($all = false) + { + $db = \App::get('db'); + + $query = "UPDATE `#__trac_project` SET "; + + $classvars = get_class_vars(__CLASS__); + + $first = true; + + foreach ($classvars as $property => $value) + { + if (($property{0} == '_') || in_array($property, $this->_list_keys)) + { + continue; + } + + if (!$all && !in_array($property, $this->_updatedkeys)) + { + continue; + } + + if (!$first) + { + $query .= ','; + } + else + { + $first = false; + } + + $value = $this->__get($property); + + if ($value === null) + { + $query .= "`$property`=NULL"; + } + else + { + $query .= "`$property`=" . $db->quote($value); + } + } + + $query .= " WHERE `id`=" . $db->quote($this->__get('id')) . ";"; + + if ($first == true) + { + $query = ''; + } + + if (!empty($query)) + { + $db->setQuery($query); + + $result = $db->query(); + + if ($result === false) + { + return false; + } + + $affected = $db->getAffectedRows(); + + if ($affected < 1) + { + $this->create(); + + $db->setQuery($query); + + $result = $db->query(); + + if ($result === false) + { + return false; + } + + $affected = $db->getAffectedRows(); + + if ($affected < 1) + { + return false; + } + } + } + + return true; + } + + /** + * Delete a record + * + * @return boolean + */ + public function delete() + { + if (!isset($this->name) && !isset($this->id)) + { + return false; + } + + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + if (!isset($this->id)) + { + $db->setQuery("SELECT id FROM `#__trac_project` WHERE name=" . $db->quote($this->name) . ";"); + + $this->id = $db->loadResult(); + } + + if (empty($this->id)) + { + return false; + } + + $db->setQuery("DELETE FROM `#__trac_project` WHERE id=" . $db->quote($this->id) . ";"); + + if (!$db->query()) + { + return false; + } + + $db->setQuery("DELETE FROM `#__trac_user_permission` WHERE trac_project_id=" . $db->quote($this->id) . ";"); + $db->query(); + $db->setQuery("DELETE FROM `#__trac_group_permission` WHERE trac_project_id=" . $db->quote($this->id) . ";"); + $db->query(); + + return true; + } + + /** + * Property accessor + * + * @param string $property + * @return string + */ + public function __get($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + if (in_array($property, $this->_list_keys)) + { + if (!array_key_exists($property, get_object_vars($this))) + { + $db = \App::get('db'); + + if (is_object($db)) + { + $query = null; + + if (!empty($query)) + { + $db->setQuery($query); + + $result = $db->loadColumn(); + } + + if ($result !== false) + { + $this->__set($property, $result); + } + } + } + } + + if (isset($this->$property)) + { + return $this->$property; + } + + if (array_key_exists($property, get_object_vars($this))) + { + return null; + } + + $this->_error("Undefined property " . __CLASS__ . "::$" . $property, E_USER_NOTICE); + + return null; + } + + /** + * Property setter + * + * @param string $property + * @param mixed $value + * @return void + */ + public function __set($property = null, $value = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + if (in_array($property, $this->_list_keys)) + { + $value = array_diff((array) $value, array('')); + $value = array_unique($value); + $value = array_values($value); + $this->$property = $value; + } + else + { + $this->$property = $value; + } + + if (!in_array($property, $this->_updatedkeys)) + { + $this->_updatedkeys[] = $property; + } + } + + /** + * Check if a property is set + * + * @param string $property Property name + * @return boolean + */ + public function __isset($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + return isset($this->$property); + } + + /** + * Unset a property + * + * @param string $property Property name + * @return void + */ + public function __unset($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + $this->_updatedkeys = array_diff($this->_updatedkeys, array($property)); + + unset($this->$property); + } + + /** + * Echo an error message + * + * @param string $message + * @param integer $level + * @return void + */ + private function _error($message, $level = E_USER_NOTICE) + { + $caller = next(debug_backtrace()); + + switch ($level) + { + case E_USER_NOTICE: + echo "Notice: "; + break; + case E_USER_ERROR: + echo "Fatal error: "; + break; + default: + echo "Unknown error: "; + break; + } + + echo $message . ' in ' . $caller['file'] . ' on line ' . $caller['line'] . "\n"; + } + + /** + * Property accessor + * + * @param string $property + * @return string + */ + public function get($key) + { + return $this->__get($key); + } + + /** + * Property setter + * + * @param string $property + * @param mixed $value + * @return void + */ + public function set($key, $value) + { + return $this->__set($key, $value); + } + + /** + * Add permission to a user + * + * @param string $user + * @param string $action + * @return void + */ + public function add_user_permission($user, $action) + { + $db = \App::get('db'); + + if ($user == 'anonymous') + { + $user = '0'; + } + + if (!is_numeric($user)) + { + $query = "SELECT id FROM `#__users` WHERE username=" . $db->quote($user) . ";"; + $db->setQuery($query); + $user_id = $db->loadResult(); + + if ($user_id === false) + { + $this->_error("Unknown user $user"); + return false; + } + } + else + { + $user_id = $user; + } + + $quoted_project_id = $db->quote($this->id); + $quoted_user_id = $db->quote($user_id); + $values = ''; + + foreach ((array) $action as $a) + { + if (!empty($values)) + { + $values .= ','; + } + $values .= "($quoted_project_id,$quoted_user_id," . $db->quote($a) .")"; + } + + $query = "INSERT IGNORE INTO `#__trac_user_permission` (trac_project_id,user_id,action) VALUES " . $values . ";"; + + $db->setQuery($query); + $db->query(); + } + + /** + * Add permission to a group + * + * @param string $group + * @param string $action + * @return void + */ + public function add_group_permission($group, $action) + { + $db = \App::get('db'); + + if ($group == 'authenticated') + { + $group = '0'; + } + + if (!is_numeric($group)) + { + $query = "SELECT gidNumber FROM `#__xgroups` WHERE cn=" . $db->quote($group) . ";"; + $db->setQuery($query); + $group_id = $db->loadResult(); + + if ($group_id === false) + { + $this->_error("Unknown group $group"); + return false; + } + } + + $quoted_project_id = $db->quote($this->id); + $quoted_group_id = $db->quote($group_id); + $values = ''; + + foreach ((array) $action as $a) + { + if (!empty($values)) + { + $values .= ','; + } + $values .= "($quoted_project_id,$quoted_group_id," . $db->quote($a) .")"; + } + + $query = "INSERT IGNORE INTO `#__trac_group_permission` (trac_project_id,group_id,action) VALUES " . $values . ";"; + + $db->setQuery($query); + $db->query(); + } + + /** + * Remove permission form a user + * + * @param string $user + * @param string $action + * @return void + */ + public function remove_user_permission($user, $action) + { + $db = \App::get('db'); + $all = false; + + if ($user == 'anonymous') + { + $user = '0'; + } + + if (!is_numeric($user)) + { + $query = "SELECT id FROM `#__users` WHERE username=" . $db->quote($user) . ";"; + $db->setQuery($query); + $user_id = $db->loadResult(); + + if ($user_id === false) + { + $this->_error("Unknown user $user"); + return false; + } + } + else + { + $user_id = $user; + } + + $quoted_project_id = $db->quote($this->id); + $quoted_user_id = $db->quote($user_id); + $values = ''; + + foreach ((array) $action as $a) + { + if ($a == '*') + { + $all = true; + } + if (!empty($values)) + { + $values .= ','; + } + $values .= $db->quote($a); + } + + $query = "DELETE FROM `#__trac_user_permission` WHERE trac_project_id=$quoted_project_id AND user_id=$quoted_user_id"; + + if (!$all) + { + $query .= " AND action IN (" . $values . ");"; + } + + $db->setQuery($query); + $db->query(); + } + + /** + * Remove permission form a group + * + * @param string $group + * @param string $action + * @return void + */ + public function remove_group_permission($group, $action) + { + $db = \App::get('db'); + $all = false; + + if ($group == 'authenticated') + { + $group = '0'; + } + + if (!is_numeric($group)) + { + $query = "SELECT gidNumber FROM `#__xgroups` WHERE cn=" . $db->quote($group) . ";"; + $db->setQuery($query); + $group_id = $db->loadResult(); + + if ($group_id === null) + { + $this->_error("Unknown group $group"); + return false; + } + } + + $quoted_project_id = $db->quote($this->id); + $quoted_group_id = $db->quote($group_id); + $values = ''; + + foreach ((array) $action as $a) + { + if ($a == '*') + { + $all = true; + } + if (!empty($values)) + { + $values .= ','; + } + $values .= $db->quote($a); + } + + $query = "DELETE FROM `#__trac_group_permission` WHERE trac_project_id=$quoted_project_id AND group_id=$quoted_group_id"; + + if (!$all) + { + $query .= " AND action IN (" . $values . ");"; + } + + $db->setQuery($query); + $db->query(); + } + + /** + * Get permissions for a user + * + * @param string $user + * @return array + */ + public function get_user_permission($user) + { + $db = \App::get('db'); + $quoted_project_id = $db->quote($this->id); + + if ($user == "anonymous") + { + $user = '0'; + } + $quoted_user = $db->quote($user); + if (is_numeric($user)) + { + $query = "SELECT action FROM `#__trac_user_permission` AS up WHERE up.trac_project_id=$quoted_project_id AND up.user_id=$quoted_user;"; + } + else + { + $query = "SELECT action FROM `#__trac_user_permission` AS up, `#__users` AS u WHERE up.trac_project_id=$quoted_project_id AND u.id=up.user_id AND u.username=$quoted_user;"; + } + + $db->setQuery($query); + $result = $db->loadColumn(); + + return $result; + } + + /** + * Get permissions for a group + * + * @param string $group + * @return array + */ + public function get_group_permission($group) + { + $db = \App::get('db'); + $quoted_project_id = $db->quote($this->id); + + if ($group == 'authenticated') + { + $group = '0'; + } + $quoted_group = $db->quote($group); + if (is_numeric($group)) + { + $query = "SELECT action FROM `#__trac_group_permission` AS gp WHERE gp.trac_project_id=$quoted_project_id AND gp.group_id=$quoted_group;"; + } + else + { + $query = "SELECT action FROM `#__trac_group_permission` AS gp, `#__xgroups` AS g WHERE gp.trac_project_id=$quoted_project_id AND g.gidNumber=gp.group_id AND g.cn=$quoted_group;"; + } + + $db->setQuery($query); + $result = $db->loadColumn(); + + return $result; + } +} diff --git a/core/libraries/Hubzero/User/Group.php b/core/libraries/Hubzero/User/Group.php new file mode 100644 index 00000000000..a12416d2370 --- /dev/null +++ b/core/libraries/Hubzero/User/Group.php @@ -0,0 +1,1539 @@ +_updatedkeys = array(); + + foreach ($cvars as $key => $value) + { + if ($key{0} != '_') + { + unset($this->$key); + + if (!in_array($key, self::$_list_keys)) + { + $this->$key = null; + } + else + { + $this->$key = array(); + } + } + } + + $this->_updatedkeys = array(); + + return true; + } + + /** + * Returns a reference to a group object + * + * @param mixed $group A string (cn) or integer (ID) + * @return mixed Object if instance found, false if not + */ + public static function getInstance($group) + { + static $instances; + + // Set instances array + if (!isset($instances)) + { + $instances = array(); + } + + // Do we have a matching instance? + if (!isset($instances[$group])) + { + // If an ID is passed, check for a match in existing instances + if (is_numeric($group)) + { + foreach ($instances as $instance) + { + if ($instance && $instance->get('gidNumber') == $group) + { + // Match found + return $instance; + break; + } + } + } + + // No matches + // Create group object + $hzg = new self(); + + if ($hzg->read($group) === false) + { + $instances[$group] = false; + } + else + { + $instances[$group] = $hzg; + } + } + + // Return instance + return $instances[$group]; + } + + /** + * Creates a new group in the CMS. + * Creates a new entry under the #__xgroups table. + * + * @return mixed Returns false if error or gid upon success. + */ + public function create() + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + $cn = $this->cn; + $gidNumber = $this->gidNumber; + + if (empty($cn) && empty($gidNumber)) + { + return false; + } + + if (is_numeric($gidNumber)) + { + if (empty($cn)) + { + $cn = '_gid' . $gidNumber; + } + + $query = "INSERT INTO `#__xgroups` (gidNumber,cn) VALUES (" . $db->quote($gidNumber) . "," . $db->quote($cn) . ");"; + + $db->setQuery($query); + + $result = $db->query(); + + if ($result === false && $db->getErrorNum() != 1062) + { + return false; + } + } + else + { + $query = "INSERT INTO `#__xgroups` (cn) VALUES (" . $db->quote($cn) . ");"; + + $db->setQuery($query); + + $result = $db->query(); + + if ($result === false && $db->getErrorNum() == 1062) // row exists + { + $query = "SELECT gidNumber FROM `#__xgroups` WHERE cn=" . $db->quote($cn) . ";"; + + $db->setQeury($query); + + $result = $db->loadResult(); + + if (empty($result)) + { + return false; + } + + $this->__set('gidNumber', $result); + } + else if ($result !== false) + { + $this->__set('gidNumber', $db->insertid()); + } + else + { + return false; + } + } + + //trigger the onAfterStoreGroup event + \Event::trigger('user.onAfterStoreGroup', array($this)); + + return $this->gidNumber; + } + + /** + * Read a record + * + * @param mixed $name + * @return boolean + */ + public function read($name = null) + { + $this->clear(); + + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + if (!is_null($name)) + { + if (Validate::positiveInteger($name)) + { + $this->gidNumber = $name; + } + else + { + $this->cn = $name; + } + } + + $result = true; + $lazyloading = false; + + if (is_numeric($this->gidNumber)) + { + $query = "SELECT * FROM `#__xgroups` WHERE gidNumber = " . $db->quote($this->gidNumber) . ";"; + } + else + { + $query = "SELECT * FROM `#__xgroups` WHERE cn = " . $db->quote($this->cn) . ";"; + } + + $db->setQuery($query); + + $result = $db->loadAssoc(); + + if (empty($result)) + { + $this->clear(); + return false; + } + + foreach ($result as $key => $value) + { + if (property_exists(__CLASS__, $key) && $key{0} != '_') + { + $this->__set($key, $value); + } + } + + $this->__unset('members'); // we unset the lists so we can detect whether they have been loaded or not + $this->__unset('invitees'); + $this->__unset('applicants'); + $this->__unset('managers'); + + if (!$lazyloading) + { + $this->__get('members'); + $this->__get('invitees'); + $this->__get('applicants'); + $this->__get('managers'); + } + + $this->_updatedkeys = array(); + + return true; + } + + /** + * Update a record + * + * @return boolean + */ + public function update() + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + if (!is_numeric($this->gidNumber)) + { + return false; + } + + if (empty($this->_updatedkeys)) + { + return true; + } + + $query = "UPDATE `#__xgroups` SET "; + + $classvars = get_class_vars(__CLASS__); + + $first = true; + $affected = 0; + + foreach ($classvars as $property => $value) + { + if (($property{0} == '_') || in_array($property, self::$_list_keys)) + { + continue; + } + + if (!in_array($property, $this->_updatedkeys)) + { + continue; + } + + if (!$first) + { + $query .= ','; + } + else + { + $first = false; + } + + $value = $this->$property; + + if ($value === null) + { + $query .= "`$property`=NULL"; + } + else + { + $query .= "`$property`=" . $db->quote($value); + } + } + + $query .= " WHERE `gidNumber`=" . $db->quote($this->gidNumber) . ";"; + + if (($first != true) && !empty($query)) + { + $db->setQuery($query); + + $result = $db->query(); + + if ($result === false) + { + return false; + } + + $affected = $db->getAffectedRows(); + } + + $aNewUserGroupEnrollments = array(); + + foreach (self::$_list_keys as $property) + { + if (!in_array($property, $this->_updatedkeys)) + { + continue; + } + + $aux_table = "#__xgroups_" . $property; + + $list = $this->$property; + + if (!is_null($list) && !is_array($list)) + { + $list = array($list); + } + + $ulist = null; + $tlist = null; + + foreach ($list as $value) + { + if (!is_null($ulist)) + { + $ulist .= ','; + $tlist .= ','; + } + + $ulist .= $db->quote($value); + $tlist .= '(' . $db->quote($this->gidNumber) . ',' . $db->quote($value) . ')'; + } + + // @FIXME: I don't have a better solution yet. But the next refactoring of this class + // should eliminate the ability to read the entire member table due to problems with + // scale on a large (thousands of members) groups. The add function should track the members + // being added to a group, but would need to be verified to handle adding members + // already in group. *njk* + + // @FIXME: Not neat, but because all group membership is resaved every time even for single additions + // there is no nice way to detect only *new* additions without this check. I don't want to + // fire off an 'onUserGroupEnrollment' event for users unless they are really being enrolled. *drb* + + if (in_array($property, array('members', 'managers'))) + { + $query = "SELECT uidNumber FROM `#__xgroups_members` WHERE gidNumber=" . $this->gidNumber; + $db->setQuery($query); + + // compile current list of members in this group + $aExistingUserMembership = array(); + + if (($results = $db->loadAssoc())) + { + foreach ($results as $uid) + { + $aExistingUserMembership[] = $uid; + } + } + + // see who is new, merge with previous additions so we have a complete list after we are done + $aNewUserGroupEnrollments = array_merge($aNewUserGroupEnrollments, array_diff($list, $aExistingUserMembership)); + + } + + if (is_array($list) && count($list) > 0) + { + if (in_array($property, array('members', 'managers', 'applicants', 'invitees'))) + { + $query = "REPLACE INTO $aux_table (gidNumber,uidNumber) VALUES $tlist;"; + } + + $db->setQuery($query); + + if ($db->query()) + { + $affected += $db->getAffectedRows(); + } + } + + if (!is_array($list) || count($list) == 0) + { + if (in_array($property, array('members', 'managers', 'applicants', 'invitees'))) + { + $query = "DELETE FROM $aux_table WHERE gidNumber=" . $db->quote($this->gidNumber) . ";"; + } + } + else + { + if (in_array($property, array('members', 'managers', 'applicants', 'invitees'))) + { + $query = "DELETE m FROM `#__xgroups_$property` AS m WHERE " . " m.gidNumber=" . + $db->quote($this->gidNumber) . " AND m.uidNumber NOT IN (" . $ulist . ");"; + } + } + + $db->setQuery($query); + + if ($db->query()) + { + $affected += $db->getAffectedRows(); + } + } + + // After SQL is done and has no errors, fire off onGroupUserEnrolledEvents + // for every user added to this group + foreach ($aNewUserGroupEnrollments as $userid) + { + \Event::trigger('groups.onGroupUserEnrollment', array($this->gidNumber, $userid)); + } + + if ($affected > 0) + { + \Event::trigger('user.onAfterStoreGroup', array($this)); + } + + return true; + } + + /** + * Delete a record + * + * @return boolean + */ + public function delete() + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + if (!is_numeric($this->gidNumber)) + { + $db->setQuery("SELECT gidNumber FROM `#__xgroups` WHERE cn=" . $db->quote($this->cn) . ";"); + + $gidNumber = $db->loadResult(); + + if (!is_numeric($this->gidNumber)) + { + return false; + } + + $this->gidNumber = $gidNumber; + } + + $db->setQuery("DELETE FROM `#__xgroups` WHERE gidNumber=" . $db->quote($this->gidNumber) . ";"); + + $result = $db->query(); + + if ($result === false) + { + return false; + } + + $db->setQuery("DELETE FROM `#__xgroups_applicants` WHERE gidNumber=" . $db->quote($this->gidNumber) . ";"); + $db->query(); + $db->setQuery("DELETE FROM `#__xgroups_invitees` WHERE gidNumber=" . $db->quote($this->gidNumber) . ";"); + $db->query(); + $db->setQuery("DELETE FROM `#__xgroups_managers` WHERE gidNumber=" . $db->quote($this->gidNumber) . ";"); + $db->query(); + $db->setQuery("DELETE FROM `#__xgroups_members` WHERE gidNumber=" . $db->quote($this->gidNumber) . ";"); + $db->query(); + + //trigger the onAfterStoreGroup event + \Event::trigger('user.onAfterStoreGroup', array($this)); + + return true; + } + + /** + * Get a property's value + * + * @param string $property + * @return mixed + */ + public function __get($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + if (in_array($property, self::$_list_keys)) + { + if (!array_key_exists($property, get_object_vars($this))) + { + $db = \App::get('db'); + + if (is_object($db)) + { + $groups = array('applicants'=>array(), 'invitees'=>array(), 'members'=>array(), 'managers'=>array()); + + foreach ($groups as $key => $data) + { + $this->__set($key, $data); + } + + $query = "(select uidNumber, 'invitees' AS role from #__xgroups_invitees where gidNumber=" . $db->quote($this->gidNumber) . ") + UNION + (select uidNumber, 'applicants' AS role from #__xgroups_applicants where gidNumber=" . $db->quote($this->gidNumber) . ") + UNION + (select uidNumber, 'members' AS role from #__xgroups_members where gidNumber=" . $db->quote($this->gidNumber) . ") + UNION + (select uidNumber, 'managers' AS role from #__xgroups_managers where gidNumber=" . $db->quote($this->gidNumber) . ")"; + + $db->setQuery($query); + + if (($results = $db->loadObjectList())) + { + foreach ($results as $result) + { + if (isset($groups[$result->role])) + { + $groups[$result->role][] = $result->uidNumber; + } + } + + foreach ($groups as $key => $data) + { + $this->__set($key, $data); + } + } + } + } + } + + if (isset($this->$property)) + { + return $this->$property; + } + + if (array_key_exists($property, get_object_vars($this))) + { + return null; + } + + $this->_error("Undefined property " . __CLASS__ . "::$" . $property, E_USER_NOTICE); + + return null; + } + + /** + * Set a property's value + * + * @param string $property + * @param mixed $value + * @return void + */ + public function __set($property = null, $value = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + if (in_array($property, self::$_list_keys)) + { + $value = array_diff((array) $value, array('')); + + if (in_array($property, array('managers', 'members', 'applicants', 'invitees'))) + { + $value = $this->_userids($value); + } + + $value = array_unique($value); + $value = array_values($value); + $this->$property = $value; + } + else + { + $this->$property = $value; + } + + if (!in_array($property, $this->_updatedkeys)) + { + $this->_updatedkeys[] = $property; + } + } + + /** + * Check if a property is set + * + * @param string $property + * @return bool + */ + public function __isset($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + return isset($this->$property); + } + + /** + * Unset a property + * + * @param string $property + * @return void + */ + public function __unset($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + $this->_updatedkeys = array_diff($this->_updatedkeys, array($property)); + + unset($this->$property); + } + + /** + * Output an error message + * + * @param string $message + * @param integer $level + * @return void + */ + private function _error($message, $level = E_USER_NOTICE) + { + $caller = next(debug_backtrace()); + + switch ($level) + { + case E_USER_NOTICE: + echo "Notice: "; + break; + case E_USER_ERROR: + echo "Fatal error: "; + break; + default: + echo "Unknown error: "; + break; + } + + echo $message . ' in ' . $caller['file'] . ' on line ' . $caller['line'] . "\n"; + } + + /** + * Get a property's value + * + * @param string $property + * @return mixed + */ + public function get($key, $default=null) + { + return $this->__get($key); + } + + /** + * Set a property's value + * + * @param string $property + * @param mixed $value + * @return void + */ + public function set($key, $value) + { + return $this->__set($key, $value); + } + + /** + * Get a list of user IDs from a string, list, or list of usernames + * + * @param array $users + * @return mixed + */ + private function _userids($users) + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + $usernames = array(); + $userids = array(); + + if (!is_array($users)) + { + $users = array($users); + } + + foreach ($users as $u) + { + if (is_numeric($u)) + { + $userids[] = $u; + } + else + { + $usernames[] = $db->quote($u); + } + } + + if (empty($usernames)) + { + return $userids; + } + + $set = implode($usernames, ","); + + $sql = "SELECT id FROM `#__users` WHERE username IN ($set);"; + + $db->setQuery($sql); + + $result = $db->loadColumn(); + + if (empty($result)) + { + $result = array(); + } + + $result = array_merge($result, $userids); + + return $result; + } + + /** + * Add users to a table + * + * @param string $key + * @param array $value + * @return void + */ + public function add($key = null, $value = array()) + { + $users = $this->_userids($value); + + $this->__set($key, array_merge($this->__get($key), $users)); + } + + /** + * Remove users form a table + * + * @param string $key + * @param array $value + * @return void + */ + public function remove($key = null, $value = array()) + { + $users = $this->_userids($value); + + $this->__set($key, array_diff($this->__get($key), $users)); + } + + /** + * Iterate through each group and do something + * + * @param string $func + * @return boolean + */ + static function iterate($func) + { + $db = \App::get('db'); + + $query = "SELECT cn FROM `#__xgroups`;"; + + $db->setQuery($query); + + $result = $db->loadColumn(); + + if ($result === false) + { + return false; + } + + foreach ($result as $row) + { + call_user_func($func, $row); + } + + return true; + } + + /** + * Check if a group exists. + * Given the group id, returns true if group exists. + * + * @param integer $group The group id number (GID) of the group being verified. + * @param boolean $check_system Boolean for checking against POSIX user. + * @return boolean Returns false if group does not exist; true if group exists. + */ + public static function exists($group, $check_system = false) + { + $db = \App::get('db'); + + if (empty($group)) + { + return false; + } + + if ($check_system) + { + if (is_numeric($group) && posix_getgrgid($group)) + { + return true; + } + + if (!is_numeric($group) && posix_getgrnam($group)) + { + return true; + } + } + + // check reserved + if (Validate::reserved('group', $group)) + { + return true; + } + + if (is_numeric($group)) + { + $query = 'SELECT gidNumber FROM `#__xgroups` WHERE gidNumber=' . $db->quote($group); + } + else + { + $query = 'SELECT gidNumber FROM `#__xgroups` WHERE cn=' . $db->quote($group); + } + + $db->setQuery($query); + + if (!$db->query()) + { + return false; + } + + if ($db->loadResult() > 0) + { + return true; + } + + return false; + } + + /** + * Find groups + * + * @param array $filters + * @return mixed + */ + static function find($filters = array()) + { + $db = \App::get('db'); + + // Type 0 - System Group + // Type 1 - HUB Group + // Type 2 - Project Group + // Type 3 - Partner "Special" Group + // Type 4 - Course group + $gTypes = array('all', 'system', 'hub', 'project', 'super', 'course', '0', '1', '2', '3', '4'); + + $types = !empty($filters['type']) ? $filters['type'] : array('all'); + + foreach ($types as $type) + { + if (!in_array($type, $gTypes)) + { + return false; + } + } + + $where = array(); + + if (!in_array('all', $types)) + { + $t = implode(",", $types); + + //replace group type names with group type id + $t = str_replace('hub', 1, $t); + $t = str_replace('project', 2, $t); + $t = str_replace('super', 3, $t); + $t = str_replace('course', 4, $t); + $t = str_replace('system', 0, $t); + + $where[] = 'type IN (' . $t . ')'; + } + + if (isset($filters['search']) && $filters['search'] != '') + { + if (is_numeric($filters['search'])) + { + $where[] = "gidNumber=" . $db->quote($filters['search']); + } + else + { + $where[] = "(LOWER(description) LIKE " . $db->quote('%' . strtolower($filters['search']) . '%') . " OR LOWER(cn) LIKE " . $db->quote('%' . strtolower($filters['search']) . '%') . ")"; + } + } + + if (isset($filters['index']) && $filters['index'] != '') + { + $where[] = "(LOWER(description) LIKE " . $db->quote(strtolower($filters['index']) . '%') . ")"; + } + + if (isset($filters['authorized']) && $filters['authorized'] === 'admin') + { + if (isset($filters['discoverability']) && $filters['discoverability'] != '') + { + switch ($filters['discoverability']) + { + case 0: + $where[] = "discoverability=0"; + break; + case 1: + $where[] = "discoverability=1"; + break; + } + } + } + else + { + $where[] = "discoverability=0"; + } + + if (isset($filters['policy']) && $filters['policy']) + { + switch ($filters['policy']) + { + case 'closed': + $where[] = "join_policy=3"; + break; + case 'invite': + $where[] = "join_policy=2"; + break; + case 'restricted': + $where[] = "join_policy=1"; + break; + case 'open': + default: + $where[] = "join_policy=0"; + break; + } + } + + if (isset($filters['published']) && $filters['published'] != '') + { + $where[] = "published=" . $db->quote($filters['published']); + } + + if (isset($filters['approved']) && $filters['approved'] != '') + { + $where[] = "approved=" . $db->quote($filters['approved']); + } + + if (isset($filters['created']) && $filters['created'] != '') + { + if ($filters['created'] == 'pastday') + { + $pastDay = date("Y-m-d H:i:s", strtotime('-1 DAY')); + $where[] = "created >= " . $db->quote($pastDay); + } + } + + + if (empty($filters['fields'])) + { + $filters['fields'][] = 'cn'; + } + + $field = implode(',', $filters['fields']); + + $query = "SELECT $field FROM `#__xgroups`"; + if (count($where) > 0) + { + $query .= " WHERE " . implode(" AND ", $where); + } + + if (isset($filters['sortby']) && $filters['sortby'] != '') + { + $query .= " ORDER BY "; + + switch ($filters['sortby']) + { + case 'alias': + $query .= 'cn ASC'; + break; + case 'title': + $query .= 'description ASC'; + break; + default: + $query .= $filters['sortby']; + break; + } + } + + if (isset($filters['limit']) && $filters['limit'] != 'all') + { + $query .= " LIMIT " . $filters['start'] . "," . $filters['limit']; + } + + $query .= ";"; + + $db->setQuery($query); + + if (!in_array('COUNT(*)', $filters['fields'])) + { + $result = $db->loadObjectList(); + } + else + { + $result = $db->loadResult(); + } + + if (empty($result)) + { + return false; + } + + return $result; + } + + /** + * Check if the user is a member of a given table + * + * @param string $table Table to check + * @param integer $uid User ID + * @return boolean + */ + public function is_member_of($table, $uid) + { + if (!in_array($table, array('applicants', 'members', 'managers', 'invitees'))) + { + return false; + } + + if (!is_numeric($uid)) + { + $uid = User::oneByUsername($uid)->get('id'); + } + + return in_array($uid, $this->get($table)); + } + + /** + * Is user a member of the group? + * + * @param integer $uid + * @return bool + */ + public function isMember($uid) + { + return $this->is_member_of('members', $uid); + } + + /** + * Is user an applicant of the group? + * + * @param integer $uid + * @return bool + */ + public function isApplicant($uid) + { + return $this->is_member_of('applicants', $uid); + } + + /** + * Is user a manager of the group? + * + * @param integer $uid + * @return bool + */ + public function isManager($uid) + { + return $this->is_member_of('managers', $uid); + } + + /** + * Is user an invitee of the group? + * + * @param integer $uid + * @return bool + */ + public function isInvitee($uid) + { + return $this->is_member_of('invitees', $uid); + } + + /** + * Get emails for users + * + * @param string $tbl + * @return array + */ + public function getEmails($tbl = 'managers') + { + if (!in_array($tbl, array('applicants', 'members', 'managers', 'invitees'))) + { + return false; + } + + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + $query = 'SELECT u.email FROM #__users AS u, #__xgroups_' . $tbl . ' AS gm WHERE gm.gidNumber=' . $db->quote($this->gidNumber) . ' AND u.id=gm.uidNumber;'; + + $db->setQuery($query); + + $emails = $db->loadColumn(); + + return $emails; + } + + /** + * Search + * + * @param string $tbl + * @param string $q + * @return array + */ + public function search($tbl = '', $q = '') + { + if (!in_array($tbl, array('applicants', 'members', 'managers', 'invitees'))) + { + return false; + } + + if ($q == '') + { + return false; + } + + $table = '#__xgroups_' . $tbl; + $user_table = '#__users'; + + $db = \App::get('db'); + + $query = "SELECT u.id FROM {$table} AS t, {$user_table} AS u + WHERE t.gidNumber={$db->quote($this->gidNumber)} + AND u.id=t.uidNumber + AND LOWER(u.name) LIKE " . $db->quote('%' . strtolower($q) . '%') . ";"; + $db->setQuery($query); + return $db->loadColumn(); + } + + /** + * Is a group a super group? + * + * @return bool + */ + public function isSuperGroup() + { + return ($this->get('type') == 3) ? true : false; + } + + /** + * Return a groups logo + * + * @param string $what What data to return? + * @return mixed + */ + public function getLogo($what='') + { + //default logo + static $default_logo; + + if (!$default_logo) + { + $default_logo = '/core/components/com_groups/site/assets/img/group_default_logo.png'; + } + + //logo link - links to group overview page + $link = \Route::url('index.php?option=com_groups&cn=' . $this->get('cn')); + + //path to group uploaded logo + $path = substr(PATH_APP, strlen(PATH_ROOT)) . '/site/groups/' . $this->get('gidNumber') . DS . 'uploads' . DS . $this->get('logo'); + + //if logo exists and file is uploaded use that logo instead of default + $src = ($this->get('logo') != '' && is_file(PATH_ROOT . $path)) ? $path : $default_logo; + + //check to make sure were a member to show logo for hidden group + $members_and_invitees = array_merge($this->get('members'), $this->get('invitees')); + if ($this->get('discoverability') == 1 + && !in_array(\User::get('id'), $members_and_invitees)) + { + $src = $default_logo; + } + + $what = strtolower($what); + if ($what == 'size') + { + return getimagesize(PATH_ROOT . $src); + } + + if ($what == 'path') + { + return $src; + } + + return \Request::base(true) . $src; + } + + /** + * Get groups path + * + * @return string + */ + public function getBasePath() + { + $groupParams = \Component::params('com_groups'); + $uploadPath = $groupParams->get('uploadpath', '/site/groups'); + return $uploadPath . DS . $this->get('gidNumber'); + } + + /** + * Return serve up path + * + * @param string $path + * @return string + */ + public function downloadLinkForPath($base = 'uploads', $path = '', $type = 'file') + { + // get base path + $groupFolder = $this->getBasePath(); + + // split segments by directory separator + $segments = array_map('trim', explode(DS, $path)); + + // prepend base segments to original segments + if ($base != 'uploads') + { + $baseSegments = array_map('trim', explode(DS, $base)); + $segments = array_merge($baseSegments, $segments); + } + + // build link + $link = \Route::url('index.php?option=com_groups&cn=' . $this->get('cn')); + $link .= '/' . ucfirst($type) . ':' . implode('/', $segments); + + // return link + return $link; + } + + /** + * Get the content of the entry + * + * @param string $as Format to return state in [text, number] + * @param integer $shorten Number of characters to shorten text to + * @param string $type Type to get [public, private] + * @return string + */ + public function getDescription($as='parsed', $shorten=0, $type='public') + { + $options = array(); + + // get description before parsing + $before = $this->get($type . '_desc'); + + switch (strtolower($as)) + { + case 'parsed': + $config = array( + 'option' => 'com_groups', + 'scope' => '', //$this->get('cn') . DS . 'wiki', + 'pagename' => $this->get('cn'), + 'pageid' => 0, //$this->get('gidNumber'), + 'filepath' => \Component::params('com_groups')->get('uploadpath', '/site/groups') . DS . $this->get('gidNumber') . DS . 'uploads', + 'domain' => $this->get('cn'), + 'camelcase' => 0 + ); + + \Event::trigger('content.onContentPrepare', array( + 'com_groups.group.' . $type . '_desc', + &$this, + &$config + )); + $content = $this->get($type . '_desc'); + + $options = array('html' => true); + break; + + case 'clean': + $content = strip_tags($this->getDescription('parsed', 0, $type)); + break; + + case 'raw': + default: + $content = stripslashes($this->get($type . '_desc')); + $content = preg_replace('/^()/i', '', $content); + break; + } + + if ($shorten) + { + $content = Str::truncate($content, $shorten, $options); + } + + // set our descriptions to be html + if ($before != $content && $as == 'parsed') + { + $this->set($type . '_desc', trim($content)); + //$this->update(); + } + + return $content; + } +} diff --git a/core/libraries/Hubzero/User/Group/Helper.php b/core/libraries/Hubzero/User/Group/Helper.php new file mode 100644 index 00000000000..c3713ff3a9c --- /dev/null +++ b/core/libraries/Hubzero/User/Group/Helper.php @@ -0,0 +1,402 @@ + 0) + { + $sql .= " LIMIT {$limit}"; + } + + //execute query and return result + $database->setQuery( $sql ); + if (!$database->getError()) + { + return $database->loadObjectList(); + } + } + + /** + * Gets featured groups + * + * @param string $groupList + * @return array + */ + public static function getFeaturedGroups($groupList) + { + //database object + $database = \App::get('db'); + + //parse the group list + $groupList = array_map('trim', array_filter(explode(',', $groupList), 'trim')); + + //make sure we have a list of groups + if (count($groupList) < 1) + { + return array(); + } + + //query to get groups + $sql = "SELECT g.gidNumber, g.cn, g.description, g.public_desc + FROM `#__xgroups` AS g + WHERE (g.type=1 + OR g.type=3) + AND g.published=1 + AND g.approved=1 + AND g.discoverability=0 + AND g.cn IN ('".implode("','", $groupList)."')"; + + $database->setQuery( $sql ); + if (!$database->getError()) + { + return $database->loadObjectList(); + } + } + + /** + * Gets groups matching tag string + * + * @param string $usertags + * @param string $usergroups + * @return string + */ + public static function getGroupsMatchingTagString($usertags, $usergroups) + { + //database object + $database = \App::get('db'); + + //turn users tag string into array + $mytags = ($usertags != '') ? array_map('trim', explode(',', $usertags)) : array(); + + //users groups + $mygroups = array(); + if (is_array($usergroups)) + { + foreach ($usergroups as $ug) + { + $mygroups[] = $ug->gidNumber; + } + } + + //query the databse for all published, type "HUB" groups + $sql = "SELECT g.gidNumber, g.cn, g.description, g.public_desc + FROM `#__xgroups` AS g + WHERE g.type=1 + AND g.published=1 + AND g.discoverability=0"; + $database->setQuery($sql); + + //get all groups + $groups = $database->loadObjectList(); + + //loop through each group and see if there is a tag match + foreach ($groups as $k => $group) + { + //get the groups tags + $gt = new \Components\Groups\Models\Tags($group->gidNumber); + + $group->tags = $gt->render('string'); + $group->tags = array_map('trim', explode(',', $group->tags)); + + //get common tags + $group->matches = array_intersect($mytags, $group->tags); + + //remove tags from the group object since its no longer needed + unset($group->tags); + + //if we dont have a match remove group from return results + //or if we are already a member of the group remove from return results + if (count($group->matches) < 1 || in_array($group->gidNumber, $mygroups)) + { + unset($groups[$k]); + } + } + + return $groups; + } + + /** + * Converts invite emails to true group + * + * @param string $email + * @param integer $user_id + * @return void + */ + public function convertInviteEmails($email, $user_id) + { + // @FIXME: Should wrap this up in a nice transaction to handle partial failures and + // race conditions. + + if (empty($email) || empty($user_id)) + { + return false; + } + + $db = \App::get('db'); + + $sql = 'SELECT gidNumber FROM `#__xgroups_inviteemails` WHERE email=' . $db->quote($email) . ';'; + + $db->setQuery($sql); + + $result = $db->loadColumn(); + + if ($result === false) + { + return false; + } + + if (empty($result)) + { + return true; + } + + foreach ($result as $r) + { + $values .= "($r,$user_id),"; + $gids .= "$r,"; + } + + $values = rtrim($values, ','); + $gids = rtrim($gids, ','); + + $sql = 'INSERT INTO `#__xgroups_invitees` (gidNumber,uidNumber) VALUES ' . $values . ';'; + + $db->setQuery($sql); + + $result = $db->query(); + + if (!$result) + { + return false; + } + + $sql = 'DELETE FROM `#__xgroups_inviteemails` WHERE email=' . $db->quote($email) . ' AND gidNumber IN (' . $gids . ');'; + + $db->setQuery($sql); + + $result = $db->query(); + + if (!$result) + { + return false; + } + + return true; + } + + /** + * Search group roles + * + * @param object $group + * @param string $role + * @return array + */ + public static function search_roles($group, $role = '') + { + if ($role == '') + { + return false; + } + + $db = \App::get('db'); + + $query = "SELECT uidNumber FROM `#__xgroups_roles` as r, `#__xgroups_member_roles` as m WHERE r.id='" . $role . "' AND r.id=m.roleid AND r.gidNumber='" . $group->gidNumber . "'"; + + $db->setQuery($query); + + $result = $db->loadColumn(); + + $result = array_intersect($result, $group->members); + + if (count($result) > 0) + { + return $result; + } + } + + /** + * Get the access level for a group plugin + * + * @param object $group + * @param string $get_plugin + * @return mixed + */ + public static function getPluginAccess($group, $get_plugin = '') + { + // make sure we have a Hubzero group + if (!($group instanceof Group)) + { + return; + } + + // Trigger the functions that return the areas we'll be using + //then add overview to array + $hub_group_plugins = \Event::trigger('groups.onGroupAreas', array()); + array_unshift($hub_group_plugins, array('name'=>'overview', 'title'=>'Overview', 'default_access'=>'anyone')); + + //array to store plugin preferences when after retrieved from db + $active_group_plugins = array(); + + //get the group plugin preferences + //returns array of tabs and their access level (ex. [overview] => 'anyone', [messages] => 'registered') + $group_plugins = $group->get('plugins'); + + if ($group_plugins) + { + $group_plugins = explode(',', $group_plugins); + + foreach ($group_plugins as $plugin) + { + $temp = explode('=', trim($plugin)); + + if ($temp[0]) + { + $active_group_plugins[$temp[0]] = trim($temp[1]); + } + } + } + + //array to store final group plugin preferences + //array of acceptable access levels + $group_plugin_access = array(); + $acceptable_levels = array('nobody', 'anyone', 'registered', 'members'); + + //if we have already set some + if ($active_group_plugins) + { + //for each plugin that is active on the hub + foreach ($hub_group_plugins as $hgp) + { + //if group defined access level is not an acceptable value or not set use default value that is set per plugin + //else use group defined access level + if (!isset($active_group_plugins[$hgp['name']]) || !in_array($active_group_plugins[$hgp['name']], $acceptable_levels)) + { + $value = $hgp['default_access']; + } + else + { + $value = $active_group_plugins[$hgp['name']]; + } + + //store final access level in array of access levels + $group_plugin_access[$hgp['name']] = $value; + } + } + else + { + //for each plugin that is active on the hub + foreach ($hub_group_plugins as $hgp) + { + $value = $hgp['default_access']; + + //store final access level in array of access levels + $group_plugin_access[$hgp['name']] = $value; + } + } + + //if we wanted to return only a specific level return that otherwise return all access levels + if ($get_plugin != '') + { + return $group_plugin_access[$get_plugin]; + } + else + { + return $group_plugin_access; + } + } + + /** + * Get Instance of Super Group Database + * + * Always returns the same instance whenever this method is called + * + * @param array $config Array of database options + * @param string $cname + * @return object Database Object + */ + public static function getDbo($config = array(), $cname = '') + { + // empty instance of db + $db = App::get('db'); + + // make sure we have a group object + if (!$group = Group::getInstance(\Request::getString('cn', $cname))) + { + return $db; + } + + // make sure we are a super group + if (!$group->isSuperGroup()) + { + return $db; + } + + // load super group db config if not passed in + if (empty($config)) + { + // build path to config file + $uploadPath = \Component::params( 'com_groups' )->get('uploadpath'); + $configPath = PATH_APP . DS . trim($uploadPath, DS) . DS . $group->get('gidNumber') . DS . 'config' . DS . 'db.php'; + + // make sure file exists + if (!file_exists($configPath)) + { + return $db; + } + + // include config + $config = include $configPath; + } + + // return instance of db + return \Hubzero\Database\Driver::getInstance($config); + } +} diff --git a/core/libraries/Hubzero/User/Group/InviteEmail.php b/core/libraries/Hubzero/User/Group/InviteEmail.php new file mode 100644 index 00000000000..ebd02bddae3 --- /dev/null +++ b/core/libraries/Hubzero/User/Group/InviteEmail.php @@ -0,0 +1,169 @@ + 'positive|nonzero', + 'email' => 'notempty', + 'token' => 'notempty' + ); + + /** + * Get parent group + * + * @return object + */ + public function group() + { + return \Hubzero\User\Group::getInstance($this->get('gidNumber')); + } + + /** + * Get a list of email invites for a group + * + * @param integer $gid Group ID + * @param boolean $email_only Resturn only email addresses? + * @return array + */ + public function getInviteEmails($gid, $email_only = false) + { + $final = array(); + + $invitees = self::all() + ->whereEquals('gidNumber', $gid) + ->ordered() + ->rows(); + + if ($email_only) + { + foreach ($invitees as $invitee) + { + $final[] = $invitee->get('email'); + } + } + else + { + $final = $invitees; + } + + return $final; + } + + /** + * Add a list of emails to a group as invitees + * + * @param integer $gid Group ID + * @param array $emails Array of email addresses + * @return array + */ + public function addInvites($gid, $emails) + { + $exists = array(); + $added = array(); + + $current = $this->getInviteEmails($gid, true); + + foreach ($emails as $e) + { + if (in_array($e, $current)) + { + $exists[] = $e; + } + else + { + $added[] = $e; + } + } + + if (count($added) > 0) + { + foreach ($added as $a) + { + $model = self::blank(); + $model->set([ + 'email' => $a, + 'gidNumber' => $gid, + 'token' => md5($a) + ]); + $model->save(); + } + } + + $return['exists'] = $exists; + $return['added'] = $added; + + return $return; + } + + /** + * Remove Invite Emails + * + * @param integer $gid Group ID + * @param array $emails Array of email addresses + * @return void + */ + public function removeInvites($gid, $emails) + { + foreach ($emails as $email) + { + $model = self::all() + ->whereEquals('gidNumber', $gid) + ->whereEquals('email', $email) + ->row(); + + if ($model->get('id')) + { + $model->destroy(); + } + } + } +} diff --git a/core/libraries/Hubzero/User/Helper.php b/core/libraries/Hubzero/User/Helper.php new file mode 100644 index 00000000000..90aa1737b22 --- /dev/null +++ b/core/libraries/Hubzero/User/Helper.php @@ -0,0 +1,450 @@ +quote($domain) . ';'; + $db->setQuery($query); + + $result = $db->loadObject(); + + if (empty($result)) + { + return false; + } + + return $result->domain_id; + } + + /** + * Get domain user ID + * + * @param string $domain_username + * @param string $domain + * @return mixed + */ + public static function getXDomainUserId($domain_username, $domain) + { + $db = \App::get('db'); + + if (empty($domain) || ($domain == 'hubzero')) + { + return $domain_username; + } + + $query = 'SELECT uidNumber FROM #__xdomain_users,#__xdomains WHERE ' . + '#__xdomains.domain_id=#__xdomain_users.domain_id AND ' . + '#__xdomains.domain=' . $db->quote($domain) . ' AND ' . + '#__xdomain_users.domain_username=' . $db->quote($domain_username); + $db->setQuery($query); + + $result = $db->loadObject(); + + if (empty($result)) + { + return false; + } + + return $result->uidNumber; + } + + /** + * Delete a record by user ID + * + * @param integer $id + * @return boolean + */ + public static function deleteXDomainUserId($id) + { + $db = \App::get('db'); + + if (empty($id)) + { + return false; + } + + $id = intval($id); + + if ($id <= 0) + { + return false; + } + + $query = 'DELETE FROM `#__xdomain_users` WHERE uidNumber=' . $db->quote($id) . ';'; + + $db->setQuery($query); + + $db->query(); + + return true; + } + + /** + * Check if a user has a domain record + * + * @param integer $uid + * @return boolean + */ + public static function isXDomainUser($uid) + { + $db = \App::get('db'); + + $query = 'SELECT uidNumber FROM `#__xdomain_users` WHERE #__xdomain_users.uidNumber=' . $db->quote($uid); + + $db->setQuery($query); + + $result = $db->loadObject(); + + if (empty($result)) + { + return false; + } + + return true; + } + + /** + * Creeate a domain record + * + * @param string $domain + * @return boolean + */ + public static function createXDomain($domain) + { + $db = \App::get('db'); + + if (empty($domain) || ($domain == 'hubzero')) + { + return false; + } + + $query = 'SELECT domain_id FROM `#__xdomains` WHERE ' . + '#__xdomains.domain=' . $db->quote($domain); + + $db->setQuery($query); + + $result = $db->loadObject(); + + if (empty($result)) + { + $query = 'INSERT INTO `#__xdomains` (domain) VALUES (' . $db->quote($domain) . ')'; + + $db->setQuery($query); + + $db->query(); + + $domain_id = $db->insertid(); + } + else + { + $domain_id = $result->domain_id; + } + + return $domain_id; + } + + /** + * Set a domain for a user + * + * @param string $domain_username + * @param string $domain + * @param integer $uidNumber + * @return bool + */ + public static function setXDomainUserId($domain_username, $domain, $uidNumber) + { + return self::mapXDomainUser($domain_username, $domain, $uidNumber); + } + + /** + * Map a domain to a user + * + * @param string $domain_username + * @param string $domain + * @param integer $uidNumber + * @return boolean + */ + public static function mapXDomainUser($domain_username, $domain, $uidNumber) + { + $db = \App::get('db'); + + if (empty($domain)) + { + return 0; + } + + $query = 'SELECT domain_id FROM `#__xdomains` WHERE ' . + '#__xdomains.domain=' . $db->quote($domain); + + $db->setQuery($query); + + $result = $db->loadObject(); + + if (empty($result)) + { + $query = 'INSERT INTO `#__xdomains` (domain) VALUES (' . $db->quote($domain) . ')'; + + $db->setQuery($query); + + $db->query(); + + $domain_id = $db->insertid(); + } + else + { + $domain_id = $result->domain_id; + } + + $query = 'INSERT INTO `#__xdomain_users` (domain_id,domain_username,uidNumber) ' . + ' VALUES (' . $db->quote($domain_id) . ',' . + $db->quote($domain_username) . ',' . $db->quote($uidNumber) . ')'; + + $db->setQuery($query); + + if (!$db->query()) + { + return false; + } + + return true; + } + + /** + * Get a list of groups for a user + * + * @param string $uid + * @param string $type + * @param string $cat + * @return boolean + */ + public static function getGroups($uid, $type='all', $cat = null) + { + $db = \App::get('db'); + + $g = ''; + if ($cat == 1) + { + $g .= "(g.type='".$cat."' OR g.type='3') AND"; + } + elseif ($cat !== null) + { + $g .= "g.type=" . $db->quote($cat) . " AND "; + } + + // Get all groups the user is a member of + $query1 = "SELECT g.gidNumber, g.published, g.approved, g.cn, g.description, g.join_policy, '1' AS registered, '0' AS regconfirmed, '0' AS manager FROM #__xgroups AS g, #__xgroups_applicants AS m WHERE $g m.gidNumber=g.gidNumber AND m.uidNumber=".$uid; + $query2 = "SELECT g.gidNumber, g.published, g.approved, g.cn, g.description, g.join_policy, '1' AS registered, '1' AS regconfirmed, '0' AS manager FROM #__xgroups AS g, #__xgroups_members AS m WHERE $g m.gidNumber=g.gidNumber AND m.uidNumber=".$uid; + $query3 = "SELECT g.gidNumber, g.published, g.approved, g.cn, g.description, g.join_policy, '1' AS registered, '1' AS regconfirmed, '1' AS manager FROM #__xgroups AS g, #__xgroups_managers AS m WHERE $g m.gidNumber=g.gidNumber AND m.uidNumber=".$uid; + $query4 = "SELECT g.gidNumber, g.published, g.approved, g.cn, g.description, g.join_policy, '0' AS registered, '1' AS regconfirmed, '0' AS manager FROM #__xgroups AS g, #__xgroups_invitees AS m WHERE $g m.gidNumber=g.gidNumber AND m.uidNumber=".$uid; + + switch ($type) + { + case 'all': + $query = "( $query1 ) UNION ( $query2 ) UNION ( $query3 ) UNION ( $query4 )"; + break; + case 'applicants': + $query = $query1." ORDER BY description, cn"; + break; + case 'members': + $query = $query2." ORDER BY description, cn"; + break; + case 'managers': + $query = $query3." ORDER BY description, cn"; + break; + case 'invitees': + $query = $query4." ORDER BY description, cn"; + break; + } + + $db->setQuery($query); + + $result = $db->loadObjectList(); + + if (empty($result)) + { + return false; + } + + return $result; + } + + /** + * Remove User From Groups + * + * @param integer $uid + * @return boolean + */ + public static function removeUserFromGroups($uid) + { + $db = \App::get('db'); + $tables = array('#__xgroups_members', '#__xgroups_managers', '#__xgroups_invitees', '#__xgroups_applicants'); + + foreach ($tables as $table) + { + $sql = "DELETE FROM `".$table."` WHERE uidNumber=" . $db->quote($uid); + $db->setQuery($sql); + $db->query(); + } + + return true; + } + + /** + * Get courses for a user + * + * @param string $uid + * @param string $type + * @param string $cat + * @return boolean + */ + public static function getCourses($uid, $type='all', $cat = null) + { + $db = \App::get('db'); + + $g = ''; + if ($cat == 1) { + $g .= "(g.type='".$cat."' OR g.type='3') AND"; + } + + // Get all courses the user is a member of + $query1 = "SELECT g.id, g.state, g.alias, g.title, g.join_policy, '1' AS registered, '0' AS regconfirmed, '0' AS manager FROM #__courses AS g, #__courses_applicants AS m WHERE $g m.course_id=g.id AND m.user_id=".$uid; + $query2 = "SELECT g.id, g.state, g.alias, g.title, g.join_policy, '1' AS registered, '1' AS regconfirmed, '0' AS manager FROM #__courses AS g, #__courses_members AS m WHERE $g m.course_id=g.id AND m.user_id=".$uid; + $query3 = "SELECT g.id, g.state, g.alias, g.title, g.join_policy, '1' AS registered, '1' AS regconfirmed, '1' AS manager FROM #__courses AS g, #__courses_managers AS m WHERE $g m.course_id=g.id AND m.user_id=".$uid; + $query4 = "SELECT g.id, g.state, g.alias, g.title, g.join_policy, '0' AS registered, '1' AS regconfirmed, '0' AS manager FROM #__courses AS g, #__courses_invitees AS m WHERE $g m.course_id=g.id AND m.user_id=".$uid; + + switch ($type) + { + case 'all': + $query = "( $query1 ) UNION ( $query2 ) UNION ( $query3 ) UNION ( $query4 )"; + break; + case 'applicants': + $query = $query1." ORDER BY title, alias"; + break; + case 'members': + $query = $query2." ORDER BY title, alias"; + break; + case 'managers': + $query = $query3." ORDER BY title, alias"; + break; + case 'invitees': + $query = $query4." ORDER BY title, alias"; + break; + } + + $db->setQuery($query); + + $result = $db->loadObjectList(); + + if (empty($result)) + { + return false; + } + + return $result; + } + + /** + * Get common groups between two users + * + * @param integer $uid + * @param integer $pid + * @return boolean + */ + public static function getCommonGroups($uid, $pid) + { + // Get the groups the visiting user + $xgroups = self::getGroups($uid, 'all', 1); + + $usersgroups = array(); + if (!empty($xgroups)) + { + foreach ($xgroups as $group) + { + if ($group->regconfirmed) + { + $usersgroups[] = $group->cn; + } + } + } + + // Get the groups of the profile + $pgroups = self::getGroups($pid, 'all', 1); + + // Get the groups the user has access to + $profilesgroups = array(); + if (!empty($pgroups)) + { + foreach ($pgroups as $group) + { + if ($group->regconfirmed) + { + $profilesgroups[] = $group->cn; + } + } + } + + // Find the common groups + return array_intersect($usersgroups, $profilesgroups); + } +} diff --git a/core/libraries/Hubzero/User/Log/Auth.php b/core/libraries/Hubzero/User/Log/Auth.php new file mode 100644 index 00000000000..ff1bae8f747 --- /dev/null +++ b/core/libraries/Hubzero/User/Log/Auth.php @@ -0,0 +1,57 @@ +toSql(); + } + + /** + * Generates automatic source ip + * + * @param array $data The data being saved + * @return string + **/ + public function automaticIp($data) + { + return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; + } +} diff --git a/core/libraries/Hubzero/User/Logger.php b/core/libraries/Hubzero/User/Logger.php new file mode 100644 index 00000000000..41077e17971 --- /dev/null +++ b/core/libraries/Hubzero/User/Logger.php @@ -0,0 +1,48 @@ +user = $user; + } + + /** + * Defines a one to many relationship between users and auth log entries + * + * @return object \Hubzero\Database\Relationship\OneToMany + * @since 2.0.0 + */ + public function auth() + { + return $this->user->oneToMany('Hubzero\User\Log\Auth'); + } +} diff --git a/core/libraries/Hubzero/User/Manager.php b/core/libraries/Hubzero/User/Manager.php new file mode 100644 index 00000000000..249e528c304 --- /dev/null +++ b/core/libraries/Hubzero/User/Manager.php @@ -0,0 +1,165 @@ +app = $app; + } + + /** + * Get the default user + * + * @return object + */ + public function getCurrentUser() + { + $instance = $this->app['session']->get('user'); + + if (!($instance instanceof User)) + { + $instance = new User; + } + + return $instance; + } + + /** + * Get a user instance + * + * @param mixed $id Integer or string + * @return object + */ + public function getInstance($id = null) + { + $current = $this->getCurrentUser(); + + // Does the ID match to the current user? + // If so, we already have that info and can just return it + if (is_null($id)) + { + return $current; + } + + if (is_numeric($id)) + { + // Cast as an integer so we can do an exact (===) comparison below + $id = (int)$id; + } + + if ($id === (int)$current->get('id') + || $id === (string)$current->get('username')) + { + return $current; + } + + // Is the ID numeric? + // If not, let's try to resolve by username or email address. + if (!is_numeric($id)) + { + $user = $this->resolveUser($id); + $id = $user->get('id', 0); + + if ($id) + { + $this->users[$id] = $user; + } + } + + // If the $id is zero, just return an empty User. + // Note: don't cache this user because it'll have a new ID on save! + if ($id === 0) + { + return new User; + } + + // If the given user has not been created before, we will create the instance + // here and cache it so we can return it next time very quickly. If there is + // already a user created for this ID, we'll just return that instance. + if (!isset($this->users[$id])) + { + $this->users[$id] = $this->resolveUser($id); + } + + return $this->users[$id]; + } + + /** + * Create a new user instance. + * + * @param mixed $id + * @return object + */ + protected function resolveUser($id) + { + if (!is_numeric($id)) + { + if (strstr($id, '@')) + { + $user = User::oneByEmail($id); + } + else + { + $user = User::oneByUsername($id); + } + } + else + { + $user = User::oneOrNew($id); + } + + return $user; + } + + /** + * Get all of the created "users". + * + * @return array + */ + public function getUsers() + { + return $this->users; + } + + /** + * Dynamically call the router instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return call_user_func_array(array($this->getInstance(), $method), $parameters); + } +} diff --git a/core/libraries/Hubzero/User/Password.php b/core/libraries/Hubzero/User/Password.php new file mode 100644 index 00000000000..3cf3cb40ddf --- /dev/null +++ b/core/libraries/Hubzero/User/Password.php @@ -0,0 +1,1093 @@ +_updatedkeys = array(); + + foreach ($cvars as $key => $value) + { + if ($key{0} != '_') + { + unset($this->$key); + + $this->$key = null; + } + } + + $this->_updatedkeys = array(); + } + + /** + * Short description for 'getInstance' Long description (if any) . .. + * + * @param unknown $instance Parameter description (if any) ... + * @param unknown $storage Parameter description (if any) ... + * @return mixed Return description (if any) ... + */ + public static function getInstance($instance, $storage = null) + { + $hzup = new self(); + + if ($hzup->read($instance) === false) + { + return false; + } + + return $hzup; + } + + /** + * Create a record + * + * @return boolean + */ + public function create() + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + // @FIXME: this should fail if id doesn't exist in #__users + if ($this->user_id > 0) + { + $query = "INSERT INTO #__users_password (user_id) VALUES ( " . $db->quote($this->user_id) . ");"; + + $db->setQuery($query); + + $result = $db->query(); + + if ($result !== false || $db->getErrorNum() == 1062) + { + return true; + } + + $this->update(); + } + + return false; + } + + /** + * Read a record + * + * @param integer $instance + * @return boolean + */ + public function read($instance = null) + { + if (empty($instance)) + { + $instance = $this->user_id; + } + + if (empty($instance)) + { + return false; + } + + $this->clear(); + + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + $result = true; + + if (is_numeric($instance)) + { + if ($instance <= 0) + { + return false; + } + + $query = "SELECT user_id,passhash,shadowLastChange,shadowMin," . "shadowMax,shadowWarning,shadowInactive,shadowExpire," . "shadowFlag FROM #__users_password WHERE user_id=" . $db->quote($instance) . ";"; + } + else + { + $query = "SELECT user_id,passhash,shadowLastChange,shadowMin," . "shadowMax,shadowWarning,shadowInactive,shadowExpire," . "shadowFlag FROM #__users_password,#__users WHERE user_id=id" . " AND username=" . $db->quote($instance) . ";"; + } + + $db->setQuery($query); + + $result = $db->loadAssoc(); + + if (!empty($result)) + { + foreach ($result as $key => $value) + { + $this->__set($key, $value); + } + + $this->_updatedkeys = array(); + } + else + { + $hzp = Profile::getInstance($instance); + + if (is_object($hzp)) + { + $this->__set('user_id', $hzp->get('uidNumber')); + $this->__set('passhash', $hzp->get('userPassword')); + $this->create(); + } + else + { + return false; + } + } + + return true; + } + + /** + * Update a record + * + * @return boolean + */ + public function update() + { + $db = \App::get('db'); + + if (!$this->__get('user_id')) + { + return false; + } + + $query = "UPDATE `#__users_password` SET "; + + $classvars = get_class_vars(__CLASS__); + + $first = true; + + foreach ($classvars as $property => $value) + { + if (($property{0} == '_')) + { + continue; + } + + if (!in_array($property, $this->_updatedkeys)) + { + continue; + } + + if (!$first) + { + $query .= ','; + } + else + { + $first = false; + } + + $value = $this->__get($property); + + if ($value === null) + { + $query .= "`$property`=NULL"; + } + else + { + $query .= "`$property`=" . $db->quote($value); + } + } + + $query .= " WHERE `user_id`=" . $db->quote($this->__get('user_id')); + + if ($first == true) + { + $query = ''; + } + + $affected = 0; + if (!empty($query)) + { + $db->setQuery($query); + + $result = $db->query(); + + if ($result) + { + $affected = $db->getAffectedRows(); + } + } + + if ($affected > 0) + { + \Event::trigger('user.onAfterStorePassword', array($this)); + } + + return true; + } + + /** + * Delete a record + * + * @return boolean + */ + public function delete() + { + if ($this->user_id <= 0) + { + return false; + } + + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + if (!isset($this->user_id)) + { + $db->setQuery("SELECT user_id FROM `#__users_password` WHERE user_id" . $db->quote($this->user_id) . ";"); + + $this->__set('user_id', $db->loadResult()); + } + + if (empty($this->user_id)) + { + return false; + } + + $db->setQuery("DELETE FROM `#__users_password` WHERE user_id= " . $db->quote($this->user_id) . ";"); + + $affected = 0; + + if ($db->query()) + { + $affected = $db->getAffectedRows(); + } + + if ($affected > 0) + { + \Event::trigger('user.onAfterDeletePassword', array($this)); + } + + return true; + } + + /** + * Get a property's value + * + * @param string $property + * @return mixed + */ + public function __get($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + if (isset($this->$property)) + { + return $this->$property; + } + + if (array_key_exists($property, get_object_vars($this))) + { + return null; + } + + $this->_error("Undefined property " . __CLASS__ . "::$" . $property, E_USER_NOTICE); + + return null; + } + + /** + * Set a property's value + * + * @param string $property + * @param mixed $value + * @return void + */ + public function __set($property = null, $value = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + $this->$property = $value; + + if (!in_array($property, $this->_updatedkeys)) + { + $this->_updatedkeys[] = $property; + } + } + + /** + * Check if a property is set + * + * @param string $property + * @return bool + */ + public function __isset($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + return isset($this->$property); + } + + /** + * Unset a property + * + * @param string $property + * @return void + */ + public function __unset($property = null) + { + if (!property_exists(__CLASS__, $property) || $property{0} == '_') + { + if (empty($property)) + { + $property = '(null)'; + } + + $this->_error("Cannot access property " . __CLASS__ . "::$" . $property, E_USER_ERROR); + die(); + } + + $this->_updatedkeys = array_diff($this->_updatedkeys, array($property)); + + unset($this->$property); + } + + /** + * Output an error message + * + * @param string $message + * @param integer $level + * @return void + */ + private function _error($message, $level = E_USER_NOTICE) + { + $caller = next(debug_backtrace()); + + switch ($level) + { + case E_USER_NOTICE: + echo "Notice: "; + break; + case E_USER_ERROR: + echo "Fatal error: "; + break; + default: + echo "Unknown error: "; + break; + } + + echo $message . ' in ' . $caller['file'] . ' on line ' . $caller['line'] . "\n"; + } + + /** + * Get a property's value + * + * @param string $property + * @return mixed + */ + public function get($key) + { + return $this->__get($key); + } + + /** + * Set a property's value + * + * @param string $property + * @param mixed $value + * @return void + */ + public function set($key, $value) + { + return $this->__set($key, $value); + } + + /** + * Check if a password is expired + * + * @param mixed $user + * @return bool + */ + public static function isPasswordExpired($user = null) + { + $hzup = self::getInstance($user); + + if (!is_object($hzup)) + { + return false; + } + + if (empty($hzup->shadowLastChange)) + { + return false; + } + + if ($hzup->shadowMax === '0') + { + return true; + } + + if (empty($hzup->shadowMax)) + { + return false; + } + + $chgtime = time(); + $chgtime = intval($chgtime / 86400); + + if (($hzup->shadowLastChange + $hzup->shadowMax) >= $chgtime) + { + return false; + } + + return true; + } + + /** + * Get a hash of a password + * + * @param string $password + * @return string + */ + public static function getPasshash($password) + { + // Get the password encryption/hashing mechanism + $config = \Component::params('com_members'); + $type = $config->get('passhash_mechanism', 'CRYPT_SHA512'); + + switch ($type) + { + case 'MD5': + $passhash = "{MD5}" . base64_encode(pack('H*', md5($password))); + break; + + case 'CRYPT_SHA512': + default: + $encrypted = crypt($password, '$6$' . self::genRandomPassword(8) . '$'); + $passhash = "{CRYPT}" . $encrypted; + break; + } + + return $passhash; + } + + /** + * Generate a random password + * + * @param integer $length Length of the password to generate + * @return string Random Password + */ + public static function genRandomPassword($length = 8) + { + $salt = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + $base = strlen($salt); + $makepass = ''; + + // Start with a cryptographic strength random string, then convert it to + // a string with the numeric base of the salt. + // Shift the base conversion on each character so the character + // distribution is even, and randomize the start shift so it's not + // predictable. + $random = \Hubzero\Encryption\Encrypter::genRandomBytes($length + 1); + $shift = ord($random[0]); + for ($i = 1; $i <= $length; ++$i) + { + $makepass .= $salt[($shift + ord($random[$i])) % $base]; + $shift += ord($random[$i]); + } + + return $makepass; + } + + /** + * Change a user's password + * + * @param mixed $user + * @param string $password + * @return bool + */ + public static function changePassword($user = null, $password) + { + $passhash = self::getPasshash($password); + + return self::changePasshash($user, $passhash); + } + + /** + * Change a user's pass hash + * + * @param mixed $user + * @param string $password + * @return bool + */ + public static function changePasshash($user = null, $passhash) + { + // Get config values for min, max, and warning + $config = \Component::params('com_members'); + $shadowMin = $config->get('shadowMin', '0'); + $shadowMax = $config->get('shadowMax', null); + $shadowWarning = $config->get('shadowWarning', '7'); + + // Translate empty shadowMax to mean NULL + $shadowMax = ($shadowMax == '') ? null : $shadowMax; + + $hzup = self::getInstance($user); + + // Make sure we found a record + if (!$hzup) + { + return false; + } + + $oldhash = $hzup->__get('passhash'); + + $hzup->__set('passhash', $passhash); + $hzup->__set('shadowFlag', null); + $hzup->__set('shadowLastChange', intval(time() / 86400)); + $hzup->__set('shadowMin', $shadowMin); + $hzup->__set('shadowMax', $shadowMax); + $hzup->__set('shadowWarning', $shadowWarning); + $hzup->__set('shadowInactive', '0'); + $hzup->__set('shadowExpire', null); + $hzup->update(); + + $db = \App::get('db'); + + $db->setQuery("UPDATE `#__xprofiles` SET userPassword=" . $db->quote($passhash) . " WHERE uidNumber=" . $db->quote($hzup->get('user_id'))); + $db->query(); + + $db->setQuery("UPDATE `#__users` SET password=" . $db->quote($passhash) . " WHERE id=" . $db->quote($hzup->get('user_id'))); + $db->query(); + + if (!empty($oldhash)) + { + History::addPassword($oldhash, $user); + } + + return true; + } + + /** + * Compare passwords + * + * @param string $passhash + * @param string $password + * @return bool + */ + public static function comparePasswords($passhash, $password) + { + if (empty($passhash) || empty($password)) + { + return false; + } + + preg_match("/^\s*(\{(.*)\}\s*|)((.*?)\s*:\s*|)(.*?)\s*$/", $passhash, $matches); + + $encryption = strtolower($matches[2]); + + if (empty($encryption)) + { + // Joomla + $encryption = "md5-hex"; + + if (!empty($matches[4])) + { + // Joomla 1.5 + $crypt = $matches[4]; + $salt = $matches[5]; + } + else + { + // Joomla 1.0 + $crypt = $matches[5]; + $salt = ''; + } + } + else + { + $salt = $matches[4]; + $crypt = $matches[5]; + } + + if ($encryption == 'md5') + { + $encryption = "md5-base64"; + } + else if ($encryption == 'crypt') + { + preg_match('/\$([[:alnum:]]{1,2})\$[[:alnum:]]{8}\$/', $passhash, $parts); + $salt = $parts[0]; + + switch ($parts[1]) + { + case '6': + default: + $encryption = 'crypt-sha512'; + break; + } + } + + if (empty($salt) && ($encryption == 'ssha')) + { + $salt = substr(base64_decode(substr($crypt, -32)), -4); + $hashed = base64_encode(mhash(MHASH_SHA1, $password . $salt) . $salt); + } + else + { + $hashed = self::_getCryptedPassword($password, $salt, $encryption); + } + + return ($crypt == $hashed); + } + + /** + * Formats a password using the current encryption. + * + * @param string $plaintext The plaintext password to encrypt. + * @param string $salt The salt to use to encrypt the password. If not present, a new salt will be generated. + * @param string $encryption The kind of password encryption to use. Defaults to md5-hex. + * @param boolean $show_encrypt Some password systems prepend the kind of encryption to the crypted password ({SHA}, etc). Defaults to false. + * @return string The encrypted password. + */ + protected static function _getCryptedPassword($plaintext, $salt = '', $encryption = 'md5-hex', $show_encrypt = false) + { + // Get the salt to use. + $salt = self::getSalt($encryption, $salt, $plaintext); + + // Encrypt the password. + switch ($encryption) + { + case 'plain': + return $plaintext; + + case 'sha': + $encrypted = base64_encode(hash('SHA1', $plaintext, true)); + return ($show_encrypt) ? '{SHA}' . $encrypted : $encrypted; + + case 'crypt': + case 'crypt-des': + case 'crypt-md5': + case 'crypt-blowfish': + case 'crypt-sha512': + return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt); + + case 'md5-base64': + $encrypted = base64_encode(hash('MD5', $plaintext, true)); + return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted; + + case 'ssha': + $encrypted = base64_encode(hash('SHA1', $plaintext . $salt, true) . $salt); + return ($show_encrypt) ? '{SSHA}' . $encrypted : $encrypted; + + case 'smd5': + $encrypted = base64_encode(hash('MD5', $plaintext . $salt, true) . $salt); + return ($show_encrypt) ? '{SMD5}' . $encrypted : $encrypted; + + case 'aprmd5': + $length = strlen($plaintext); + $context = $plaintext . '$apr1$' . $salt; + $binary = self::_bin(md5($plaintext . $salt . $plaintext)); + + for ($i = $length; $i > 0; $i -= 16) + { + $context .= substr($binary, 0, ($i > 16 ? 16 : $i)); + } + for ($i = $length; $i > 0; $i >>= 1) + { + $context .= ($i & 1) ? chr(0) : $plaintext[0]; + } + + $binary = self::_bin(md5($context)); + + for ($i = 0; $i < 1000; $i++) + { + $new = ($i & 1) ? $plaintext : substr($binary, 0, 16); + if ($i % 3) + { + $new .= $salt; + } + if ($i % 7) + { + $new .= $plaintext; + } + $new .= ($i & 1) ? substr($binary, 0, 16) : $plaintext; + $binary = self::_bin(md5($new)); + } + + $p = array(); + for ($i = 0; $i < 5; $i++) + { + $k = $i + 6; + $j = $i + 12; + if ($j == 16) + { + $j = 5; + } + $p[] = self::_toAPRMD5((ord($binary[$i]) << 16) | (ord($binary[$k]) << 8) | (ord($binary[$j])), 5); + } + + return '$apr1$' . $salt . '$' . implode('', $p) . self::_toAPRMD5(ord($binary[11]), 3); + + case 'md5-hex': + default: + $encrypted = ($salt) ? md5($plaintext . $salt) : md5($plaintext); + return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted; + } + } + + /** + * Returns a salt for the appropriate kind of password encryption. + * Optionally takes a seed and a plaintext password, to extract the seed + * of an existing password, or for encryption types that use the plaintext + * in the generation of the salt. + * + * @param string $encryption The kind of password encryption to use. Defaults to md5-hex. + * @param string $seed The seed to get the salt from (probably a previously generated password). Defaults to generating a new seed. + * @param string $plaintext The plaintext password that we're generating a salt for. Defaults to none. + * @return string The generated or extracted salt. + */ + public static function getSalt($encryption = 'md5-hex', $seed = '', $plaintext = '') + { + // Encrypt the password. + switch ($encryption) + { + case 'crypt': + case 'crypt-des': + if ($seed) + { + return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 2); + } + else + { + return substr(md5(mt_rand()), 0, 2); + } + break; + + case 'crypt-md5': + if ($seed) + { + return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 12); + } + else + { + return '$1$' . substr(md5(mt_rand()), 0, 8) . '$'; + } + break; + + case 'crypt-blowfish': + if ($seed) + { + return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 16); + } + else + { + return '$2$' . substr(md5(mt_rand()), 0, 12) . '$'; + } + break; + + case 'crypt-sha512': + if ($seed) + { + return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 12); + } + else + { + return '$6$' . substr(md5(mt_rand()), 0, 8) . '$'; + } + break; + + case 'ssha': + if ($seed) + { + return substr(preg_replace('|^{SSHA}|', '', $seed), -20); + } + else + { + return mhash_keygen_s2k(MHASH_SHA1, $plaintext, substr(pack('h*', md5(mt_rand())), 0, 8), 4); + } + break; + + case 'smd5': + if ($seed) + { + return substr(preg_replace('|^{SMD5}|', '', $seed), -16); + } + else + { + return mhash_keygen_s2k(MHASH_MD5, $plaintext, substr(pack('h*', md5(mt_rand())), 0, 8), 4); + } + break; + + case 'aprmd5': + /* 64 characters that are valid for APRMD5 passwords. */ + $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + if ($seed) + { + return substr(preg_replace('/^\$apr1\$(.{8}).*/', '\\1', $seed), 0, 8); + } + else + { + $salt = ''; + for ($i = 0; $i < 8; $i++) + { + $salt .= $APRMD5{rand(0, 63)}; + } + return $salt; + } + break; + + default: + $salt = ''; + if ($seed) + { + $salt = $seed; + } + return $salt; + break; + } + } + + /** + * Converts to allowed 64 characters for APRMD5 passwords. + * + * @param string $value The value to convert. + * @param integer $count The number of characters to convert. + * @return string $value converted to the 64 MD5 characters. + */ + protected static function _toAPRMD5($value, $count) + { + // 64 characters that are valid for APRMD5 passwords. + $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + $aprmd5 = ''; + $count = abs($count); + while (--$count) + { + $aprmd5 .= $APRMD5[$value & 0x3f]; + $value >>= 6; + } + return $aprmd5; + } + + /** + * Converts hexadecimal string to binary data. + * + * @param string $hex Hex data. + * @return string Binary data. + */ + protected static function _bin($hex) + { + $bin = ''; + $length = strlen($hex); + for ($i = 0; $i < $length; $i += 2) + { + $tmp = sscanf(substr($hex, $i, 2), '%x'); + $bin .= chr(array_shift($tmp)); + } + return $bin; + } + + /** + * Check if a password matches + * + * @param mixed $user + * @param string $password + * @param bool $alltables + * @return bool + */ + public static function passwordMatches($user = null, $password, $alltables = false) + { + $passhash = null; + + $hzup = self::getInstance($user); + + if (is_object($hzup) && !empty($hzup->passhash)) + { + $passhash = $hzup->passhash; + } + else if ($alltables) + { + $profile = Profile::getInstance($user); + + if (is_object($profile) && ($profile->get('userPassword') != '')) + { + $passhash = $profile->get('userPassword'); + } + else + { + $user = \User::getInstance($user); + + if (is_object($user) && !empty($user->password)) + { + $passhash = $user->password; + } + } + } + + return self::comparePasswords($passhash, $password); + } + + /** + * Invalidate a user's password + * + * @param mixed $user + * @return bool + */ + public static function invalidatePassword($user = null) + { + $hzup = self::getInstance($user); + + $hzup->__set('shadowFlag', '-1'); + $hzup->update(); + + return true; + } + + /** + * Expire a user's password + * + * @param mixed $user + * @return bool + */ + public static function expirePassword($user = null) + { + $hzup = self::getInstance($user); + + $hzup->__set('shadowLastChange', '1'); + $hzup->__set('shadowMax', '0'); + $hzup->update(); + + return true; + } +} diff --git a/core/libraries/Hubzero/User/Password/History.php b/core/libraries/Hubzero/User/Password/History.php new file mode 100644 index 00000000000..43d22a6da63 --- /dev/null +++ b/core/libraries/Hubzero/User/Password/History.php @@ -0,0 +1,215 @@ +logger('debug'); + $xlog->debug($msg); + } + + /** + * Get an instance of a user's password History + * + * @param mixed $instance User ID (integer) or username (string) + * @return object + */ + public static function getInstance($instance) + { + $db = \App::get('db'); + + if (empty($db)) { + return false; + } + + $hzph = new self(); + + if (is_numeric($instance) && $instance > 0) + { + $hzph->user_id = $instance; + } + else + { + $query = "SELECT id FROM `#__users` WHERE username=" . $db->quote($instance) . ";"; + $db->setQuery($query); + $result = $db->loadResult(); + if (is_numeric($result) && $result > 0) + { + $hzph->user_id = $result; + } + } + + if (empty($hzph->user_id)) + { + return false; + } + + return $hzph; + } + + /** + * Add a passhash to a user's history + * + * @param string $passhash + * @param string $invalidated + * @return boolean + */ + public function add($passhash = null, $invalidated = null) + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + if (empty($passhash)) + { + $passhash = null; + } + + if (empty($invalidated)) + { + $invalidated = "UTC_TIMESTAMP()"; + } + else + { + $invalidated = $db->quote($invalidated); + } + + $user_id = $this->user_id; + + $query = "INSERT INTO `#__users_password_history` (user_id," . + "passhash,invalidated)" . + " VALUES ( " . + $db->quote($user_id) . "," . + $db->quote($passhash) . "," . + $invalidated . + ");"; + + $db->setQuery($query); + + $result = $db->query(); + + if ($result !== false || $db->getErrorNum() == 1062) + { + return true; + } + + return false; + } + + /** + * Check if a password exists for a user + * + * @param string $password + * @param string $since + * @return boolean + */ + public function exists($password = null, $since = null) + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + $query = "SELECT `passhash` FROM `#__users_password_history` WHERE user_id = " . $db->quote($this->user_id); + + if (!empty($since)) + { + $query .= " AND invalidated >= " . $db->quote($since); + } + + $db->setQuery($query); + + $results = $db->loadObjectList(); + + if ($results && count($results) > 0) + { + foreach ($results as $result) + { + $compare = \Hubzero\User\Password::comparePasswords($result->passhash, $password); + if ($compare) + { + return true; + } + } + } + + return false; + } + + /** + * Remove a passhash from a user's history + * + * @param string $passhash + * @param string $timestamp + * @return boolean + */ + public function remove($passhash, $timestamp) + { + if ($this->user_id <= 0) + { + return false; + } + + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + $db->setQuery( + "DELETE FROM `#__users_password_history` WHERE user_id= " . + $db->quote($this->user_id) . " AND passhash = " . + $db->quote($passhash) . " AND invalidated = " . + $db->quote($timestamp) . ";" + ); + + if (!$db->query()) + { + return false; + } + + return true; + } + + /** + * Shortcut helper method for adding + * a password to a user's history + * + * @param string $passhash + * @param string $user + * @return boolean + */ + public static function addPassword($passhash, $user = null) + { + $hzuph = self::getInstance($user); + $hzuph->add($passhash); + + return true; + } +} diff --git a/core/libraries/Hubzero/User/Picture/File.php b/core/libraries/Hubzero/User/Picture/File.php new file mode 100644 index 00000000000..4f0d09c526a --- /dev/null +++ b/core/libraries/Hubzero/User/Picture/File.php @@ -0,0 +1,90 @@ +pictureName = $config['pictureName']; + } + + if (array_key_exists('thumbnailName', $config)) + { + $this->thumbnailName = $config['thumbnailName']; + } + + if (array_key_exists('path', $config)) + { + $this->path = $config['path']; + } + } + + /** + * Get a path or URL to a user pciture + * + * @param integer $id + * @param string $name + * @param string $email + * @param bool $thumbnail + * @return string + */ + public function picture($id, $name, $email, $thumbnail = true) + { + $file = $this->pictureName; + + if ($thumbnail) + { + $file = $this->thumbnailName; + } + + $path = $this->path . DIRECTORY_SEPARATOR . Str::pad($id, 5) . DIRECTORY_SEPARATOR . $file; + + if (file_exists($path)) + { + return with(new Moderator($path, 'public'))->getUrl(); + } + + return ''; + } +} diff --git a/core/libraries/Hubzero/User/Picture/Gravatar.php b/core/libraries/Hubzero/User/Picture/Gravatar.php new file mode 100644 index 00000000000..3da2a985b9b --- /dev/null +++ b/core/libraries/Hubzero/User/Picture/Gravatar.php @@ -0,0 +1,236 @@ + false, + 'fallback' => false, + 'forceExtension' => false, + 'forceDefault' => false, + 'maximumRating' => null, + 'size' => null + ); + + /** + * Constructor + * + * @param array $config + * @return void + */ + public function __construct($config=array()) + { + foreach ($this->config as $key => $val) + { + if (array_key_exists($key, $config)) + { + if ($key == 'fallback') + { + if (filter_var($config['fallback'], FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) + || in_array($config['fallback'], array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank'))) + { + $this->set('fallback', $config['fallback']); + } + } + + $this->set($key, $config[$key]); + } + } + } + + /** + * Helper function to set config values + * + * @param string $key + * @param mixed $value + * @return object + */ + public function set($key, $value) + { + $this->config[$key] = $value; + + return $this; + } + + /** + * Helper function to retrieve config values + * + * @param string $value + * @param mixed $default + * @return mixed + */ + public function get($value, $default = null) + { + return array_key_exists($value, $this->config) ? $this->config[$value] : $default; + } + + /** + * Get a path or URL to a user pciture + * + * @param integer $id + * @param string $name + * @param string $email + * @param bool $thumbnail + * @return string + */ + public function picture($id, $name, $email, $thumbnail = true) + { + if ($thumbnail) + { + $this->set('size', 300); + } + + $url = $this->get('secure') === true ? $this->secureBaseUrl : $this->publicBaseUrl; + $url .= htmlspecialchars($this->hash($email)); + $url .= $this->extension(); + $url .= $this->parameters(); + + return $url; + } + + /** + * Helper function to hash an email address. + * + * @param string $email + * @return string + * @throws InvalidEmailException + */ + public function hash($email) + { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) + { + throw new InvalidEmailException('Please specify a valid email address'); + } + + return md5(strtolower(trim($email))); + } + + /** + * Force file extension + * + * @return string + */ + public function extension() + { + $v = $this->get('forceExtension'); + + return $v ? '.' . $v : ''; + } + + /** + * Get querystring of parameters + * + * @return string + */ + public function parameters() + { + $build = array(); + + foreach (get_class_methods($this) as $method) + { + if (substr($method, -strlen('Parameter')) !== 'Parameter') + { + continue; + } + + if ($called = call_user_func(array($this, $method))) + { + $build = array_replace($build, $called); + } + } + + return '?' . http_build_query($build); + } + + /** + * Get size parameter + * + * @return array|null + */ + public function sizeParameter() + { + if (!$this->get('size') || !is_integer($this->get('size'))) + { + return null; + } + + return array('s' => (int)$this->get('size')); + } + + /** + * Get fallback image URL + * + * @return array|null + */ + public function fallbackParameter() + { + $fallback = $this->get('fallback'); + + if (!$fallback) + { + return null; + } + + return array('d' => $fallback); + } + + /** + * Get rating + * + * @return mixed array|null + */ + public function ratingParameter() + { + $rating = $this->get('maximumRating'); + + if (!$rating || !in_array($rating, array('g','pg','r','x'))) + { + return null; + } + + return array('r' => $rating); + } + + /** + * Force default? + * + * @return mixed array|null + */ + public function forceDefaultParameter() + { + if ($this->get('forceDefault') === true) + { + return array('forcedefault' => 'y'); + } + + return null; + } +} diff --git a/core/libraries/Hubzero/User/Picture/Identicon.php b/core/libraries/Hubzero/User/Picture/Identicon.php new file mode 100644 index 00000000000..03be9529cb2 --- /dev/null +++ b/core/libraries/Hubzero/User/Picture/Identicon.php @@ -0,0 +1,145 @@ +pictureSize = $config['pictureSize']; + } + + if (array_key_exists('pictureName', $config)) + { + $this->pictureName = $config['pictureName']; + } + + if (array_key_exists('thumbnailSize', $config)) + { + $this->thumbnailSize = $config['thumbnailSize']; + } + + if (array_key_exists('thumbnailName', $config)) + { + $this->thumbnailName = $config['thumbnailName']; + } + + if (array_key_exists('path', $config)) + { + $this->path = $config['path']; + } + + if (array_key_exists('color', $config)) + { + $this->color = $config['color']; + } + } + + /** + * Get a path or URL to a user pciture + * + * @param integer $id + * @param string $name + * @param string $email + * @param bool $thumbnail + * @return string + */ + public function picture($id, $name, $email, $thumbnail = true) + { + $processor = new Processor(); + + $size = $this->pictureSize; + $file = $this->pictureName; + + if ($thumbnail) + { + $size = $this->thumbnailSize; + $file = $this->thumbnailName; + } + + $dir = $this->path . DIRECTORY_SEPARATOR . Str::pad($id, 5) . DIRECTORY_SEPARATOR; + $path = $dir . $file; + + if (file_exists($path)) + { + return with(new Moderator($path, 'public'))->getUrl(); + } + + $image = $processor->getImageData($email, $size, $this->color); + + if (!is_dir($dir)) + { + @mkdir($dir, 0755, true); + } + @file_put_contents($path, $image); + + if (!file_exists($path)) + { + return sprintf('data:image/png;base64,%s', base64_encode($image)); + } + + return with(new Moderator($path, 'public'))->getUrl(); + } +} diff --git a/core/libraries/Hubzero/User/Picture/Initialcon.php b/core/libraries/Hubzero/User/Picture/Initialcon.php new file mode 100644 index 00000000000..c6f22739876 --- /dev/null +++ b/core/libraries/Hubzero/User/Picture/Initialcon.php @@ -0,0 +1,162 @@ +pictureSize = $config['pictureSize']; + } + + if (array_key_exists('pictureName', $config)) + { + $this->pictureName = $config['pictureName']; + } + + if (array_key_exists('thumbnailSize', $config)) + { + $this->thumbnailSize = $config['thumbnailSize']; + } + + if (array_key_exists('thumbnailName', $config)) + { + $this->thumbnailName = $config['thumbnailName']; + } + + if (array_key_exists('path', $config)) + { + $this->path = $config['path']; + } + + if (array_key_exists('color', $config)) + { + $this->color = $config['color']; + } + } + + /** + * Get a path or URL to a user pciture + * + * @param integer $id + * @param string $name + * @param string $email + * @param bool $thumbnail + * @return string + */ + public function picture($id, $name, $email, $thumbnail = true) + { + $processor = new Processor(); + + $size = $this->pictureSize; + $file = $this->pictureName; + + if ($thumbnail) + { + $size = $this->thumbnailSize; + $file = $this->thumbnailName; + } + + $dir = $this->path . DIRECTORY_SEPARATOR . Str::pad($id, 5) . DIRECTORY_SEPARATOR; + $path = $dir . $file; + + if (file_exists($path)) + { + return with(new Moderator($path, 'public'))->getUrl(); + } + + // If the name has a space + if (strstr($name, ' ')) + { + $parts = explode(' ', $name); + $first = array_shift($parts); + $last = array_pop($parts); + + $initials = substr($first, 0, 1) . substr($last, 0, 1); + } + // One word name? (e.g., "Madonna") + // Take the first two letters + else + { + $initials = substr($name, 0, 2); + } + $initials = strtoupper(trim($initials)); + + $image = $processor->getImageData($initials, $size, $this->color); + + if (!is_dir($dir)) + { + @mkdir($dir, 0755, true); + } + @file_put_contents($path, $image); + + if (!file_exists($path)) + { + return sprintf('data:image/png;base64,%s', base64_encode($image)); + } + + return with(new Moderator($path, 'public'))->getUrl(); + } +} diff --git a/core/libraries/Hubzero/User/Picture/Namedfile.php b/core/libraries/Hubzero/User/Picture/Namedfile.php new file mode 100644 index 00000000000..40bb44d12d1 --- /dev/null +++ b/core/libraries/Hubzero/User/Picture/Namedfile.php @@ -0,0 +1,156 @@ +pictureName = $config['pictureName']; + } + + if (array_key_exists('thumbnailName', $config)) + { + $this->thumbnailName = $config['thumbnailName']; + } + + if (array_key_exists('path', $config)) + { + $this->path = $config['path']; + } + } + + /** + * Get a path or URL to a user pciture + * + * @param integer $id + * @param string $name + * @param string $email + * @param bool $thumbnail + * @return string + */ + public function picture($id, $name, $email, $thumbnail = true) + { + $member = Profile::getInstance($id); + + if (!$member) + { + return ''; + } + + // If member has a picture set + if ($file = $member->get('picture')) + { + $path = $this->path . DIRECTORY_SEPARATOR . Str::pad($id, 5) . DIRECTORY_SEPARATOR; + $file = ltrim($file, DIRECTORY_SEPARATOR); + + // Does the file exist? + if ($file != 'profile.png' && file_exists($path . $file)) + { + try + { + // Attempt to rename and resize to 'profile.png' + $hi = new Processor($path . $file); + if (count($hi->getErrors()) == 0) + { + $hi->autoRotate(); + $hi->resize(400); + $hi->setImageType(IMAGETYPE_PNG); + $hi->save($path . $this->pictureName); + } + + // If we sucessfully made a 'profile.png', + // attempt to rename and resize to 'thumb.png' + if (file_exists($path . $this->pictureName)) + { + $hi = new Processor($path . $this->pictureName); + if (count($hi->getErrors()) == 0) + { + $hi->resize(50, false, true, true); + $hi->save($path . $this->thumbnailName); + } + } + } + catch (\Exception $e) + { + return ''; + } + } + + $file = $this->pictureName; + + if ($thumbnail) + { + $file = $this->thumbnailName; + } + + $path .= $file; + + if (file_exists($path)) + { + return with(new Moderator($path, 'public'))->getUrl(); + } + } + + return ''; + } + + /** + * Generate a thumbnail file name format + * example.jpg -> example_thumb.jpg + * + * @param string $thumb Filename to get thumbnail of + * @return string + */ + public static function thumbit($thumb) + { + $dot = strrpos($thumb, '.') + 1; + $ext = substr($thumb, $dot); + + return preg_replace('#\.[^.]*$#', '', $thumb) . '_thumb.' . $ext; + } +} diff --git a/core/libraries/Hubzero/User/Picture/Resolver.php b/core/libraries/Hubzero/User/Picture/Resolver.php new file mode 100644 index 00000000000..3fe649b11f1 --- /dev/null +++ b/core/libraries/Hubzero/User/Picture/Resolver.php @@ -0,0 +1,25 @@ + $value) + { + if ('_s_' == substr($property, 0, 3)) // Don't touch static variables + { + continue; + } + + unset($this->$property); + $this->$property = $value; + } + + $objvars = get_object_vars($this); + + foreach ($objvars as $property => $value) + { + if (!array_key_exists($property, $classvars)) + { + unset($this->$property); + } + } + + return true; + } + + /** + * Load a record from the MySQL database + * + * @param mixed $user Integer (ID) or string (username) + * @return boolean True on success, False on error + */ + private function _mysql_load($user) + { + $db = \App::get('db'); + + if (empty($user)) + { + $this->setError('No user specified'); + return false; + } + + // zooley: Removed check for >= 0 because profiles without acounts have negative IDs + //if (is_numeric($user) && $user >= 0) + if (is_numeric($user)) + { + $query = "SELECT * FROM `#__xprofiles` WHERE uidNumber = " . $db->quote(intval($user)) . ";"; + } + else + { + $query = "SELECT * FROM `#__xprofiles` WHERE username = " . $db->quote($user) . " AND uidNumber>0;"; + } + + $db->setQuery($query); + + $result = $db->loadAssoc(); + + if ($result === false) + { + $this->setError('Error retrieving data from xprofiles table: ' . $db->getErrorMsg()); + return false; + } + + if (empty($result)) + { + $this->setError('No such user [' . $user . ']'); + return false; + } + + $this->clear(); + + $this->_params = new Registry($result['params']); + + foreach ($result as $property => $value) + { + $this->set($property, $value); + } + + $classvars = get_class_vars(__CLASS__); + + foreach ($classvars as $property => $value) + { + if ('_auxv_' == substr($property, 0, 6) || '_auxs_' == substr($property, 0, 6)) + { + $this->$property = false; // This property is loaded on demand + } + } + + $this->_params->merge($this->params); + + return true; + } + + /** + * Load an author record into this profile + * + * @param integer $authorid Author ID + * @return boolean True on success, False on error + */ + private function _mysql_author_load($authorid) + { + static $_propertyauthormap = array( + 'uidNumber' => 'id', + 'givenName' => 'firstname', + 'middleName' => 'middlename', + 'surname' => 'lastname', + 'organization' => 'org', + 'bio' => 'bio', + 'url' => 'url', + 'picture' => 'picture', + 'vip' => 'principal_investigator' + ); + + $db = \App::get('db'); + + $query = "SELECT * FROM `#__author` WHERE id=" . $db->quote($authorid); + + $db->setQuery($query); + + $result = $db->loadAssoc(); + + if ($result === false) + { + $this->setError('Error retrieving data from author table: ' . $db->getErrorMsg()); + return false; + } + + if (empty($result)) + { + $this->setError('No such author [' . $authorid . ']'); + return false; + } + + $this->clear(); + + foreach ($_propertyauthormap as $property => $aproperty) + { + if (!empty($result[$aproperty])) + { + $this->set($property, $result[$aproperty]); + } + } + + return true; + } + + /** + * Bind registration data + * + * @param object $registration + * @return boolean + */ + private function _xregistration_load($registration) + { + static $_propertyregmap = array('username'=>'login', 'name'=>'name', 'email'=>'email', 'orcid'=>'orcid', 'orgtype'=>'orgtype', 'organization'=>'org', 'countryresident'=>'countryresident', 'countryorigin'=>'countryorigin', 'gender'=>'sex', 'url'=>'web', 'reason'=>'reason', 'mailPreferenceOption'=>'mailPreferenceOption', 'usageAgreement'=>'usageAgreement', 'nativeTribe'=>'nativeTribe', 'phone'=>'phone', 'disability'=>'disability', 'hispanic'=>'hispanic', 'race'=>'race', 'admin'=>'admin', 'host'=>'host', 'edulevel'=>'edulevel', 'role'=>'role', 'givenName'=>'givenName', 'middleName'=>'middleName', 'surname'=>'surname'); + + if (!is_object($registration)) + { + $this->setError("Invalid XRegistration object"); + return false; + } + + foreach ($_propertyregmap as $property => $rproperty) + { + if ($registration->get($rproperty) !== null) + { + $this->set($property, $registration->get($rproperty)); + } + } + + $this->set('mailPreferenceOption', $this->get('mailPreferenceOption') ? $this->get('mailPreferenceOption') : '-1'); + $this->set('usageAgreement', $this->get('usageAgreement') ? '1' : '0'); + + return true; + } + + /** + * Load registration data + * + * @param object &$registration + * @return boolean + */ + public function loadRegistration(&$registration) + { + if (!is_object($registration)) + { + return false; + } + + $keys = array('email', 'name', 'orgtype', 'countryresident', 'countryorigin', 'disability', 'hispanic', 'race', 'phone', 'reason', 'edulevel', 'role', 'surname', 'givenName', 'middleName', 'orcid'); + + foreach ($keys as $key) + { + if ($registration->get($key) !== null) + { + $this->set($key, $registration->get($key)); + } + } + + if ($registration->get('login') !== null) + { + $this->set('username', $registration->get('login')); + } + + if ($registration->get('password') !== null) + { + $this->set('password', $registration->get('password')); + } + + if ($registration->get('org') !== null || $registration->get('orgtext') !== null) + { + $this->set('organization', $registration->get('org')); + + if ($registration->get('orgtext')) + { + $this->set('organization', $registration->get('orgtext')); + } + } + + if ($registration->get('sex') !== null) + { + $this->set('gender', $registration->get('sex')); + } + + if ($registration->get('nativetribe') !== null) + { + $this->set('nativeTribe', $registration->get('nativetribe')); + } + + if ($registration->get('web') !== null) + { + $this->set('url', $registration->get('web')); + } + + if ($registration->get('mailPreferenceOption') !== null) + { + $this->set('mailPreferenceOption', $registration->get('mailPreferenceOption') ? $registration->get('mailPreferenceOption') : '-1'); + } + + if ($registration->get('usageAgreement') !== null) + { + $this->set('usageAgreement', $registration->get('usageAgreement') ? true : false); + } + + return true; + } + + /** + * Load a record + * + * @param mixed $user User data + * @param string $storage Storage type + * @return boolean True on success, False on error + */ + public function load($user, $storage = 'mysql') + { + if (!empty($storage) && !in_array($storage, array('mysql', 'author', 'xregistration'))) + { + $this->setError('Invalid storage option requested [' . $storage . ']'); + return false; + } + + if ($storage == 'mysql') + { + return $this->_mysql_load($user); + } + + if ($storage == 'author') + { + return $this->_mysql_load_author($user); + } + + if ($storage == 'xregistration') + { + return $this->_xregistration_load($user); + } + + return true; + } + + /** + * Constructor + * + * @param mixed $user User data + * @return boolean True on success, False on error + */ + public function __construct($user = null) + { + if (!empty($user)) + { + return $this->load($user); + } + + return true; + } + + /** + * Returns a reference to the global User object, only creating it if it doesn't already exist. + * + * @param mixed $id The user to load - Can be an integer or string + * @return mixed Returns object if valid record found, false if not + */ + public static function getInstance($id = null) + { + static $instances; + static $usernames; + + if (!isset($instances)) + { + $instances = array(); + } + + if (!isset($usernames)) + { + $usernames = array(); + } + + // Is this a username? + if (!is_numeric($id)) + { + // Normalize and check if we have data for this username + $id = strtolower(trim($id)); + if (!isset($usernames[$id])) + { + $user = new self($id); + + // Save + $usernames[$id] = $user->get('uidNumber'); + $instances[$usernames[$id]] = $user; + } + // Change the $id from username to numeric ID + $id = $usernames[$id]; + } + + // Check for existing record + if (empty($instances[$id]) || $instances[$id]->get('uidNumber') != $id) + { + $user = new self($id); + $instances[$id] = $user; + } + + // Ensure record has data + if (!$instances[$id]->get('uidNumber')) + { + return false; + } + + return $instances[$id]; + } + + /** + * Create a new entry in the profiles table + * + * @return boolean True on success, False on error + */ + public function create() + { + $db = \App::get('db'); + + $modifiedDate = gmdate('Y-m-d H:i:s'); + + if (is_numeric($this->get('uidNumber'))) + { + $query = "INSERT INTO `#__xprofiles` (uidNumber,username,modifiedDate) VALUE (" . $db->quote($this->get('uidNumber')) . ',' . $db->quote($this->get('username')) . ',' . $db->quote($modifiedDate) . ");"; + + $db->setQuery($query); + + if (!$db->query()) + { + $errno = $db->getErrorNum(); + + if ($errno == 1062) + { + $this->setError('uidNumber (' . $this->get('uidNumber') . ') already exists' . ' in xprofiles table'); + } + else + { + $this->setError('Error inserting user data to xprofiles table: ' . $db->getErrorMsg()); + } + + return false; + } + } + else + { + $token = uniqid(); + + $query = "INSERT INTO `#__xprofiles` (uidNumber,username,modifiedDate) SELECT " . "IF(MIN(uidNumber)>0,-1,MIN(uidNumber)-1)," . $db->quote($token) . ',' . $db->quote($modifiedDate) . " FROM #__xprofiles;"; + + $db->setQuery($query); + + if (!$db->query()) + { + $this->setError('Error inserting non-user data to xprofiles table: ' . $db->getErrorMsg()); + + return false; + } + + $query = "SELECT uidNumber FROM `#__xprofiles` WHERE username=" . $db->quote($token) . " AND modifiedDate=" . $db->quote($modifiedDate); + + $db->setQuery($query); + + $result = $db->loadColumn(); + + if ($result === false) + { + $this->setError('Error adding data to xprofiles table: ' . $db->getErrorMsg()); + + return false; + } + + if (count($result) > 1) + { + $this->setError('Error adding data to xprofiles table: ' . $db->getErrorMsg()); + + return false; + } + + $this->set('uidNumber', $result[0]); + } + + $this->set('modifiedDate', $modifiedDate); + + if ($this->update() === false) + { + return false; + } + + return true; + } + + /** + * Alias for the load() method + * + * @param boolean $instance The user to load - Can be an integer or string + * @return boolean True on success, False on error + */ + public function read($instance = null) + { + return $this->load($instance); + } + + /** + * Store data to the database record. + * Creates a record if doesn't exist, otherwise updates record. + * + * @return boolean True on success, False on error + */ + public function store() + { + if (!is_numeric($this->get('uidNumber'))) + { + return $this->create(); + } + return $this->update(); + } + + /** + * Update an existing record + * + * @return boolean True on success, False on error + */ + public function update() + { + if (!is_numeric($this->get('uidNumber'))) + { + return false; + } + + $db = \App::get('db'); + + $modifiedDate = gmdate('Y-m-d H:i:s'); + + $this->set('modifiedDate', $modifiedDate); + + $query = "UPDATE `#__xprofiles` SET "; + + $classvars = get_class_vars(__CLASS__); + + $first = true; + $affected = 0; + + foreach ($classvars as $property => $value) + { + if ('_' == substr($property, 0, 1)) + { + continue; + } + + if (!$first) + { + $query .= ','; + } + else + { + $first = false; + } + + if ($property == 'params') + { + if (is_object($this->_params)) + { + $query .= "params='".str_replace("", "", $this->_params->toString())."'"; + } + else + { + $query .= "params=''"; + } + continue; + } + + if ($this->get($property) === null) + { + $query .= "$property=NULL"; + } + else + { + $query .= "$property=" . $db->quote($this->get($property)); + } + } + + $query .= " WHERE uidNumber=" . $db->quote($this->get('uidNumber')) . ";"; + + $db->setQuery($query); + + if (!$db->query()) + { + $this->setError('Error updating data in xprofiles table: ' . $db->getErrorMsg()); + } + + $affected = $db->getAffectedRows(); + + foreach ($classvars as $property => $value) + { + if (('_auxv_' != substr($property, 0, 6)) && ('_auxs_' != substr($property, 0, 6))) + { + continue; + } + + $property = substr($property, 6); + + $first = true; + + $query = "REPLACE INTO #__xprofiles_" . $property . " (uidNumber, " . $property . ") VALUES "; + $query_values = ""; + + $list = $this->get($property); + + if (!is_array($list)) + { + $list = array($list); + } + + foreach ($list as $value) + { + if (!$first) + { + $query_values .= ','; + } + + $first = false; + + $query_values .= '(' . $db->quote($this->get('uidNumber')) . ',' . $db->quote($value) . ')'; + } + + if ($query_values != '') + { + $db->setQuery($query . $query_values); + + if (!$db->query()) + { + $this->setError("Error updating data in xprofiles $property table: " . $db->getErrorMsg()); + } + else + { + $affected += $db->getAffectedRows(); + } + } + + if (property_exists(__CLASS__, '_auxv_' . $property)) + { + foreach ($list as $key => $value) + { + $list[$key] = $db->quote($value); + } + + $valuelist = implode($list, ","); + + if (empty($valuelist)) + { + $valuelist = "''"; + } + + $query = "DELETE FROM #__xprofiles_" . $property . " WHERE uidNumber=" . $this->get('uidNumber') . " AND $property NOT IN ($valuelist);"; + + $db->setQuery($query); + + if (!$db->query()) + { + $this->setError("Error deleting data in xprofiles $property table: " . $db->getErrorMsg()); + } + else + { + $affected += $db->getAffectedRows(); + } + } + } + + if ($affected > 0) + { + Event::trigger('user.onAfterStoreProfile', array($this)); + } + + return true; + } + + /** + * Delete a record + * + * @return boolean True on success, False on error + */ + public function delete() + { + $db = \App::get('db'); + + if (!is_numeric($this->get('uidNumber'))) + { + $this->setError("missing required field 'uidNumber'"); + return false; + } + + $classvars = get_class_vars(__CLASS__); + + $affected = 0; + + foreach ($classvars as $property => $value) + { + if ('_auxv_' != substr($property, 0, 6) && '_auxs_' != substr($property, 0, 6)) + { + continue; + } + + $property = substr($property, 6); + + $query = "DELETE FROM `#__xprofiles_$property` WHERE uidNumber = " . $db->quote($this->get('uidNumber')); + $db->setQuery($query); + + if (!$db->query()) + { + $this->setError("Error deleting from xprofiles $property table: " . $db->getErrorMsg()); + } + else + { + $affected += $db->getAffectedRows(); + } + } + + $query = "DELETE FROM `#__xprofiles` WHERE uidNumber = " . $db->quote($this->get('uidNumber')); + $db->setQuery($query); + + if (!$db->query()) + { + $this->setError("Error deleting from xprofiles table: " . $db->getErrorMsg()); + } + else + { + $affected += $db->getAffectedRows(); + } + + if ($affected > 0) + { + Event::trigger('user.onAfterDeleteProfile', array($this)); + } + + $this->clear(); + + return true; + } + + /** + * Get a property's value + * + * @param string $property Name of the property to retrieve + * @param mixed $value Default value + * @return mixed + */ + public function get($property, $default = null) + { + if ($property == 'password') + { + return $this->_password; + } + + if ('_' == substr($property, 0, 1)) + { + $this->setError("Can't access private properties"); + return false; + } + + if (!property_exists(__CLASS__, $property)) + { + if (property_exists(__CLASS__, '_auxs_' . $property)) + { + $property = '_auxs_' . $property; + } + else if (property_exists(__CLASS__, '_auxv_' . $property)) + { + $property = '_auxv_' . $property; + } + else + { + $this->setError("Unknown property: $property"); + return false; + } + } + + if ($this->$property === false) + { + $db = \App::get('db'); + + $property_name = substr($property, 6); + + $query = "SELECT $property_name FROM `#__xprofiles` AS x, `#__xprofiles_$property_name` AS xp WHERE x.uidNumber=xp.uidNumber AND xp.uidNumber=" . $db->quote($this->get('uidNumber')) . " ORDER BY $property_name ASC;"; + + $db->setQuery($query); + + $result = $db->loadColumn(); + + if ($result === false) + { + $this->setError("Error retrieving data from xprofiles $property table: " . $db->getErrorMsg()); + } + else if ('_auxs_' == substr($property, 0, 6)) + { + if (isset($result[0])) + { + $this->set($property_name, $result[0]); + } + else + { + $this->set($property_name, ''); + } + } + else + { + if (is_array($result) && count($result) <= 1 && empty($result[0])) + { + $this->set($property_name, array()); + } + else + { + $this->set($property_name, $result); + } + } + } + + return $this->$property; + } + + /** + * Set a property's value + * + * @param string $property Property name + * @param mixed $value Property value + * @return boolean True on success, False on error + */ + public function set($property, $value = null) + { + if ($property == 'password') + { + if ($value != '') + { + $this->userPassword = \Hubzero\User\Password::getPasshash($value); + } + else + { + $this->userPassword = ''; + } + + $this->_password = $value; + + return true; + } + + if ('_' == substr($property, 0, 1)) + { + $this->setError("Can't access private properties"); + return false; + } + + if (!property_exists(__CLASS__, $property)) + { + if (property_exists(__CLASS__, '_auxs_' . $property)) + { + $property = '_auxs_' . $property; + } + else if (property_exists(__CLASS__, '_auxv_' . $property)) + { + $property = '_auxv_' . $property; + } + else + { + $this->setError("Unknown property: $property"); + return false; + } + } + + if ('_auxv_' == substr($property, 0, 6)) + { + if (empty($value)) + { + $value = array(); + } + else + { + if (!is_array($value)) + { + $value = array($value); + } + + $list = array_unique($value); + sort($list); + unset($value); + + foreach ($list as $v) + { + $value[] = strval($v); + } + } + } + elseif (is_string($value)) + { + $value = strval($value); + } + + $this->$property = $value; + + if ($property == 'userPassword') + { + $this->_password = ''; + } + + return true; + } + + /** + * Add to a list of values for multi-value properties + * + * @param string $property Property name + * @param array $value Property values + * @return boolean True on success, False on error + */ + public function add($property, $value) + { + if ('_' == substr($property, 0, 1)) + { + $this->setError("Can't access private properties"); + return false; + } + + if (property_exists(__CLASS__, $property) || property_exists(__CLASS__, '_auxs_' . $property)) + { + $this->setError("Can't add value(s) to non-array property."); + return false; + } + + if (!property_exists(__CLASS__, '_auxv_' . $property)) + { + $this->setError("Unknown property: $property"); + return false; + } + + if (empty($value)) + { + return true; + } + + if (!is_array($value)) + { + $value = array($value); + } + + $property = '_auxv_' . $property; + + foreach ($value as $v) + { + $v = strval($v); + + if (!in_array($v, $this->$property)) + { + array_push($this->$property, $v); + } + } + + sort($this->$property); + + return true; + } + + /** + * Remove from a list of values for multi-value properties + * + * @param string $property Property name + * @param array $value Property values + * @return boolean True on success, False on error + */ + public function remove($property, $value) + { + if ('_' == substr($property, 0, 1)) + { + $this->setError("Can't access private properties"); + return false; + } + + if (property_exists(__CLASS__, $property) || property_exists(__CLASS__, '_auxs_' . $property)) + { + $this->setError("Can't remove value(s) from non-array property."); + return false; + } + + if (!property_exists(__CLASS__, '_auxv_' . $property)) + { + $this->setError("Unknown property: $property"); + return false; + } + + if (!isset($value)) + { + return true; + } + + if (!is_array($value)) + { + $value = array($value); + } + + $property = '_auxv_' . $property; + + foreach ($value as $v) + { + $v = strval($v); + + if (in_array($v, $this->$property)) + { + $this->$property = array_diff($this->$property, array($v)); + } + } + + return true; + } + + /** + * Returns a property of the Params object or + * the default value if the property is not set. + * + * @param string $key The name of the property. + * @param mixed $default The default value. + * @return boolean + */ + public function getParam($key, $default = null) + { + return $this->_params->get($key, $default); + } + + /** + * Modifies a property of the Params object. + * + * @param string $key The name of the property. + * @param mixed $value The value of the property to set. + * @return boolean + */ + public function setParam($key, $value) + { + return $this->_params->set($key, $value); + } + + /** + * Sets a default value on the Params object + * if not alreay assigned. + * + * @param string $key The name of the property. + * @param mixed $value The default value. + * @return boolean + */ + public function defParam($key, $value) + { + return $this->_params->def($key, $value); + } + + /** + * Get parameters object + * + * @param boolean $loadsetupfile Load the XML set up file? + * @param string $path Path to parameters XML file + * @return object Registry + */ + public function &getParameters($loadsetupfile = false, $path = null) + { + static $parampath; + + /* + // Set a custom parampath if defined + if (isset($path)) + { + $parampath = $path; + } + + // Set the default parampath if not set already + if (!isset($parampath)) + { + $parampath = PATH_CORE . DS . 'components' . DS . 'com_members' . DS . 'admin' . DS . 'models'; + } + + if ($loadsetupfile) + { + $type = str_replace(' ', '_', strtolower($this->usertype)); + + $file = $parampath . DS . $type . '.xml'; + if (!file_exists($file)) + { + $file = $parampath . DS . 'user.xml'; + } + + $this->_params->loadSetupFile($file); + } + */ + + return $this->_params; + } + + /** + * Set parameters + * + * @param object $params Parameters object to set + * @return void + */ + public function setParameters($params) + { + $this->_params = $params; + } + + /** + * Get group roles for a specific member/group pair + * + * @param string $uid User ID + * @param string $gid Group ID + * @return array + */ + public static function getGroupMemberRoles($uid, $gid) + { + $db = \App::get('db'); + $sql = "SELECT r.id, r.name, r.permissions FROM `#__xgroups_roles` as r, `#__xgroups_member_roles` as m WHERE r.id=m.roleid AND m.uidNumber=" . $db->quote($uid) . " AND r.gidNumber=" . $db->quote($gid); + $db->setQuery($sql); + + return $db->loadAssocList(); + } + + /** + * Check to see if user has permission to perform task + * + * @param object $group \Hubzero\User\Group + * @param string $action Group Action to perform + * @return boolean + */ + public static function userHasPermissionForGroupAction($group, $action) + { + // Get user roles + $roles = self::getGroupMemberRoles( + \User::get('id'), + $group->get('gidNumber') + ); + + // Check to see if any of our roles for user has permission for action + foreach ($roles as $role) + { + $permissions = json_decode($role['permissions']); + $permissions = (is_object($permissions)) ? $permissions : new \stdClass; + if (property_exists($permissions, $action) && $permissions->$action == 1) + { + return true; + } + } + return false; + } + + /** + * Get the groups for a user + * + * @param string $role The group set to return. Returns all groups if not set + * @return array Array of groups + */ + public function getGroups($role = 'all') + { + static $groups; + + if (!isset($groups)) + { + $groups = array( + 'applicants' => array(), + 'invitees' => array(), + 'members' => array(), + 'managers' => array(), + 'all' => array() + ); + $groups['all'] = Helper::getGroups($this->get('uidNumber'), 'all', 1); + + if ($groups['all']) + { + foreach ($groups['all'] as $item) + { + if ($item->registered) + { + if (!$item->regconfirmed) + { + $groups['applicants'][] = $item; + } + else + { + if ($item->manager) + { + $groups['managers'][] = $item; + } + else + { + $groups['members'][] = $item; + } + } + } + else + { + $groups['invitees'][] = $item; + } + } + } + } + + if ($role) + { + return (isset($groups[$role])) ? $groups[$role] : false; + } + + return $groups; + } + + /** + * Get the content of the entry + * + * @param string $as Format to return state in [text, number] + * @param integer $shorten Number of characters to shorten text to + * @return string + */ + public function getBio($as='parsed', $shorten=0) + { + $options = array(); + + switch (strtolower($as)) + { + case 'parsed': + $config = array( + 'option' => 'com_members', + 'scope' => 'profile', + 'pagename' => 'member', + 'pageid' => 0, + 'filepath' => '', + 'domain' => '', + 'camelcase' => 0 + ); + + Event::trigger('content.onContentPrepare', array( + 'com_members.profile.bio', + &$this, + &$config + )); + $content = $this->get('bio'); + + $options = array('html' => true); + break; + + case 'clean': + $content = strip_tags($this->getBio('parsed')); + break; + + case 'raw': + default: + $content = stripslashes($this->get('bio')); + $content = preg_replace('/^()/i', '', $content); + break; + } + + if ($shorten) + { + $content = Str::truncate($content, $shorten, $options); + } + + return $content; + } + + /** + * Get a user's picture + * + * @param integer $anonymous Is user anonymous? + * @param boolean $thumbit Show thumbnail or full picture? + * @return string + */ + public function getPicture($anonymous=0, $thumbit=true, $serveFile=true) + { + return ProfileHelper::getMemberPhoto($this, $anonymous, $thumbit, $serveFile); + } + + /** + * Generate and return various links to the entry + * Link will vary depending upon action desired such as edit, delete, etc. + * + * @param string $type The type of link to return + * @return string + */ + public function getLink($type='') + { + if (!$id = $this->get('uidNumber')) + { + return ''; + } + + $link = 'index.php?option=com_members&id=' . $id; + + // If it doesn't exist or isn't published + $type = strtolower($type); + switch ($type) + { + case 'edit': + case 'changepassword': + $link .= '&task=' . $type; + break; + + default: + break; + } + + return $link; + } +} diff --git a/core/libraries/Hubzero/User/Profile/Helper.php b/core/libraries/Hubzero/User/Profile/Helper.php new file mode 100644 index 00000000000..b104625541e --- /dev/null +++ b/core/libraries/Hubzero/User/Profile/Helper.php @@ -0,0 +1,260 @@ +setQuery("SELECT uidNumber FROM `#__xprofiles`;"); + + $result = $db->loadColumn(); + + if ($result === false) + { + throw new Exception('Error retrieving data from xprofiles table: ' . $db->getErrorMsg(), 500); + return false; + } + + foreach ($result as $row) + { + $func($row); + } + + return true; + } + + /** + * Find a username by email address + * + * @param string $email Email address to look up + * @return mixed False if not found, string if found + */ + public static function find_by_email($email) + { + if (empty($email)) + { + return false; + } + + $db = \App::get('db'); + $db->setQuery("SELECT username FROM `#__xprofiles` WHERE `email`=" . $db->quote($email)); + + $result = $db->loadColumn(); + + if (empty($result)) + { + return false; + } + + return $result; + } + + /** + * Get member picture + * + * @param mixed $member Member to get picture for + * @param integer $anonymous Anonymous user? + * @param boolean $thumbit Display thumbnail (default) or full image? + * @return string Image URL + */ + public static function getMemberPhoto($member, $anonymous=0, $thumbit=true, $serveFile=true) + { + static $dfthumb; + static $dffull; + + $config = \Component::params('com_members'); + + // Get the default picture + // We need to do this here as it may be needed by the Gravatar service + if (!$dffull) + { + $dffull = '/core/components/com_members/site/assets/img/profile.gif'; //ltrim($config->get('defaultpic', '/components/com_members/site/assets/img/profile.gif'), DS); + } + if (!$dfthumb) + { + if ($thumbit) + { + $dfthumb = self::thumbit($dffull); + } + } + + // lets make sure we have a profile object + if ($member instanceof User) + { + return $member->picture($anonymous, $thumbit, $serveFile); + } + else if (is_numeric($member) || is_string($member)) + { + $member = Profile::getInstance($member); + } + + $paths = array(); + + $apppath = trim(substr(PATH_APP, strlen(PATH_ROOT)), DS) . '/site/members'; + + // If not anonymous + if (!$anonymous) + { + // If we have a member + if (is_object($member)) + { + if (!$member->get('picture')) + { + // Do we auto-generate a picture? + if ($config->get('identicon')) + { + $path = PATH_APP . DS . trim($config->get('webpath', '/site/members'), DS) . DS . self::niceidformat($member->get('uidNumber')); + + if (!is_dir($path)) + { + \App::get('filesystem')->makeDirectory($path); + } + + if (is_dir($path)) + { + $identicon = new Identicon(); + + // Create a profile image + $imageData = $identicon->getImageData($member->get('email'), 200, $config->get('identicon_color', null)); + file_put_contents($path . DS . 'identicon.png', $imageData); + + // Create a thumbnail image + $imageData = $identicon->getImageData($member->get('email'), 50, $config->get('identicon_color', null)); + file_put_contents($path . DS . 'identicon_thumb.png', $imageData); + + // Save image to profile + $member->set('picture', 'identicon.png'); + // Update directly. Using update() method can cause unexpected data loss in some cases. + $database = \App::get('db'); + $database->setQuery("UPDATE `#__xprofiles` SET picture=" . $database->quote($member->get('picture')) . " WHERE uidNumber=" . $member->get('uidNumber')); + $database->query(); + //$member->update(); + } + } + } + + // If member has a picture set + if ($member->get('picture')) + { + $thumb = DS . $apppath . DS . self::niceidformat($member->get('uidNumber')); + + $thumbAlt = $thumb . DS . ltrim($member->get('picture'), DS); + if ($thumbit) + { + $thumbAlt = $thumb . DS . 'thumb.png'; + } + + $thumb .= DS . ltrim($member->get('picture'), DS); + + if ($thumbit) + { + $thumb = self::thumbit($thumb); + } + + $paths[] = $thumbAlt; + $paths[] = $thumb; + } + else + { + // If use of gravatars is enabled + if ($config->get('gravatar')) + { + $hash = md5(strtolower(trim($member->get('email')))); + $protocol = \App::get('request')->isSecure() ? 'https' : 'http'; + + return $protocol + . '://www.gravatar.com/avatar/' . htmlspecialchars($hash) . '?' + . (!$thumbit ? 's=300&' : '') + . 'd=' . urlencode(str_replace('/administrator', '', rtrim(\App::get('request')->base(), '/')) . '/' . $dfthumb); + } + } + } + } + + // Add the default picture last + $paths[] = ($thumbit) ? $dfthumb : $dffull; + + // Start running through paths until we find a valid one + foreach ($paths as $path) + { + if ($path && file_exists(PATH_ROOT . $path)) + { + if (!$anonymous) + { + // build base path (ex. /site/members/12345) + $baseMemberPath = DS . $apppath . DS . self::niceidformat($member->get('uidNumber')); + + // if we want to serve file & path is within /site + if ($serveFile && strpos($path, $baseMemberPath) !== false) + { + // get picture name (allows to pics in subfolder) + $pic = trim(str_replace($baseMemberPath, '', $path), DS); + + // build serve link + $link = with(new \Hubzero\Content\Moderator(PATH_ROOT . $path))->getUrl(); + return $link; + } + } + + return str_replace('/administrator', '', rtrim(\App::get('request')->base(true), '/')) . $path; + } + } + } + + /** + * Generate a thumbnail file name format + * example.jpg -> example_thumb.jpg + * + * @param string $thumb Filename to get thumbnail of + * @return string + */ + public static function thumbit($thumb) + { + $dot = strrpos($thumb, '.') + 1; + $ext = substr($thumb, $dot); + + return preg_replace('#\.[^.]*$#', '', $thumb) . '_thumb.' . $ext; + } + + /** + * Pad a user ID with zeros + * ex: 123 -> 00123 + * + * @param integer $someid + * @return integer + */ + public static function niceidformat($someid) + { + $prfx = ''; + if (substr($someid, 0, 1) == '-') + { + $prfx = 'n'; + $someid = substr($someid, 1); + } + while (strlen($someid) < 5) + { + $someid = 0 . "$someid"; + } + return $prfx . $someid; + } +} diff --git a/core/libraries/Hubzero/User/Reputation.php b/core/libraries/Hubzero/User/Reputation.php new file mode 100644 index 00000000000..b9e3da50b12 --- /dev/null +++ b/core/libraries/Hubzero/User/Reputation.php @@ -0,0 +1,64 @@ +get('spam_count', 0); + $this->set('spam_count', ($current+1)); + $this->set('user_id', \User::get('id')); + $this->save(); + + // Also increment session spam count + $current = Session::get('spam_count', 0); + Session::set('spam_count', ($current+1)); + } + + /** + * Checks to see if user is jailed + * + * @return bool + */ + public function isJailed() + { + if ($this->get('user_id', false)) + { + $params = Plugin::params('system', 'spamjail'); + $sessionCount = $params->get('session_count', 5); + $lifetimeCount = $params->get('user_count', 10); + if (Session::get('spam_count', 0) > $sessionCount || $this->get('spam_count', 0) > $lifetimeCount) + { + return true; + } + } + + return false; + } +} diff --git a/core/libraries/Hubzero/User/Token.php b/core/libraries/Hubzero/User/Token.php new file mode 100644 index 00000000000..19f208b3dea --- /dev/null +++ b/core/libraries/Hubzero/User/Token.php @@ -0,0 +1,42 @@ + 'positive|nonzero' + ); + + /** + * Automatically fillable fields + * + * @var array + */ + public $initiate = array( + 'created' + ); +} diff --git a/core/libraries/Hubzero/User/User.php b/core/libraries/Hubzero/User/User.php new file mode 100644 index 00000000000..6904311ba68 --- /dev/null +++ b/core/libraries/Hubzero/User/User.php @@ -0,0 +1,1049 @@ + 'notempty', + 'email' => 'notempty', + 'username' => 'notempty' + ); + + /** + * Automatic fields to populate every time a row is created + * + * @var array + * @since 2.1.0 + */ + public $initiate = array( + 'registerDate', + 'registerIP', + 'access' + ); + + /** + * A cached switch for if this user has root access rights. + * + * @var boolean + * @since 2.1.0 + */ + protected $isRoot = null; + + /** + * User params + * + * @var object + * @since 2.1.0 + */ + protected $userParams = null; + + /** + * Authorised access groups + * + * @var array + * @since 2.1.0 + */ + protected $authGroups = null; + + /** + * Authorised access levels + * + * @var array + * @since 2.1.0 + */ + protected $authLevels = null; + + /** + * Authorised access actions + * + * @var array + * @since 2.1.0 + */ + protected $authActions = null; + + /** + * Link pattern + * + * @var string + * @since 2.1.0 + */ + public static $linkBase = null; + + /** + * List of picture resolvers + * + * @var array + * @since 2.1.0 + */ + public static $pictureResolvers = array(); + + /** + * Serializes the model data for storage + * + * @return string + * @since 2.1.0 + */ + public function serialize() + { + $attr = $this->getAttributes(); + + $attr['guest'] = $this->guest; + + return serialize($attr); + } + + /** + * Unserializes the data into a new model + * + * @param string $data The data to build from + * @return void + * @since 2.1.0 + */ + public function unserialize($data) + { + $this->__construct(); + + $data = unserialize($data); + + if (isset($data['guest'])) + { + $this->guest = $data['guest']; + unset($data['guest']); + } + + $this->set($data); + } + + /** + * Sets up additional custom rules + * + * @return void + */ + public function setup() + { + // Check that username conforms to rules + $this->addRule('username', function($data) + { + $username = $data['username']; + + // We do this here because we need to allow one possible + // "invalid" username to pass through, used when creating + // temp accounts during the 3rd party auth registration + if (is_numeric($username) && $username < 0) + { + return false; + } + + if (preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $username) + || strlen(utf8_decode($username)) < 2 + || trim($username) != $username) + { + return \Lang::txt('JLIB_DATABASE_ERROR_VALID_AZ09', 2); + } + + return false; + }); + + // Check for existing username + $this->addRule('username', function($data) + { + $user = self::oneByUsername($data['username']); + + if ($user->get('id') && $user->get('id') != $data['id']) + { + return \Lang::txt('JLIB_DATABASE_ERROR_USERNAME_INUSE'); + } + + return false; + }); + + // Check for valid email address + // We do this here because we need to allow one possible + // "invalid" address to pass through, used when creating + // temp accounts during the 3rd party auth registration + $this->addRule('email', function($data) + { + $email = $data['email']; + + if (preg_match('/^-[0-9]+@invalid$/', $email)) + { + return false; + } + + return (\Hubzero\Utility\Validate::email($email) ? false : 'Email does not appear to be valid'); + }); + } + + /** + * Generates automatic registerDate field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticRegisterDate($data) + { + $dt = new Date('now'); + + return $dt->toSql(); + } + + /** + * Generates automatic registerIP field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticRegisterIP($data) + { + if (!isset($data['registerIP'])) + { + $data['registerIP'] = \Request::ip(); + } + return $data['registerIP']; + } + + /** + * Generates automatic access field value + * + * @param array $data the data being saved + * @return string + */ + public function automaticAccess($data) + { + if (!isset($data['access']) || !$data['access']) + { + $data['access'] = 1; + } + return $data['access']; + } + + /** + * Defines a one to many relationship between users and reset tokens + * + * @return object \Hubzero\Database\Relationship\OneToMany + * @since 2.0.0 + */ + public function tokens() + { + return $this->oneToMany('Hubzero\User\Token', 'user_id'); + } + + /** + * Defines a one to one relationship between a user and their reputation + * + * @return object \Hubzero\Database\Relationship\OneToOne + * @since 2.0.0 + */ + public function reputation() + { + return $this->oneToOne('Hubzero\User\Reputation', 'user_id'); + } + + /** + * Get access groups + * + * @return object + */ + public function accessgroups() + { + return $this->oneToMany('Hubzero\Access\Map', 'user_id'); + } + + /** + * Get groups + * + * @param string $role + * @return array + */ + public function groups($role = 'all') + { + //return $this->manyToMany('Hubzero\User\Extended\Group', 'id', 'uidNumber'); + + static $groups; + + if (!isset($groups)) + { + $groups = array( + 'applicants' => array(), + 'invitees' => array(), + 'members' => array(), + 'managers' => array(), + 'all' => array() + ); + $all = Helper::getGroups($this->get('id'), 'all', 1); + + if ($all) + { + $groups['all'] = $all; + + foreach ($groups['all'] as $item) + { + if ($item->registered) + { + if (!$item->regconfirmed) + { + $groups['applicants'][] = $item; + } + else + { + if ($item->manager) + { + $groups['managers'][] = $item; + } + else + { + $groups['members'][] = $item; + } + } + } + else + { + $groups['invitees'][] = $item; + } + } + } + } + + if ($role) + { + return (isset($groups[$role])) ? $groups[$role] : array(); + } + + return $groups; + } + + /** + * Defines a relationship with a generic user logging class (not a relational model itself) + * + * @return object \Hubzero\User\Logger + * @since 2.0.0 + */ + public function logger() + { + return new Logger($this); + } + + /** + * Gets an attribute by key + * + * This will not retrieve properties directly attached to the model, + * even if they are public - those should be accessed directly! + * + * Also, make sure to access properties in transformers using the get method. + * Otherwise you'll just get stuck in a loop! + * + * @param string $key The attribute key to get + * @param mixed $default The value to provide, should the key be non-existent + * @return mixed + */ + public function get($key, $default = null) + { + if ($key == 'guest') + { + return $this->isGuest(); + } + + if ($key == 'uidNumber') + { + $key = 'id'; + } + + // If the givenName, middleName, or surname isn't set, try to determine it from the name + if (($key == 'givenName' || $key == 'middleName' || $key == 'surname') && parent::get($key, null) == null) + { + return $this->parseName($key); + } + + // Legacy code expects get('id') to always + // return an integer, even if user is logged out + if ($key == 'id' && is_null($default)) + { + $default = 0; + } + + return parent::get($key, $default); + } + + /** + * Sets attributes (i.e. fields) on the model + * + * This must be used when setting data to be saved. Otherwise, the properties + * will be attached directly to the model itself and not included in the save. + * + * @param array|string $key The key to set, or array of key/value pairs + * @param mixed $value The value to set if key is string + * @return object $this Chainable + * @since 2.1.0 + */ + public function set($key, $value = null) + { + if (is_string($key) && $key == 'guest') + { + return $this->guest = $value; + } + + if (is_string($key) && $key == 'uidNumber') + { + $key = 'id'; + } + + return parent::set($key, $value); + } + + /** + * Is the current user a guest (logged out) or not? + * + * @return boolean + */ + public function isGuest() + { + $pubkeyb64 = Config::get('jwt_pub_key', null); + $env = substr(Config::get('application_env', ''), -5); + + // check for a jwt if user is not logged in + if ($this->guest && array_key_exists('jwt', $_COOKIE) && + $env == 'cloud' && !is_null($pubkeyb64)) + { + try + { + // decode public key and use it to check jwt signature + $pubkey = base64_decode($pubkeyb64); + $jwt = \Firebase\JWT\JWT::decode($_COOKIE['jwt'], $pubkey, array('RS512')); + + // if we have information for a user, populate the user variable + if (isset($jwt->email) && isset($jwt->id) && isset($jwt->username) && isset($jwt->name) && isset($jwt->exp)) + { + if ($jwt->exp < time()) + { + setcookie('jwt', -86400, '', '/', '.' . \Hubzero\Utility\Dns::domain(), true, true); + return $this->guest(); + } + $jwtid = $jwt->id; + $jwtemail = $jwt->email; + $jwtuser = $jwt->username; + $jwtname = $jwt->name; + + // check if we have a user by this email address + $user = \User::oneByEmail($jwtemail); + + // this user does not exist + // we should create this in the hub database + if ($user->isNew()) + { + // Using SQL here because the ORM does not currently support writing + // new records with a specific primary key value + $db = App::get('db'); + $query = "INSERT INTO `#__users` (`id`, `name`, `username`, `email`, `password`, `usertype`, `block`, " . + "`approved`, `sendEmail`, `activation`, `params`, `access`, `usageAgreement`, `homeDirectory`, `loginShell`, `ftpShell`) + VALUES (" . $db->quote($jwtid) . ", " . $db->quote($jwtname) . ", " . $db->quote($jwtuser) . + ", " . $db->quote($jwtemail) . ", " . $db->quote('') . ", " . $db->quote('') . ", " . + $db->quote('0') . ", " . $db->quote('2') . ", " . $db->quote('0') . ", " . $db->quote('1') . + ", " . $db->quote('') . ", " . $db->quote('5') . ", " . $db->quote('1') . ", " . + $db->quote('/home/' . $jwtuser) . ", " . $db->quote('/bin/bash') . ", " . + $db->quote('/usr/lib/sftp-server') . ")"; + + $db->setQuery($query); + $result = $db->query(); + + $usersConfig = Component::params('com_members'); + $newUsertype = $usersConfig->get('new_usertype', '2'); + $query = "INSERT INTO `#__user_usergroup_map` (`user_id`, `group_id`) VALUES (" . $db->quote($jwtid) . ", " . $db->quote($newUsertype) . ")"; + $db->setQuery($query); + $result = $db->query(); + // Clear the session that was not logged in + App::get('session')->restart(); + } + + // set up the user object to be logged in + \User::set('id', $user->get('id')); + \User::set('email', $jwtemail); + \User::set('username', $jwtuser); + \User::set('guest', false); + \User::set('approved', 2); + + // set the user object in the session such that + // next visit and other plugins that use the session + // know what user is logged in + App::get('session')->set('user', App::get('user')->getInstance()); + $this->guest = false; + + $data = App::get('user')->getInstance()->toArray(); + \Event::trigger('user.onUserLogin', array($data)); + } + } + catch (Exception $e) + { + // something likely went wrong with the jwt + } + } + return $this->guest; + } + + /** + * Transform parameters into object + * + * @return object \Hubzero\Config\Registry + * @since 2.1.0 + */ + public function transformParams() + { + if (!isset($this->userParams)) + { + $this->userParams = new Registry($this->get('params')); + } + + return $this->userParams; + } + + /** + * Method to get a parameter value + * + * @param string $key Parameter key + * @param mixed $default Parameter default value + * @return mixed The value or the default if it did not exist + * @since 2.1.0 + */ + public function getParam($key, $default = null) + { + return $this->params->get($key, $default); + } + + /** + * Method to set a parameter + * + * @param string $key Parameter key + * @param mixed $value Parameter value + * @return mixed Set parameter value + * @since 2.1.0 + */ + public function setParam($key, $value) + { + return $this->params->set($key, $value); + } + + /** + * Method to set a default parameter if it does not exist + * + * @param string $key Parameter key + * @param mixed $value Parameter value + * @return mixed Set parameter value + * @since 2.1.0 + */ + public function defParam($key, $value) + { + return $this->params->def($key, $value); + } + + /** + * Get a user's picture + * + * @param integer $anonymous Is user anonymous? + * @param boolean $thumbnail Show thumbnail or full picture? + * @param boolean $serveFile Serve file? + * @return string + * @since 2.1.0 + */ + public function picture($anonymous=0, $thumbnail=true, $serveFile=true) + { + static $fallback; + + if (!isset($fallback)) + { + $image = "" . + "" . + ""; + + $fallback = sprintf('data:image/svg+xml;base64,%s', base64_encode($image)); + } + + if (!$this->get('id') || $anonymous) + { + return $fallback; + } + + $picture = null; + + foreach (self::$pictureResolvers as $resolver) + { + $picture = $resolver->picture($this->get('id'), $this->get('name'), $this->get('email'), $thumbnail); + + if ($picture) + { + break; + } + } + + if (!$picture) + { + $picture = $fallback; + } + + return $picture; + } + + /** + * Generate and return various links to the entry + * Link will vary depending upon action desired such as edit, delete, etc. + * + * @param string $type The type of link to return + * @return string + * @since 2.1.0 + */ + public function link($type='') + { + if (!$this->get('id') || !self::$linkBase) + { + return ''; + } + + $link = str_replace( + array( + '{ID}', + '{USERNAME}', + '{EMAIL}', + '{NAME}' + ), + array( + $this->get('id'), + $this->get('username'), + $this->get('email'), + str_replace(' ', '+', $this->get('name')) + ), + self::$linkBase + ); + + return $link; + } + + /** + * Finds a user by username + * + * @param string $username + * @return object + * @since 2.1.0 + */ + public static function oneByUsername($username) + { + return self::all() + ->whereEquals('username', $username) + ->row(); + } + + /** + * Finds a user by email + * + * @param string $email + * @return object + * @since 2.1.0 + */ + public static function oneByEmail($email) + { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) + { + return self::oneByUsername($email); + } + + return self::all() + ->whereEquals('email', $email) + ->row(); + } + + /** + * Finds a user by activation token + * + * @param string $token + * @return object + * @since 2.1.0 + */ + public static function oneByActivationToken($token) + { + return self::all() + ->whereEquals('activation', $token) + ->row(); + } + + /** + * Pass through method to the table for setting the last visit date + * + * @param integer $timestamp The timestamp, defaults to 'now'. + * @return boolean True on success. + * @since 2.1.0 + */ + public function setLastVisit($timestamp = 'now') + { + $timestamp = new Date($timestamp); + + $query = $this->getQuery() + ->update($this->getTableName()) + ->set(array('lastvisitDate' => $timestamp->toSql())) + ->whereEquals('id', $this->get('id')); + + return $query->execute(); + } + + /** + * Alias for authorise() method + * + * @param string $action The name of the action to check for permission. + * @param string $assetname The name of the asset on which to perform the action. + * @return boolean True if authorised + * @since 2.1.0 + */ + public function authorize($action, $assetname = null) + { + return $this->authorise($action, $assetname); + } + + /** + * Method to check User object authorisation against an access control + * object and optionally an access extension object + * + * @param string $action The name of the action to check for permission. + * @param string $assetname The name of the asset on which to perform the action. + * @return boolean True if authorised + * @since 2.1.0 + */ + public function authorise($action, $assetname = null) + { + // Make sure we only check for core.admin once during the run. + if ($this->isRoot === null) + { + $this->isRoot = false; + + // Check for the configuration file failsafe. + $rootUser = \App::get('config')->get('root_user'); + + // The root_user variable can be a numeric user ID or a username. + if (is_numeric($rootUser) && $this->get('id') > 0 && $this->get('id') == $rootUser) + { + $this->isRoot = true; + } + elseif ($this->username && $this->username == $rootUser) + { + $this->isRoot = true; + } + else + { + // Get all groups against which the user is mapped. + $identities = $this->getAuthorisedGroups(); + + array_unshift($identities, $this->get('id') * -1); + + if (Access::getAssetRules(1)->allow('core.admin', $identities)) + { + $this->isRoot = true; + return true; + } + } + } + + return $this->isRoot ? true : Access::check($this->get('id'), $action, $assetname); + } + + /** + * Method to return a list of all categories that a user has permission for a given action + * + * @param string $component The component from which to retrieve the categories + * @param string $action The name of the section within the component from which to retrieve the actions. + * @return array List of categories that this group can do this action to (empty array if none). Categories must be published. + * @since 2.1.0 + */ + public function getAuthorisedCategories($component, $action) + { + // Brute force method: get all published category rows for the component and check each one + // TODO: Move to ORM-based models + $db = \App::get('db'); + $query = $db->getQuery() + ->select('c.id', 'id') + ->select('a.name', 'asset_name') + ->from('#__categories', 'c') + ->join('#__assets AS a', 'c.asset_id', 'a.id', 'inner') + ->whereEquals('c.extension', $component) + ->whereEquals('c.published', '1'); + $db->setQuery($query->toString()); + + $allCategories = $db->loadObjectList('id'); + + $allowedCategories = array(); + + foreach ($allCategories as $category) + { + if ($this->authorise($action, $category->asset_name)) + { + $allowedCategories[] = (int) $category->id; + } + } + + return $allowedCategories; + } + + /** + * Gets an array of the authorised access levels for the user + * + * @return array + * @since 2.1.0 + */ + public function getAuthorisedViewLevels() + { + if (is_null($this->authLevels)) + { + $this->authLevels = array(); + } + + if (empty($this->_authLevels)) + { + $this->authLevels = Access::getAuthorisedViewLevels($this->get('id')); + } + + return $this->authLevels; + } + + /** + * Gets an array of the authorised user groups + * + * @return array + * @since 2.1.0 + */ + public function getAuthorisedGroups() + { + if (is_null($this->authGroups)) + { + $this->authGroups = array(); + } + + if (empty($this->authGroups)) + { + $this->authGroups = Access::getGroupsByUser($this->get('id')); + } + + return $this->authGroups; + } + + /** + * Save data + * + * @return boolean + */ + public function save() + { + // Trigger the onUserBeforeSave event. + $data = $this->toArray(); + $isNew = $this->isNew(); + + // Allow an exception to be thrown. + try + { + $oldUser = self::oneOrNew($this->get('id')); + + // Trigger the onUserBeforeSave event. + $result = Event::trigger('user.onUserBeforeSave', array($oldUser->toArray(), $isNew, $data)); + + if (in_array(false, $result, true)) + { + // Plugin will have to raise its own error or throw an exception. + return false; + } + + // Get any set access groups + $groups = null; + + if ($this->hasAttribute('accessgroups')) + { + $groups = $this->get('accessgroups'); + + $this->removeAttribute('accessgroups'); + } + + // Save record + $result = parent::save(); + + if (!$result) + { + throw new Exception($this->getError()); + } + + // Update access groups + if ($groups && is_array($groups)) + { + Map::destroyByUser($this->get('id')); + + Map::addUserToGroup($this->get('id'), $groups); + } + + // In case it's a new user, we need to grab the ID + $data['id'] = $this->get('id'); + + // Fire the onUserAfterSave event + Event::trigger('user.onUserAfterSave', array($data, $isNew, $result, $this->getError())); + + $this->purgeCache(); + } + catch (Exception $e) + { + $this->addError($e->getMessage()); + + $result = false; + } + + return $result; + } + + /** + * Delete the record and associated data + * + * @return boolean False if error, True on success + */ + public function destroy() + { + $data = $this->toArray(); + + // Trigger the onUserBeforeDelete event + Event::trigger('user.onUserBeforeDelete', array($data)); + + // Remove associated data + if ($this->reputation->get('id')) + { + if (!$this->reputation->destroy()) + { + $this->addError($this->reputation->getError()); + return false; + } + } + + foreach ($this->tokens()->rows() as $token) + { + if (!$token->destroy()) + { + $this->addError($token->getError()); + return false; + } + } + + Map::destroyByUser($this->get('id')); + + // Attempt to delete the record + $result = parent::destroy(); + + if ($result) + { + // Trigger the onUserAfterDelete event + Event::trigger('user.onUserAfterDelete', array($data, true, $this->getError())); + } + + return $result; + } + + /** + * Parse a users name and set the name parts on the instance + * + * @return void + */ + private function parseName($key=null) + { + $name = $this->get('name'); + if ($name) + { + $firstname = ""; + $middlename = ""; + $lastname = ""; + + $words = array_map('trim', explode(' ', $this->get('name'))); + $count = count($words); + + if ($count == 1) + { + $firstname = $words[0]; + } + else if ($count == 2) + { + $firstname = $words[0]; + $lastname = $words[1]; + } + else if ($count == 3) + { + $firstname = $words[0]; + $middlename = $words[1]; + $lastname = $words[2]; + } + else + { + $firstname = $words[0]; + $lastname = $words[$count-1]; + $middlename = $words[1]; + + for ($i = 2; $i < $count-1; $i++) + { + $middlename .= ' ' . $words[$i]; + } + } + switch ($key) + { + case 'givenName': + return trim($firstname); + break; + case 'middleName': + return trim($middlename); + break; + case 'surname': + return trim($lastname); + break; + default: + return ''; + } + } + } +} diff --git a/core/libraries/Hubzero/Utility/Arr.php b/core/libraries/Hubzero/Utility/Arr.php new file mode 100644 index 00000000000..b9fe70f0d72 --- /dev/null +++ b/core/libraries/Hubzero/Utility/Arr.php @@ -0,0 +1,576 @@ + $v) + { + $array[$i] = (int) $v; + } + } + else + { + if ($default === null) + { + $array = array(); + } + elseif (is_array($default)) + { + self::toInteger($default, null); + $array = $default; + } + else + { + $array = array((int) $default); + } + } + } + + /** + * Utility function to map an array to a stdClass object. + * + * @param array &$array The array to map. + * @param string $class Name of the class to create + * @return object The object mapped from the given array + */ + public static function toObject(&$array, $class = 'stdClass') + { + $obj = new $class; + if (is_object($array)) + { + $array = (array)$array; + } + if (is_array($array)) + { + foreach ($array as $k => $v) + { + if (is_array($v)) + { + $obj->$k = self::toObject($v, $class); + } + else + { + $obj->$k = $v; + } + } + } + return $obj; + } + + /** + * Utility function to map an array to a string. + * + * @param array $array The array to map. + * @param string $inner_glue The glue (optional, defaults to '=') between the key and the value. + * @param string $outer_glue The glue (optional, defaults to ' ') between array elements. + * @param boolean $keepOuterKey True if final key should be kept. + * @return string The string mapped from the given array + */ + public static function toString($array = null, $inner_glue = '=', $outer_glue = ' ', $keepOuterKey = false) + { + $output = array(); + + if (is_array($array)) + { + foreach ($array as $key => $item) + { + if (is_array($item)) + { + if ($keepOuterKey) + { + $output[] = $key; + } + // This is value is an array, go and do it again! + $output[] = self::toString($item, $inner_glue, $outer_glue, $keepOuterKey); + } + else + { + $output[] = $key . $inner_glue . '"' . $item . '"'; + } + } + } + + return implode($outer_glue, $output); + } + + /** + * Utility function to map an object or array to an array + * + * @param mixed $item The source object or array + * @param boolean $recurse True to recurse through multi-level objects + * @param string $regex An optional regular expression to match on field names + * @return array The array mapped from the given object + */ + public static function fromObject($item, $recurse = true, $regex = null) + { + if (is_object($item)) + { + $result = array(); + foreach (get_object_vars($item) as $k => $v) + { + if (!$regex || preg_match($regex, $k)) + { + if ($recurse) + { + $result[$k] = self::fromObject($v, $recurse, $regex); + } + else + { + $result[$k] = $v; + } + } + } + } + elseif (is_array($item)) + { + $result = array(); + foreach ($item as $k => $v) + { + $result[$k] = self::fromObject($v, $recurse, $regex); + } + } + else + { + $result = $item; + } + return $result; + } + + /** + * Extracts a column from an array of arrays or objects + * + * @param array &$array The source array + * @param string $index The index of the column or name of object property + * @return array Column of values from the source array + */ + public static function getColumn(&$array, $index) + { + $result = array(); + + if (is_array($array)) + { + $n = count($array); + + for ($i = 0; $i < $n; $i++) + { + $item = &$array[$i]; + + if (is_array($item) && isset($item[$index])) + { + $result[] = $item[$index]; + } + elseif (is_object($item) && isset($item->$index)) + { + $result[] = $item->$index; + } + // else ignore the entry + } + } + return $result; + } + + /** + * Utility function to return a value from a named array or a specified default + * + * @param array &$array A named array + * @param string $name The key to search for + * @param mixed $default The default value to give if no key found + * @param string $type Return type for the variable (INT, FLOAT, STRING, WORD, BOOLEAN, ARRAY) + * @return mixed The value from the source array + */ + public static function getValue(&$array, $name, $default = null, $type = '') + { + // Initialise variables. + $result = null; + + if (isset($array[$name])) + { + $result = $array[$name]; + } + + // Handle the default case + if (is_null($result)) + { + $result = $default; + } + + // Handle the type constraint + switch (strtoupper($type)) + { + case 'INT': + case 'INTEGER': + // Only use the first integer value + @preg_match('/-?[0-9]+/', $result, $matches); + $result = @(int) $matches[0]; + break; + + case 'FLOAT': + case 'DOUBLE': + // Only use the first floating point value + @preg_match('/-?[0-9]+(\.[0-9]+)?/', $result, $matches); + $result = @(float) $matches[0]; + break; + + case 'BOOL': + case 'BOOLEAN': + $result = (bool) $result; + break; + + case 'ARRAY': + if (!is_array($result)) + { + $result = array($result); + } + break; + + case 'STRING': + $result = (string) $result; + break; + + case 'WORD': + $result = (string) preg_replace('#\W#', '', $result); + break; + + case 'NONE': + default: + // No casting necessary + break; + } + return $result; + } + + /** + * Method to determine if an array is an associative array. + * + * @param array $array An array to test. + * @return boolean True if the array is an associative array. + */ + public static function isAssociative($array) + { + if (is_array($array)) + { + foreach (array_keys($array) as $k => $v) + { + if ($k !== $v) + { + return true; + } + } + } + + return false; + } + + /** + * Pivots an array to create a reverse lookup of an array of scalars, arrays or objects. + * + * @param array $source The source array. + * @param string $key Where the elements of the source array are objects or arrays, the key to pivot on. + * @return array An array of arrays pivoted either on the value of the keys, or an individual key of an object or array. + */ + public static function pivot($source, $key = null) + { + $result = array(); + $counter = array(); + + foreach ($source as $index => $value) + { + // Determine the name of the pivot key, and its value. + if (is_array($value)) + { + // If the key does not exist, ignore it. + if (!isset($value[$key])) + { + continue; + } + + $resultKey = $value[$key]; + $resultValue = &$source[$index]; + } + elseif (is_object($value)) + { + // If the key does not exist, ignore it. + if (!isset($value->$key)) + { + continue; + } + + $resultKey = $value->$key; + $resultValue = &$source[$index]; + } + else + { + // Just a scalar value. + $resultKey = $value; + $resultValue = $index; + } + + // The counter tracks how many times a key has been used. + if (empty($counter[$resultKey])) + { + // The first time around we just assign the value to the key. + $result[$resultKey] = $resultValue; + $counter[$resultKey] = 1; + } + elseif ($counter[$resultKey] == 1) + { + // If there is a second time, we convert the value into an array. + $result[$resultKey] = array( + $result[$resultKey], + $resultValue, + ); + $counter[$resultKey]++; + } + else + { + // After the second time, no need to track any more. Just append to the existing array. + $result[$resultKey][] = $resultValue; + } + } + + unset($counter); + + return $result; + } + + /** + * Utility function to sort an array of objects on a given field + * + * @param array &$a An array of objects + * @param mixed $k The key (string) or a array of key to sort on + * @param mixed $direction Direction (integer) or an array of direction to sort in [1 = Ascending] [-1 = Descending] + * @param mixed $caseSensitive Boolean or array of booleans to let sort occur case sensitive or insensitive + * @param mixed $locale Boolean or array of booleans to let sort occur using the locale language or not + * @return array The sorted array of objects + */ + public static function sortObjects(&$a, $k, $direction = 1, $caseSensitive = true, $locale = false) + { + if (!is_array($locale) or !is_array($locale[0])) + { + $locale = array($locale); + } + + self::$sortCase = (array) $caseSensitive; + self::$sortDirection = (array) $direction; + self::$sortKey = (array) $k; + self::$sortLocale = $locale; + + usort($a, array(__CLASS__, '_sortObjects')); + + self::$sortCase = null; + self::$sortDirection = null; + self::$sortKey = null; + self::$sortLocale = null; + + return $a; + } + + /** + * Callback function for sorting an array of objects on a key + * + * @param array &$a An array of objects + * @param array &$b An array of objects + * @return integer Comparison status + */ + protected static function _sortObjects(&$a, &$b) + { + $key = self::$sortKey; + + for ($i = 0, $count = count($key); $i < $count; $i++) + { + if (isset(self::$sortDirection[$i])) + { + $direction = self::$sortDirection[$i]; + } + + if (isset(self::$sortCase[$i])) + { + $caseSensitive = self::$sortCase[$i]; + } + + if (isset(self::$sortLocale[$i])) + { + $locale = self::$sortLocale[$i]; + } + + $va = $a->{$key[$i]}; + $vb = $b->{$key[$i]}; + + if ((is_bool($va) or is_numeric($va)) and (is_bool($vb) or is_numeric($vb))) + { + $cmp = $va - $vb; + } + elseif ($caseSensitive) + { + $cmp = strcmp($va, $vb); + } + else + { + $cmp = strcasecmp($va, $vb); + } + + if ($cmp > 0) + { + + return $direction; + } + + if ($cmp < 0) + { + return -$direction; + } + } + + return 0; + } + + /** + * Multidimensional array safe unique test + * + * @param array $myArray The array to make unique. + * @return array + */ + public static function arrayUnique($myArray) + { + if (!is_array($myArray)) + { + return $myArray; + } + + foreach ($myArray as &$myvalue) + { + $myvalue = serialize($myvalue); + } + + $myArray = array_unique($myArray); + + foreach ($myArray as &$myvalue) + { + $myvalue = unserialize($myvalue); + } + + return $myArray; + } + + /** + * Filters keys from given array based on whitelist + * + * @param array $unfiltered Array to filter + * @param array $whitelist List of allowed keys + * @return array + */ + public static function filterKeys($unfiltered, $whitelist) + { + $filtered = array_filter($unfiltered, function($key) use ($whitelist) { + return in_array($key, $whitelist); + }, ARRAY_FILTER_USE_KEY); + + return $filtered; + } + + /** + * Returns value under given key and removes key from array + * + * @param array $array Array to pluck from + * @param string $name Key to search for + * @param mixed $default Default value to return if key not found + * @return mixed + */ + public static function pluck(&$array, $name, $default = null) + { + $value = static::getValue($array, $name, $default); + + unset($array[$name]); + + return $value; + } + + /** + * Function to randomly append pirate phrases to + * strings in an array. + * + * @codeCoverageIgnore + * @param array $data + * @return array + */ + public static function meMatey($data) + { + $pirate = array( + 'Ahoy', + 'Arrr!', + 'Bilge Rat!', + 'Feed the fishes', + 'Davy Jones\' Locker', + 'Jolly Roger', + 'Scurvy dog!', + 'Shiver me timbers!', + 'Walk the plank', + 'X Marks the spot', + 'Yo-ho-ho' + ); + + $nqs = count($pirate); + + foreach ($data as $key => $val) + { + if (is_string($val)) + { + $use = rand(0, $nqs-1); + + $val .= ' ' . $pirate[$use]; + } + + $data[$key] = $val; + } + + return $data; + } +} diff --git a/core/libraries/Hubzero/Utility/Composer.php b/core/libraries/Hubzero/Utility/Composer.php new file mode 100644 index 00000000000..e77b1fa9058 --- /dev/null +++ b/core/libraries/Hubzero/Utility/Composer.php @@ -0,0 +1,592 @@ +createComposer(self::$io, PATH_APP . '/composer.json', false, PATH_APP, true); + } + return true; + } + + /** + * Return the factory, ensuring Composer was set up already + * + * @return object Composer\Factory object in use + */ + private static function _getFactory() + { + self::_init(); + return self::$factory; + } + + /** + * Return composer object, ensuring it has been set up + * + * @return object Composer\Composer object representing this composer instance + */ + private static function _getComposer() + { + self::_init(); + return self::$composer; + } + + /** + * Reset and reinitialize Composer. + * This is required for doing most update and remove operations + * + * @return boolean Indicates success or failure + */ + private static function _resetComposer() + { + self::$composer = null; + self::$factory = null; + self::$repositoryManager = null; + self::$localRepository = null; + self::$remoteRepositories = null; + self::$io = null; + self::$installer = null; + self::$dispatcher = null; + return self::_init(); + } + + /** + * Return the IO object to interact with composer + * + * @return object Composer\IO object in use + */ + private static function _getIO() + { + self::_init(); + return self::$io; + } + + /** + * Return the repository manager + * + * @return object Composer\Repository\RepositoryManager in use + */ + private static function _getRepositoryManager() + { + self::_init(); + if (!self::$repositoryManager) + { + self::$repositoryManager = self::$composer->getRepositoryManager(); + } + return self::$repositoryManager; + } + + /** + * Return the local(installed) repository + * + * @return object Composer\Repository\RepositoryInterface containing the local repository + */ + private static function _getLocalRepository() + { + self::_init(); + if (!self::$localRepository) + { + self::$localRepository = self::_getRepositoryManager()->getLocalRepository(); + } + return self::$localRepository; + } + + /** + * Return an array of repositories containing remote packages + * + * @return array Array of Composer\Repository\RepositoryInterfaces containing remote packages + */ + private static function _getRemoteRepositories() + { + self::_init(); + if (!self::$remoteRepositories) + { + self::$remoteRepositories = self::_getRepositoryManager()->getRepositories(); + } + return self::$remoteRepositories; + } + + /** + * Return the installer after ensuring Composer is set up + * + * @return object Composer\Installer object in use + */ + private static function _getInstaller() + { + self::_init(); + if (!self::$installer) + { + self::$installer = Installer::create(self::_getIO(), self::_getComposer()); + } + return self::$installer; + } + + /** + * Return the dispatcher in use by composer + * + * @return object Composer\EventDispatcher\EventDispatcher in use + */ + private static function _getDispatcher() + { + self::_init(); + if (!self::$dispatcher) + { + self::$dispatcher = self::_getComposer()->getEventDispatcher(); + } + return self::$dispatcher; + } + + /** + * Return the JSON file object representing the composer.json file in use + * + * @return object Composer\Json\JsonFile in use + */ + private static function _getComposerJson() + { + self::_init(); + if (!self::$json) + { + $file = self::_getFactory()->getComposerFile(); + self::$json = new JsonFile($file); + } + return self::$json; + } + + /** + * Dispatch an event to composer + * + * @param string $command Command message being dispatched + * @return void + */ + private static function _dispatch($command) + { + $dispatcher = self::_getDispatcher(); + $commandEvent = new CommandEvent(PluginEvents::COMMAND, $command, self::_getIO(), self::_getIO()); + $dispatcher->dispatch($commandEvent->getName(), $commandEvent); + } + + /** + * Require a package/version by manipulating the composer.json file + * + * @param string $packageName The package name in the form of vendor/package + * @param string $constraint The version constraint string - see https://getcomposer.org/doc/articles/versions.md + * @return boolean Indicates if the operation succeeded + */ + private static function _requirePackage($packageName, $constraint = 'dev-master') + { + if (empty($packageName)) + { + return false; + } + self::_init(); + $json = self::_getComposerJson(); + $contents = file_get_contents($json->getPath()); + $manipulator = new JsonManipulator($contents); + if (!$manipulator->addLink('require', $packageName, $constraint, false)) + { + return false; + } + file_put_contents($json->getPath(), $manipulator->getContents()); + return true; + } + + /** + * Unrequire a package by manipulating the composer.json file + * + * @param string $apackageName The package name in the form of vendor/pacakage + * @return boolean Indicates if the operation succeeded + */ + private static function _unrequirePackage($packageName) + { + if (empty($packageName)) + { + return false; + } + self::_init(); + self::_dispatch('remove'); + $json = self::_getComposerJson(); + $contents = file_get_contents($json->getPath()); + $manipulator = new JsonManipulator($contents); + if (!$manipulator->removeSubNode('require', $packageName, false)) + { + return false; + } + file_put_contents($json->getPath(), $manipulator->getContents()); + return true; + } + + /** + * Update a package or list of packages + * + * @param array $packages List of packages to be updated + * @return boolean Indicates success or failure + */ + private static function _updatePackage($packageName) + { + self::_init(); + self::_dispatch('update'); + if (is_null($packageName)) + { + $package = array(); + } + elseif (is_string($packageName)) + { + $package = array($packageName); + } + else + { + $package = $packageName; + } + $installer = self::_getInstaller(); + $installer + ->setUpdate(true) + ->setUpdateWhitelist($package); + return $installer->run(); + } + + /** + * Return Composer's configuration + * + * @return object Composer\Config object in use by composer + */ + private static function _getConfig() + { + self::_init(); + return self::$composer->getConfig(); + } + + /** + * Updates all packages according to their version contraints + * + * @return boolean Indicates success or failure + */ + public static function updatePackages() + { + // Send empty array to update all packages + self::_updatePackage(array()); + } + + /** + * Install a package as a specific version + * + * @param string $packageName Name of the package to install + * @param string $constraint Version constraint string - see https://getcomposer.org/doc/articles/versions.md + * @return boolean Indicates success or frailure + */ + public static function installPackage($packageName, $constraint = 'dev-master') + { + if (self::_requirePackage($packageName, $constraint)) + { + self::_resetComposer(); + return self::_updatePackage($packageName); + } + return false; + } + + /** + * Remove a package + * + * @param string $packageName Name of the package to remove + * @return boolean Indicates success or failure + */ + public static function removePackage($packageName) + { + if (self::_unrequirePackage($packageName)) + { + self::_resetComposer(); + return self::_updatePackage($packageName); + } + return false; + } + + /** + * Get a list of packages that are installed + * + * @return array Array of Composer\Package\PackageInterface representing locally installed packages + */ + public static function getLocalPackages() + { + $localRepo = self::_getLocalRepository(); + return $localRepo->getPackages(); + } + + /** + * Get a list of remote packages + * + * @return array Array of Composer\Package\PackageInterface representing packages from remote repositories + */ + public static function getRemotePackages() + { + $remoteRepos = self::_getRemoteRepositories(); + $remotePackages = array(); + foreach ($remoteRepos as $repo) + { + if (method_exists($repo, "getRepoConfig")) + { + $config = $repo->getRepoConfig(); + if (is_array($config) && isset($config['type']) && $config['type'] != 'composer') + { + $packages = $repo->getPackages(); + foreach ($packages as $package) + { + $remotePackages[$package->getName()] = $package; + } + } + } + } + return $remotePackages; + } + + /** + * Get a list of available packages + * + * @return array Array of packages that are available in remote repositories but have no locally installed version + */ + public static function getAvailablePackages() + { + $availablePackages = self::getRemotePackages(); + $localPackages = self::getLocalPackages(); + foreach ($localPackages as $installedPackage) + { + unset($availablePackages[$installedPackage->getName()]); + } + return $availablePackages; + } + + /** + * Get list of remote repositories + * + * @return array Array of Composer\Repository\PackageRepository that composer can use + */ + public static function getRepositories() + { + $remoteRepos = self::_getRemoteRepositories(); + return $remoteRepos; + } + + /** + * Get a single repository by URL + * + * @param string $url URL of the repository to find + * @return mixed The Composer\Repository\PackageRepository with a URL matching the parameter, or null + */ + public static function getRepositoryByUrl($url) + { + $remoteRepos = self::_getRemoteRepositories(); + foreach ($remoteRepos as $repo) + { + $config = $repo->getRepoConfig(); + if ($config['url'] == $url) + { + return $repo; + } + } + return null; + } + + /** + * Return the configuration of a repository by its alias + * + * @param string $alias Alias of the repository, as found in the composer.json + * @return mixed The Composer\Repository\PackageRepository that matches, or null + */ + public static function getRepositoryConfigByAlias($alias) + { + $config = self::_getConfig(); + $repos = self::getRepositoryConfigs(); + if (isset($repos[$alias])) + { + return $repos[$alias]; + } + return null; + } + + /** + * Get repository configurations + * + * @return array Array of configurations for repositories + */ + public static function getRepositoryConfigs() + { + $config = self::_getConfig(); + $repos = $config->getRepositories(); + return $repos; + } + + /** + * Find available packages matching the given constraints + * + * @param string $packageName Name of the package to be found + * @param string $versionConstraint Version constraint for package - see https://getcomposer.org/doc/articles/versions.md + * @return mixed Composer\Package\PackageInterface or null + */ + public static function findRemotePackages($packageName, $versionConstraint) + { + $repoManager = self::_getRepositoryManager(); + return $repoManager->findPackages($packageName, $versionConstraint); + } + + /** + * Find an installed package matching given the constraints + * + * @param string $packageName Name of the package to be found + * @param string $versionConstraint Version constraint for package - see https://getcomposer.org/doc/articles/versions.md + * @return mixed Composer\Package\PackageInterface or null + */ + public static function findLocalPackages($packageName, $versionConstraint = null) + { + $localRepo = self::_getLocalRepository(); + return $localRepo->findPackages($packageName, $versionConstraint); + } + + /** + * Find an installed package by name alone + * + * @param string $packageName Name of the package to be found + * @return mixed Composer\Package\PackageInterface or null + */ + public static function findLocalPackage($packageName) + { + $localRepo = self::_getLocalRepository(); + return $localRepo->findPackage($packageName, '*'); + } + + /** + * Add a repository to the composer.json file + * + * @param string $alias The alias for the new repository + * @param string $json The JSON representing the new repository + * @return void + */ + public static function addRepository($alias, $json) + { + $config = self::_getConfig(); + $configSource = $config->getConfigSource(); + $value = JsonFile::parseJson($json); + $configSource->addRepository($alias, $value); + } + + /** + * Remove a repository from the composer.json file + * + * @param string $alias The alias of the repository to remove + * @return void + */ + public static function removeRepository($alias) + { + $config = self::_getConfig(); + $configSource = $config->getConfigSource(); + $configSource->removeRepository($alias); + } +} diff --git a/core/libraries/Hubzero/Utility/Cookie.php b/core/libraries/Hubzero/Utility/Cookie.php new file mode 100644 index 00000000000..f5624cf19a3 --- /dev/null +++ b/core/libraries/Hubzero/Utility/Cookie.php @@ -0,0 +1,79 @@ +name . ':' . $namespace); + + $key = \App::hash(''); + $crypt = new \Hubzero\Encryption\Encrypter( + new \Hubzero\Encryption\Cipher\Simple, + new \Hubzero\Encryption\Key('simple', $key, $key) + ); + $cookie = $crypt->encrypt(serialize($data)); + + // Determine whether cookie should be 'secure' or not + $secure = false; + $forceSsl = \Config::get('force_ssl', false); + + if (\App::isAdmin() && $forceSsl >= 1) + { + $secure = true; + } + else if (\App::isSite() && $forceSsl == 2) + { + $secure = true; + } + + // Set the actual cookie + setcookie($hash, $cookie, $lifetime, '/', '', $secure, true); + } + + /** + * Retrieve a cookie + * + * @param (string) $namespace - make sure the cookie name is unique + * @return (object) $cookie data + **/ + public static function eat($namespace) + { + $hash = \App::hash(\App::get('client')->name . ':' . $namespace); + + $key = \App::hash(''); + $crypt = new \Hubzero\Encryption\Encrypter( + new \Hubzero\Encryption\Cipher\Simple, + new \Hubzero\Encryption\Key('simple', $key, $key) + ); + + if ($str = \App::get('request')->getString($hash, '', 'cookie')) + { + $sstr = $crypt->decrypt($str); + $cookie = @unserialize($sstr); + + return (object)$cookie; + } + + return false; + } +} diff --git a/core/libraries/Hubzero/Utility/Date.php b/core/libraries/Hubzero/Utility/Date.php new file mode 100644 index 00000000000..a427cde85a2 --- /dev/null +++ b/core/libraries/Hubzero/Utility/Date.php @@ -0,0 +1,684 @@ + 'Etc/GMT-12', '-11' => 'Pacific/Midway', '-10' => 'Pacific/Honolulu', '-9.5' => 'Pacific/Marquesas', + '-9' => 'US/Alaska', '-8' => 'US/Pacific', '-7' => 'US/Mountain', '-6' => 'US/Central', '-5' => 'US/Eastern', '-4.5' => 'America/Caracas', + '-4' => 'America/Barbados', '-3.5' => 'Canada/Newfoundland', '-3' => 'America/Buenos_Aires', '-2' => 'Atlantic/South_Georgia', + '-1' => 'Atlantic/Azores', '0' => 'Europe/London', '1' => 'Europe/Amsterdam', '2' => 'Europe/Istanbul', '3' => 'Asia/Riyadh', + '3.5' => 'Asia/Tehran', '4' => 'Asia/Muscat', '4.5' => 'Asia/Kabul', '5' => 'Asia/Karachi', '5.5' => 'Asia/Calcutta', + '5.75' => 'Asia/Katmandu', '6' => 'Asia/Dhaka', '6.5' => 'Indian/Cocos', '7' => 'Asia/Bangkok', '8' => 'Australia/Perth', + '8.75' => 'Australia/West', '9' => 'Asia/Tokyo', '9.5' => 'Australia/Adelaide', '10' => 'Australia/Brisbane', + '10.5' => 'Australia/Lord_Howe', '11' => 'Pacific/Kosrae', '11.5' => 'Pacific/Norfolk', '12' => 'Pacific/Auckland', + '12.75' => 'Pacific/Chatham', '13' => 'Pacific/Tongatapu', '14' => 'Pacific/Kiritimati' + ); + + /** + * The DateTimeZone object for usage in rending dates as strings. + * + * @var object + */ + protected $_tz; + + /** + * Constructor. + * + * @param string $date String in a format accepted by strtotime(), defaults to "now". + * @param mixed $tz Time zone to be used for the date. + * @return void + * @throws Exception + */ + public function __construct($date = 'now', $tz = null, $ignoreDst = false) + { + // Create the base GMT and server time zone objects. + if (empty(self::$gmt) || empty(self::$stz)) + { + self::$gmt = new DateTimeZone('GMT'); + self::$stz = new DateTimeZone(@date_default_timezone_get()); + } + + $tz = self::getTimeZoneObject($tz, $ignoreDst); + + + // If the date is numeric assume a unix timestamp and convert it. + date_default_timezone_set('UTC'); + $date = is_numeric($date) ? date('c', $date) : $date; + + // Call the DateTime constructor. + parent::__construct($date, $tz); + + // reset the timezone for 3rd party libraries/extension that does not use Date + date_default_timezone_set(self::$stz->getName()); + + // Set the timezone object for access later. + $this->_tz = $tz; + } + + /** + * Magic method to access properties of the date given by class to the format method. + * + * @param string $name The name of the property. + * @return mixed A value if the property name is valid, null otherwise. + */ + public function __get($name) + { + $value = null; + + switch ($name) + { + case 'daysinmonth': + $value = $this->format('t', true); + break; + + case 'dayofweek': + $value = $this->format('N', true); + break; + + case 'dayofyear': + $value = $this->format('z', true); + break; + + case 'isleapyear': + $value = (boolean) $this->format('L', true); + break; + + case 'day': + $value = $this->format('d', true); + break; + + case 'hour': + $value = $this->format('H', true); + break; + + case 'minute': + $value = $this->format('i', true); + break; + + case 'second': + $value = $this->format('s', true); + break; + + case 'month': + $value = $this->format('m', true); + break; + + case 'ordinal': + $value = $this->format('S', true); + break; + + case 'week': + $value = $this->format('W', true); + break; + + case 'year': + $value = $this->format('Y', true); + break; + + default: + $trace = debug_backtrace(); + trigger_error( + 'Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], + E_USER_NOTICE + ); + } + + return $value; + } + + /** + * Magic method to render the date object in the format specified in the public + * static member Date::$format. + * + * @return string The date as a formatted string. + */ + public function __toString() + { + return (string) parent::format(self::$format); + } + + /** + * Translates day of week number to a string. + * + * @param integer $day The numeric day of the week. + * @param boolean $abbr Return the abbreviated day string? + * @return string The day of the week. + */ + public function dayToString($day, $abbr = false) + { + switch ($day) + { + case 0: + return $abbr ? \Lang::txt('SUN') : \Lang::txt('SUNDAY'); + case 1: + return $abbr ? \Lang::txt('MON') : \Lang::txt('MONDAY'); + case 2: + return $abbr ? \Lang::txt('TUE') : \Lang::txt('TUESDAY'); + case 3: + return $abbr ? \Lang::txt('WED') : \Lang::txt('WEDNESDAY'); + case 4: + return $abbr ? \Lang::txt('THU') : \Lang::txt('THURSDAY'); + case 5: + return $abbr ? \Lang::txt('FRI') : \Lang::txt('FRIDAY'); + case 6: + return $abbr ? \Lang::txt('SAT') : \Lang::txt('SATURDAY'); + } + } + + /** + * Proxy for new Date(). + * + * @param string $date String in a format accepted by strtotime(), defaults to "now". + * @param mixed $tz Time zone to be used for the date. + * @return object + */ + public static function of($date = 'now', $tz = null, $ignoreDst = false) + { + return new self($date, $tz, $ignoreDst); + } + + /** + * Get TimeZone object for setting the timezone on the provided time. + * + * @param mixed $tz Time zone in either string, offset number, or a TimeZone object. + * @param boolean $ignoreDst if set to true, will prevent the date from converting to DST. + * @return object + */ + public static function getTimeZoneObject($tz, $ignoreDst = false) + { + // If the time zone object is not set, attempt to build it. + if (!($tz instanceof DateTimeZone)) + { + if ($tz === null) + { + $tz = self::$gmt; + } + elseif (is_numeric($tz)) + { + // Translate from offset. + $tz = new DateTimeZone(self::$offsets[(string) $tz]); + } + elseif (is_string($tz)) + { + $tz = new DateTimeZone($tz); + } + } + + if ($ignoreDst) + { + $lastYear = self::of('-1 year')->toUnix(); + $currentDate = self::of('now')->toUnix(); + $transitions = $tz->getTransitions($lastYear, $currentDate); + $offsets = array_reduce($transitions, function($carry, $transition){ + if (empty($transition['isdst'])) + { + $carry[] = $transition['offset']; + } + return $carry; + }); + + if (!empty($offsets)) + { + $offset = $offsets[0]; + if (abs($offset) > 0) + { + // convert seconds to hours + $offset = $offset / 3600; + } + $tz = new DateTimeZone($offset); + } + } + return $tz; + } + + /** + * Gets the date as a formatted string in a local calendar. + * + * @param string $format The date format specification string (see {@link PHP_MANUAL#date}) + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param boolean $translate True to translate localised strings + * @return string The date string in the specified format format. + */ + public function calendar($format, $local = false, $translate = true) + { + return $this->format($format, $local, $translate); + } + + /** + * Get the time offset from GMT in hours or seconds. + * + * @param boolean $hours True to return the value in hours. + * @return float The time offset from GMT either in hours or in seconds. + */ + public function getOffsetFromGMT($hours = false) + { + return (float) $hours ? ($this->_tz->getOffset($this) / 3600) : $this->_tz->getOffset($this); + } + + /** + * Translates month number to a string. + * + * @param integer $month The numeric month of the year. + * @param boolean $abbr If true, return the abbreviated month string + * @return string The month of the year. + */ + public function monthToString($month, $abbr = false) + { + switch ($month) + { + case 1: + return $abbr ? \Lang::txt('JANUARY_SHORT') : \Lang::txt('JANUARY'); + case 2: + return $abbr ? \Lang::txt('FEBRUARY_SHORT') : \Lang::txt('FEBRUARY'); + case 3: + return $abbr ? \Lang::txt('MARCH_SHORT') : \Lang::txt('MARCH'); + case 4: + return $abbr ? \Lang::txt('APRIL_SHORT') : \Lang::txt('APRIL'); + case 5: + return $abbr ? \Lang::txt('MAY_SHORT') : \Lang::txt('MAY'); + case 6: + return $abbr ? \Lang::txt('JUNE_SHORT') : \Lang::txt('JUNE'); + case 7: + return $abbr ? \Lang::txt('JULY_SHORT') : \Lang::txt('JULY'); + case 8: + return $abbr ? \Lang::txt('AUGUST_SHORT') : \Lang::txt('AUGUST'); + case 9: + return $abbr ? \Lang::txt('SEPTEMBER_SHORT') : \Lang::txt('SEPTEMBER'); + case 10: + return $abbr ? \Lang::txt('OCTOBER_SHORT') : \Lang::txt('OCTOBER'); + case 11: + return $abbr ? \Lang::txt('NOVEMBER_SHORT') : \Lang::txt('NOVEMBER'); + case 12: + return $abbr ? \Lang::txt('DECEMBER_SHORT') : \Lang::txt('DECEMBER'); + } + } + + /** + * Method to wrap the setTimezone() function and set the internal + * time zone object. + * + * @param object $tz The new DateTimeZone object. + * @return object The old DateTimeZone object. + */ + public function setTimezone($tz) + { + if (!($tz instanceof DateTimeZone)) + { + $tz = new DateTimeZone($tz); + } + + $this->_tz = $tz; + + return parent::setTimezone($tz); + } + + /** + * Add to the date + * + * @param string $modifier + * @return object + */ + public function add($modifier) + { + return $this->modify('+' . $modifier); + } + + /** + * Subtract from the date + * + * @param string $modifier + * @return object + */ + public function subtract($modifier) + { + return $this->modify('-' . $modifier); + } + + /** + * Gets the date as an ISO 8601 string. IETF RFC 3339 defines the ISO 8601 format + * and it can be found at the IETF Web site. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * + * @return string The date string in ISO 8601 format. + * + * @link http://www.ietf.org/rfc/rfc3339.txt + * @since 11.1 + */ + public function toISO8601($local = false) + { + return $this->format(DateTime::RFC3339, $local, false); + } + + /** + * Gets the date as an SQL datetime string. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param object $dbo The database driver or null to use global driver + * @return string The date string in SQL datetime format. + */ + public function toSql($local = false, $dbo = null) + { + if ($dbo === null) + { + $dbo = \App::get('db'); + } + return $this->format($dbo->getDateFormat(), $local, false); + } + + /** + * Gets the date as an RFC 822 string. IETF RFC 2822 supercedes RFC 822 and its definition + * can be found at the IETF Web site. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @return string The date string in RFC 822 format. + */ + public function toRFC822($local = false) + { + return $this->format(DateTime::RFC2822, $local, false); + } + + /** + * Gets the date as UNIX time stamp. + * + * @return integer The date as a UNIX timestamp. + */ + public function toUnix() + { + return (int) parent::format('U'); + } + + /** + * Gets the date as UNIX time stamp. + * + * @param string $format The date format specification string (see {@link PHP_MANUAL#date}) + * @return string + */ + public function toLocal($format='', $ignoreDst = false) + { + $format = $format ?: self::$format; + + // get timezone idenfier from user setting otherwise user system + $tz = \User::getParam('timezone', \Config::get('offset')); + + // format date + return $this->toTimeZone($tz, $format, $ignoreDst); + } + + /** + * Function to explicitly convert a date to the timezone and format provided. + * @param mixed $timeZone The numeric key on the Date static offsets array (a short list of common timezones) + * or a timezone string accepted by the TimeZone PHP object (see {@link PHP_MANUAL#timezones}) + * @param string $format The date format specification string (see {@link PHP_MANUAL#date}) + * @return string + */ + public function toTimeZone($timeZone, $format = null, $ignoreDst = false) + { + $format = $format ?: parent::$format; + $timeZone = self::getTimeZoneObject($timeZone, $ignoreDst); + $this->setTimezone($timeZone); + return $this->format($format, true); + } + + /** + * Function to convert a static time into a relative measurement + * + * @param string $date The date to convert + * @param string $unit The optional unit of measurement to return + * if the value of the diff is greater than one + * @param string $time An optional time to compare to, defaults to now + * @return string The converted time string + */ + public function relative($unit = null, $time = null) + { + if (is_null($time)) + { + // Get now + $time = new self('now'); + } + + // Get the difference in seconds between now and the time + $diff = strtotime($time) - strtotime($this); + + // Less than a minute + if ($diff < 60) + { + return \Lang::txt('JLIB_HTML_DATE_RELATIVE_LESSTHANAMINUTE'); + } + + // Round to minutes + $diff = round($diff / 60); + + // 1 to 59 minutes + if ($diff < 60 || $unit == 'minute') + { + return \Lang::txts('JLIB_HTML_DATE_RELATIVE_MINUTES', $diff); + } + + // Round to hours + $diff = round($diff / 60); + + // 1 to 23 hours + if ($diff < 24 || $unit == 'hour') + { + return \Lang::txts('JLIB_HTML_DATE_RELATIVE_HOURS', $diff); + } + + // Round to days + $diff = round($diff / 24); + + // 1 to 6 days + if ($diff < 7 || $unit == 'day') + { + return \Lang::txts('JLIB_HTML_DATE_RELATIVE_DAYS', $diff); + } + + // Round to weeks + $diff = round($diff / 7); + + // 1 to 4 weeks + if ($diff <= 4 || $unit == 'week') + { + return \Lang::txts('JLIB_HTML_DATE_RELATIVE_WEEKS', $diff); + } + + // [!] HUBZERO - Added months + // Round to months + /*$diff = round($diff / 4); + + // 1 to 12 months + if ($diff <= 12 || $unit == 'month') + { + return \Lang::txt('%s months ago', $diff); + }*/ + + // [!] HUBZERO - Changed default to format "% days ago" + // Over a month, return the absolute time + $text = $this->_ago(strtotime($this), strtotime($time)); + + $parts = explode(' ', $text); + + $text = $parts[0] . ' ' . $parts[1]; + $text .= ($parts[2]) ? ' ' . $parts[2] . ' ' . $parts[3] : ''; + + return sprintf('%s ago', $text); + } + + /** + * Calculate how long ago a date was + * + * @param number $timestamp Date to convert + * @return string + */ + protected function _ago($timestamp, $current_time=null) + { + // Store the current time + if (is_null($current_time)) + { + // Get now + $current_time = new self('now'); + $current_time = strtotime($current_time); + } + + // Determine the difference, between the time now and the timestamp + $difference = $current_time - $timestamp; + + // Set the periods of time + $periods = array('second', 'minute', 'hour', 'day', 'week', 'month', 'year', 'decade'); + + // Set the number of seconds per period + $lengths = array(1, 60, 3600, 86400, 604800, 2630880, 31570560, 315705600); + + // Determine which period we should use, based on the number of seconds lapsed. + // If the difference divided by the seconds is more than 1, we use that. Eg 1 year / 1 decade = 0.1, so we move on + // Go from decades backwards to seconds + for ($val = count($lengths) - 1; ($val >= 0) && (($number = $difference / $lengths[$val]) <= 1); $val--) + { + // Do nothing... + } + + // Ensure the script has found a match + if ($val < 0) + { + $val = 0; + } + + // Determine the minor value, to recurse through + $new_time = $current_time - ($difference % $lengths[$val]); + + // Set the current value to be floored + $number = floor($number); + + // If required create a plural + if ($number != 1) + { + $periods[$val] .= 's'; + } + + // Return text + $text = sprintf("%d %s ", $number, $periods[$val]); + + // Ensure there is still something to recurse through, and we have not found 1 minute and 0 seconds. + if (($val >= 1) && (($current_time - $new_time) > 0)) + { + $text .= $this->_ago($new_time, $current_time); + } + + return $text; + } + + /** + * Gets the date as a formatted string. + * + * @param string $format The date format specification string (see {@link PHP_MANUAL#date}) + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param boolean $translate True to translate localised strings + * @return string The date string in the specified format format. + */ + public function format($format, $local = false, $translate = true) + { + if ($format == 'relative') + { + return $this->relative(); + } + + if ($translate) + { + // Do string replacements for date format options that can be translated. + $format = preg_replace('/(^|[^\\\])D/', "\\1" . self::DAY_ABBR, $format); + $format = preg_replace('/(^|[^\\\])l/', "\\1" . self::DAY_NAME, $format); + $format = preg_replace('/(^|[^\\\])M/', "\\1" . self::MONTH_ABBR, $format); + $format = preg_replace('/(^|[^\\\])F/', "\\1" . self::MONTH_NAME, $format); + } + + // If the returned time should not be local use GMT. + if ($local == false) + { + parent::setTimezone(self::$gmt); + } + + // Format the date. + $return = parent::format($format); + + if ($translate) + { + // Manually modify the month and day strings in the formatted time. + if (strpos($return, self::DAY_ABBR) !== false) + { + $return = str_replace(self::DAY_ABBR, $this->dayToString(parent::format('w'), true), $return); + } + + if (strpos($return, self::DAY_NAME) !== false) + { + $return = str_replace(self::DAY_NAME, $this->dayToString(parent::format('w')), $return); + } + + if (strpos($return, self::MONTH_ABBR) !== false) + { + $return = str_replace(self::MONTH_ABBR, $this->monthToString(parent::format('n'), true), $return); + } + + if (strpos($return, self::MONTH_NAME) !== false) + { + $return = str_replace(self::MONTH_NAME, $this->monthToString(parent::format('n')), $return); + } + } + + if ($local == false) + { + parent::setTimezone($this->_tz); + } + + return $return; + } +} diff --git a/core/libraries/Hubzero/Utility/Dns.php b/core/libraries/Hubzero/Utility/Dns.php new file mode 100644 index 00000000000..d0e279fdd6e --- /dev/null +++ b/core/libraries/Hubzero/Utility/Dns.php @@ -0,0 +1,120 @@ + '\1\2en', // ox + '/([m|l])ouse$/i' => '\1ice', // mouse, louse + '/(matr|vert|ind)ix|ex$/i' => '\1ices', // matrix, vertex, index + '/(x|ch|ss|sh)$/i' => '\1es', // search, switch, fix, box, process, address + '/([^aeiouy]|qu)y$/i' => '\1ies', // query, ability, agency + '/(hive)$/i' => '\1s', // archive, hive + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', // half, safe, wife + '/sis$/i' => 'ses', // basis, diagnosis + '/([ti])um$/i' => '\1a', // datum, medium + '/(p)erson$/i' => '\1eople', // person, salesperson + '/(m)an$/i' => '\1en', // man, woman, spokesman + '/(c)hild$/i' => '\1hildren', // child + '/(buffal|tomat)o$/i' => '\1\2oes', // buffalo, tomato + '/(bu|campu)s$/i' => '\1\2ses', // bus, campus + '/(alias|status|virus)$/i' => '\1es', // alias + '/(octop)us$/i' => '\1i', // octopus + '/(ax|cris|test)is$/i' => '\1es', // axis, crisis + '/s$/' => 's', // no change (compatibility) + '/$/' => 's', + ); + + /** + * Singular inflector rules. + * + * @var array + */ + protected static $singular_rules = array( + '/(virus)es$/i' => '\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias)es$/i' => '\1', + '/([octop|vir])i$/i' => '\1us', + '/(cris|ax|test)es$/i' => '\1is', + '/(shoe)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/(bus|campus)es$/i' => '\1', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1\2ovie', + '/(s)eries$/i' => '\1\2eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/([lr])ves$/i' => '\1f', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/([^f])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(p)eople$/i' => '\1\2erson', + '/(m)en$/i' => '\1an', + '/(s)tatuses$/i' => '\1\2tatus', + '/(c)hildren$/i' => '\1\2hild', + '/(n)ews$/i' => '\1\2ews', + '/([^us])s$/i' => '\1', + ); + + /** + * Gets the plural version of the given word + * + * @param string $word the word to pluralize + * @param int $count number of instances + * @return string the plural version of $word + */ + public static function pluralize($word, $count = 0) + { + $result = strval($word); + + // If a counter is provided, and that equals 1 + // return as singular. + if ($count === 1) + { + return $result; + } + + if (! static::is_countable($result)) + { + return $result; + } + + foreach (static::$plural_rules as $rule => $replacement) + { + if (preg_match($rule, $result)) + { + $result = preg_replace($rule, $replacement, $result); + break; + } + } + + return $result; + } + + /** + * Gets the singular version of the given word + * + * @param string $word the word to singularizelize + * @return string the plural version of $word + */ + public static function singularize($word) + { + $result = strval($word); + + if (! static::is_countable($result)) + { + return $result; + } + + foreach (static::$singular_rules as $rule => $replacement) + { + if (preg_match($rule, $result)) + { + $result = preg_replace($rule, $replacement, $result); + break; + } + } + + return $result; + } + + /** + * Checks if the given word has a plural version. + * + * @param string $word the word to check + * @return bool if the word is countable + */ + public static function is_countable($word) + { + return ! (in_array(strtolower(strval($word)), static::$uncountable_words)); + } +} diff --git a/core/libraries/Hubzero/Utility/Ip.php b/core/libraries/Hubzero/Utility/Ip.php new file mode 100644 index 00000000000..ecdca7750e8 --- /dev/null +++ b/core/libraries/Hubzero/Utility/Ip.php @@ -0,0 +1,128 @@ +ip = $ip; + $this->ipLong = ip2long($ip); + } + + /** + * Checks to see if the ip address is of valid form + * + * @param string $type The IP Protocol version to validate against + * @return bool + **/ + public function isValid($type = 'both') + { + $type = strtolower($type); + $flags = 0; + + if ($type === 'ipv4') + { + $flags = FILTER_FLAG_IPV4; + } + if ($type === 'ipv6') + { + $flags = FILTER_FLAG_IPV6; + } + + return (bool) filter_var($this->ip, FILTER_VALIDATE_IP, ['flags' => $flags]); + } + + /** + * Checks to see if the ip address is within a private range + * + * @return bool + **/ + public function isPrivate() + { + return ($this->isBetween('192.168.0.0', '192.168.255.255') || + $this->isBetween('10.0.0.0', '10.255.255.255') || + $this->isBetween('172.16.0.0', '172.31.255.255')); + } + + /** + * Checks to see if the ip address is between the given range (inclusive) + * + * @param string $low The low end check + * @param string $high The high end check + * @return bool + **/ + public function isBetween($low, $high) + { + return ($this->isAbove($low) && $this->isBelow($high)); + } + + /** + * Checks to see if the ip address is greater than or equal to the given + * + * @param string $threshold The comparison threshold + * @return bool + **/ + public function isAbove($threshold) + { + return $this->isRelativeTo($threshold); + } + + /** + * Checks to see if the ip address is less than or equal to the given + * + * @param string $threshold The comparison threshold + * @return bool + **/ + public function isBelow($threshold) + { + return $this->isRelativeTo($threshold, false); + } + + /** + * Checks to see if the ip address is less than or greater than the given + * + * @param string $threshold The comparison threshold + * @param bool $above Whether to check above or below + * @return bool + **/ + private function isRelativeTo($threshold, $above = true) + { + $threshold = ip2long($threshold); + + if (!$threshold) + { + throw new \RuntimeException('Invalid input, not an IP address'); + } + + return $above ? ($this->ipLong >= $threshold) : ($this->ipLong <= $threshold); + } +} diff --git a/core/libraries/Hubzero/Utility/Ldap.php b/core/libraries/Hubzero/Utility/Ldap.php new file mode 100644 index 00000000000..8e4d8e3d6a8 --- /dev/null +++ b/core/libraries/Hubzero/Utility/Ldap.php @@ -0,0 +1,1273 @@ + true, + 'fatal' => array(), + 'warnings' => array() + ); + + /** + * Success messages + * + * @var array + */ + private static $success = array( + 'success' => true, + 'added' => 0, + 'deleted' => 0, + 'modified' => 0, + 'unchanged' => 0 + ); + + /** + * Get the list of errors + * + * @return array + */ + public static function getErrors() + { + return self::$errors; + } + + /** + * Get the list of success + * + * @return array + */ + public static function getSuccess() + { + return self::$success; + } + + /** + * Get the LDAP connection + * + * @param integer $debug + * @return mixed + */ + public static function getLDO($debug = 0) + { + static $conn = false; + + if ($conn !== false) + { + return $conn; + } + + $ldap_params = \Component::params('com_system'); + + $acctman = $ldap_params->get('ldap_managerdn', 'cn=admin'); + $acctmanPW = $ldap_params->get('ldap_managerpw', ''); + $pldap = $ldap_params->get('ldap_primary', 'ldap://localhost'); + + $negotiate_tls = $ldap_params->get('ldap_tls', 0); + $port = '389'; + + if (!is_numeric($port)) + { + $port = '389'; + + $pattern = "/^\s*(ldap[s]{0,1}:\/\/|)([^:]*)(\:(\d+)|)\s*$/"; + + if (preg_match($pattern, $pldap, $matches)) + { + $pldap = $matches[2]; + + if ($matches[1] == 'ldaps://') + { + $negotiate_tls = false; + } + + if (isset($matches[4]) && is_numeric($matches[4])) + { + $port = $matches[4]; + } + } + } + + $conn = ldap_connect($pldap, $port); + + if ($conn === false) + { + if ($debug) + { + \Log::debug("getLDO(): ldap_connect($pldap,$port) failed. [" . posix_getpid() . "] " . ldap_error($conn)); + } + + return false; + } + + if ($debug) + { + \Log::debug("getLDO(): ldap_connect($pldap,$port) success. "); + } + + if (ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3) == false) + { + if ($debug) + { + \Log::debug("getLDO(): ldap_set_option(LDAP_OPT_PROTOCOL_VERSION, 3) failed: " . ldap_error($conn)); + } + + $conn = false; + return false; + } + + if ($debug) + { + \Log::debug("getLDO(): ldap_set_option(LDAP_OPT_PROTOCOL_VERSION, 3) success."); + } + + if (ldap_set_option($conn, LDAP_OPT_RESTART, 1) == false) + { + if ($debug) + { + \Log::debug("getLDO(): ldap_set_option(LDAP_OPT_RESTART, 1) failed: " . ldap_error($conn)); + } + + $conn = false; + return false; + } + + if ($debug) + { + \Log::debug("getLDO(): ldap_set_option(LDAP_OPT_RESTART, 1) success."); + } + + if (!ldap_set_option($conn, LDAP_OPT_REFERRALS, false)) + { + if ($debug) + { + \Log::debug("getLDO(): ldap_set_option(LDAP_OPT_REFERRALS, 0) failed: " . ldap_error($conn)); + } + + $conn = false; + return false; + } + + if ($debug) + { + \Log::debug("getLDO(): ldap_set_option(LDAP_OPT_REFERRALS, 0) success."); + } + + if ($negotiate_tls) + { + if (!ldap_start_tls($conn)) + { + if ($debug) + { + \Log::debug("getLDO(): ldap_start_tls() failed: " . ldap_error($conn)); + } + + $conn = false; + return false; + } + + if ($debug) + { + \Log::debug("getLDO(): ldap_start_tls() success."); + } + } + + if (ldap_bind($conn, $acctman, $acctmanPW) == false) + { + $err = ldap_errno($conn); + $errstr = ldap_error($conn); + $errstr2 = ldap_err2str($err); + + if ($debug) + { + \Log::debug("getLDO(): ldap_bind($acctman) failed. [" . posix_getpid() . "] " . $errstr); + } + + $conn = false; + return false; + } + + if ($debug) + { + \Log::debug("getLDO(): ldap_bind() success."); + } + + return $conn; + } + + /** + * Sync a user's info to LDAP + * + * @param mixed $user + * @return boolean + */ + public static function syncUser($user) + { + $db = \App::get('db'); + + if (empty($db)) + { + self::$errors['fatal'][] = 'Error connecting to the database'; + return false; + } + + $conn = self::getLDO(); + + if (empty($conn)) + { + self::$errors['fatal'][] = 'LDAP connection failed'; + return false; + } + + $query = "SELECT u.id AS uidNumber, u.username AS uid, u.name AS cn, " . + " p.gidNumber, u.homeDirectory, u.loginShell, " . + " pwd.passhash AS userPassword, pwd.shadowLastChange, pwd.shadowMin, pwd.shadowMax, pwd.shadowWarning, " . + " pwd.shadowInactive, pwd.shadowExpire, pwd.shadowFlag " . + " FROM #__users AS u " . + " LEFT JOIN #__users_password AS pwd ON u.id = pwd.user_id " . + " LEFT JOIN #__xprofiles AS p ON u.id = p.uidNumber "; + + if (is_numeric($user) && $user >= 0) + { + $query .= " WHERE u.id = " . $db->quote($user) . " LIMIT 1;"; + } + else + { + $query .= " WHERE u.username = " . $db->quote($user) . " LIMIT 1;"; + } + + $db->setQuery($query); + $dbinfo = $db->loadAssoc(); + + if (!empty($dbinfo)) + { + // Don't sync usernames that are negative numbers (these are auth_link temp accounts) + if (is_numeric($dbinfo['uid']) && $dbinfo['uid'] <= 0) + { + return false; + } + + // Make sure we have a name + // If one isn't set, make it the same as the username + if (!trim($dbinfo['cn']) && $dbinfo['uid']) + { + $dbinfo['cn'] = $dbinfo['uid']; + + $query = "UPDATE `#__users` SET `name`=" . $db->quote($dbinfo['uid']) . " WHERE `id`=" . $db->quote($dbinfo['uidNumber']) . ";"; + $db->setQuery($query); + $db->query(); + } + + $query = "SELECT host FROM `#__xprofiles_host` WHERE uidNumber = " . $db->quote($dbinfo['uidNumber']) . ";"; + $db->setQuery($query); + $dbinfo['host'] = $db->loadColumn(); + } + + $ldap_params = \Component::params('com_system'); + $hubLDAPBaseDN = $ldap_params->get('ldap_basedn', ''); + + if (is_numeric($user) && $user >= 0) + { + $dn = 'ou=users,' . $hubLDAPBaseDN; + $filter = '(|(uidNumber=' . $user . ')(uid=' . $dbinfo['uid'] . '))'; + } + else + { + $dn = "uid=$user,ou=users," . $hubLDAPBaseDN; + $filter = '(objectclass=*)'; + } + + $reqattr = array( + 'uidNumber','uid','cn','gidNumber','homeDirectory','loginShell','userPassword','shadowLastChange', + 'shadowMin','shadowMax','shadowWarning','shadowInactive','shadowExpire','shadowFlag', 'host' + ); + + $entry = ldap_search($conn, $dn, $filter, $reqattr, 0, 0, 0); + $count = ($entry) ? ldap_count_entries($conn, $entry) : 0; + + // If there was a database entry, but there was no ldap entry, create the ldap entry + if (!empty($dbinfo) && ($count <= 0)) + { + $dn = "uid=" . $dbinfo['uid'] . ",ou=users," . $hubLDAPBaseDN; + + $entry = array(); + $entry['objectclass'][] = 'top'; + $entry['objectclass'][] = 'account'; // MUST uid + $entry['objectclass'][] = 'posixAccount'; // MUST cn,gidNumber,homeDirectory,uidNumber + $entry['objectclass'][] = 'shadowAccount'; + + foreach ($dbinfo as $key => $value) + { + if (is_array($value) && $value != array()) + { + $entry[$key] = $value; + } + else if (!is_array($value) && $value != '') + { + $entry[$key] = $value; + } + } + + if (empty($entry['uid']) || empty($entry['cn']) || empty($entry['gidNumber'])) + { + self::$errors['warning'][] = "User {$dbinfo['uid']} missing one of uid, cn, or gidNumber"; + return false; + } + + if (empty($entry['homeDirectory']) || empty($entry['uidNumber'])) + { + self::$errors['warning'][] = "User {$dbinfo['uid']} missing one of homeDirectory or uidNumber"; + return false; + } + + $result = ldap_add($conn, $dn, $entry); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + return false; + } + else + { + ++self::$success['added']; + return true; + } + } + + $ldapinfo = null; + + if ($count > 0) + { + $firstentry = ldap_first_entry($conn, $entry); + + $attr = ldap_get_attributes($conn, $firstentry); + + if (!empty($attr)) + { + foreach ($reqattr as $key) + { + unset($attr[$key]['count']); + + if (isset($attr[$key][0])) + { + if (count($attr[$key]) <= 1) + { + $ldapinfo[$key] = $attr[$key][0]; + } + else + { + $ldapinfo[$key] = $attr[$key]; + } + } + else + { + $ldapinfo[$key] = null; + } + } + } + } + + // If there was no database entry, and there was no ldap entry, nothing to do + if (empty($dbinfo) && empty($ldapinfo)) + { + return true; + } + + // If there was no database entry, but there was an ldap entry, delete the ldap entry + if (!empty($ldapinfo) && empty($dbinfo)) + { + $dn = "uid=" . $ldapinfo['uid'] . ",ou=users," . $hubLDAPBaseDN; + + $result = ldap_delete($conn, $dn); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + return false; + } + else + { + ++self::$success['deleted']; + return true; + } + } + + // Otherwise update the ldap entry + + if (!empty($ldapinfo['host']) && !is_array($ldapinfo['host'])) + { + $ldapinfo['host'] = array($ldapinfo['host']); + } + + $entry = array(); + + foreach ($dbinfo as $key => $value) + { + if ($ldapinfo[$key] != $dbinfo[$key]) + { + if ($dbinfo[$key] === null) + { + $entry[$key] = array(); + } + else + { + $entry[$key] = is_array($dbinfo[$key]) ? $dbinfo[$key] : array($dbinfo[$key]); + } + } + } + + if (empty($entry)) + { + ++self::$success['unchanged']; + return true; + } + + $dn = "uid=" . $ldapinfo['uid'] . ",ou=users," . $hubLDAPBaseDN; + + // See if we're changing uid...if so, we need to do a rename + if (array_key_exists('uid', $entry)) + { + $result = ldap_rename($conn, $dn, 'uid='.$entry['uid'][0], 'ou=users,'.$hubLDAPBaseDN, true); + + // Set aside new uid and unset from attributes needing to be changed + $newUid = $entry['uid'][0]; + unset($entry['uid']); + + // See if we have any items left + if (empty($entry)) + { + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + return false; + } + else + { + ++self::$success['modified']; + return true; + } + } + + // Build new dn + $dn = "uid=" . $newUid . ",ou=users," . $hubLDAPBaseDN; + } + + // Now do the modify + $result = ldap_modify($conn, $dn, $entry); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + return false; + } + else + { + ++self::$success['modified']; + return true; + } + } + + /** + * Sync a group's info to LDAP + * + * @param mixed $group + * @return boolean + */ + public static function syncGroup($group) + { + $db = \App::get('db'); + + if (empty($db)) + { + self::$errors['fatal'][] = 'Error connecting to the database'; + return false; + } + + $conn = self::getLDO(); + + if (empty($conn)) + { + self::$errors['fatal'][] = 'LDAP connection failed'; + return false; + } + + $query = "SELECT g.gidNumber, g.cn, g.description FROM `#__xgroups` AS g "; + + if (is_numeric($group) && ($group >= 0)) + { + $query .= " WHERE g.gidNumber = " . $db->quote($group) . " LIMIT 1;"; + } + else + { + $query .= " WHERE g.cn = " . $db->quote($group) . " LIMIT 1;"; + } + + $db->setQuery($query); + $dbinfo = $db->loadAssoc(); + + if (!empty($dbinfo)) + { + $query = "SELECT DISTINCT(u.username) AS memberUid FROM `#__xgroups_members` AS gm, `#__users` AS u WHERE gm.gidNumber = " . $db->quote($dbinfo['gidNumber']) . " AND gm.uidNumber=u.id;"; + $db->setQuery($query); + $dbinfo['memberUid'] = $db->loadColumn(); + } + + $ldap_params = \Component::params('com_system'); + $hubLDAPBaseDN = $ldap_params->get('ldap_basedn', ''); + + if (isset($dbinfo['gidNumber']) || (is_numeric($group) && $group >= 0)) + { + $dn = 'ou=groups,' . $hubLDAPBaseDN; + $filter = '(gidNumber=' . ((isset($dbinfo['gidNumber'])) ? $dbinfo['gidNumber'] : $group) . ')'; + } + else + { + $dn = "cn=" . $group . ",ou=groups," . $hubLDAPBaseDN; + $filter = '(objectclass=*)'; + } + + $reqattr = array('gidNumber','cn','description','memberUid'); + + $entry = ldap_search($conn, $dn, $filter, $reqattr, 0, 0, 0); + $count = ($entry) ? ldap_count_entries($conn, $entry) : 0; + + // If there was a database entry, but there was no ldap entry, create the ldap entry + if (!empty($dbinfo) && ($count <= 0)) + { + $dn = "cn=" . $dbinfo['cn'] . ",ou=groups," . $hubLDAPBaseDN; + + $entry = array(); + $entry['objectclass'][] = 'top'; + $entry['objectclass'][] = 'posixGroup'; + + foreach ($dbinfo as $key => $value) + { + if (is_array($value) && $value != array()) + { + $entry[$key] = $value; + } + else if (!is_array($value) && $value != '') + { + $entry[$key] = $value; + } + } + + $result = ldap_add($conn, $dn, $entry); + + if ($result !== true) + { + $result = ldap_add($conn, $dn, $entry); + self::$errors['warning'][] = ldap_error($conn); + return false; + } + else + { + ++self::$success['added']; + return true; + } + } + + $ldapinfo = null; + + $count = ($entry) ? ldap_count_entries($conn, $entry) : 0; + + if ($count > 0) + { + $firstentry = ldap_first_entry($conn, $entry); + + $attr = ldap_get_attributes($conn, $firstentry); + + if (!empty($attr) && $attr['count'] > 0) + { + foreach ($reqattr as $key) + { + unset($attr[$key]['count']); + + if (isset($attr[$key][0])) + { + if (count($attr[$key]) <= 1) + { + $ldapinfo[$key] = $attr[$key][0]; + } + else + { + $ldapinfo[$key] = $attr[$key]; + } + } + else + { + $ldapinfo[$key] = null; + } + } + } + } + + // If there was no database entry, and there was no ldap entry, nothing to do + if (empty($dbinfo) && empty($ldapinfo)) + { + return true; + } + + // If there was no database entry, but there was an ldap entry, delete the ldap entry + if (!empty($ldapinfo) && empty($dbinfo)) + { + $dn = "cn=" . $ldapinfo['cn'] . ",ou=groups," . $hubLDAPBaseDN; + + $result = ldap_delete($conn, $dn); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + return false; + } + else + { + ++self::$success['deleted']; + return true; + } + } + + // Otherwise update the ldap entry + $entry = array(); + + if (!empty($ldapinfo['memberUid']) && !is_array($ldapinfo['memberUid'])) + { + $ldapinfo['memberUid'] = array($ldapinfo['memberUid']); + } + + foreach ($dbinfo as $key => $value) + { + if ($ldapinfo[$key] != $dbinfo[$key]) + { + if ($dbinfo[$key] === null) + { + $entry[$key] = array(); + } + else + { + $entry[$key] = $dbinfo[$key]; + } + } + } + + if (empty($entry)) + { + ++self::$success['unchanged']; + return true; + } + + $dn = "cn=" . $ldapinfo['cn'] . ",ou=groups," . $hubLDAPBaseDN; + + // See if we're changing cn...if so, we need to do a rename + if (array_key_exists('cn', $entry)) + { + $result = ldap_rename($conn, $dn, 'cn='.$entry['cn'], 'ou=groups,'.$hubLDAPBaseDN, true); + + // Set aside new uid and unset from attributes needing to be changed + $newCn = $entry['cn']; + unset($entry['cn']); + + // See if we have any items left + if (empty($entry)) + { + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + return false; + } + else + { + ++self::$success['modified']; + return true; + } + } + + // Build new dn + $dn = "cn=" . $newCn . ",ou=groups," . $hubLDAPBaseDN; + } + + // Now do the modify + $result = ldap_modify($conn, $dn, $entry); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + return false; + } + else + { + ++self::$success['modified']; + return true; + } + } + + /** + * Add members to a group + * + * @param mixed $group + * @param array $members + * @return boolean + */ + public static function addGroupMemberships($group, $members) + { + self::changeGroupMemberships($group, $members, array()); + } + + /** + * Remove members from a group + * + * @param mixed $group + * @param array $members + * @return boolean + */ + public static function removeGroupMemberships($group, $members) + { + self::changeGroupMemberships($group, array(), $members); + } + + /** + * Makes changes to a group + * + * @param mixed $group + * @param array $members + * @return boolean + */ + public static function changeGroupMemberships($group,$add,$delete) + { + $db = \App::get('db'); + + if (empty($db)) + { + return false; + } + + $conn = self::getLDO(); + + if (empty($conn)) + { + return false; + } + + $ldap_params = \Component::params('com_system'); + $hubLDAPBaseDN = $ldap_params->get('ldap_basedn', ''); + + if (is_numeric($group) && $group >= 0) + { + $dn = 'ou=groups,' . $hubLDAPBaseDN; + $filter = '(gidNumber=' . $group . ')'; + } + else + { + $dn = "cn=$group,ou=groups," . $hubLDAPBaseDN; + $filter = '(objectclass=*)'; + } + + $reqattr = array('gidNumber','cn'); + + $entry = ldap_search($conn, $dn, $filter, $reqattr, 0, 0, 0); + + $count = ldap_count_entries($conn, $entry); + + // If there was a database entry, but there was no ldap entry, create the ldap entry + if ($count <= 0) + { + return false; + } + + $ldapinfo = null; + + if ($count > 0) + { + $firstentry = ldap_first_entry($conn, $entry); + + $attr = ldap_get_attributes($conn, $firstentry); + + if (!empty($attr) && $attr['count'] > 0) + { + foreach ($reqattr as $key) + { + unset($attr[$key]['count']); + + if (isset($attr[$key][0])) + { + if (count($attr[$key]) <= 2) + { + $ldapinfo[$key] = $attr[$key][0]; + } + else + { + $ldapinfo[$key] = $attr[$key]; + } + } + else + { + $ldapinfo[$key] = null; + } + } + } + } + + if (empty($ldapinfo)) + { + return false; + } + + if (!empty($add)) + { + $add = array_map( array($db, "Quote"), $add); + $addin = implode(",", $add); + + if (!empty($addin)) + { + $query = "SELECT username FROM `#__users` WHERE id IN ($addin) OR username IN ($addin);"; + $db->setQuery($query); + $add = $db->loadColumn(); + } + + $adds = array(); + + foreach ($add as $memberUid) + { + $adds['memberUid'][] = $memberUid; + } + + if (ldap_mod_add($conn, $dn, $adds) == false) + { + // if bulk add fails, try individual + foreach ($add as $memberUid) + { + ldap_mod_add($conn, $dn, array('memberUid' => $memberUid)); + } + } + } + + if (!empty($delete)) + { + $delete = array_map( array($db, "Quote"), $delete); + $deletein = implode(",", $delete); + + if (!empty($deletein)) + { + $query = "SELECT username FROM `#__users` WHERE id IN ($deletein) OR username IN ($deletein);"; + $db->setQuery($query); + $delete = $db->loadColumn(); + } + + $deletes = array(); + + foreach ($delete as $memberUid) + { + $deletes['memberUid'][] = $memberUid; + } + + ldap_mod_del($conn, $dn, $deletes); + } + } + + /** + * Sync all groups + * + * @return boolean + */ + public static function syncAllGroups() + { + // @TODO: chunk this to 1000 groups at a time + + $db = \App::get('db'); + + $query = "SELECT gidNumber FROM `#__xgroups`;"; + + $db->setQuery($query); + + $result = $db->loadColumn(); + + if ($result === false) + { + return false; + } + + foreach ($result as $row) + { + self::syncGroup($row); + + if (is_array(self::$errors['fatal']) && !empty(self::$errors['fatal'][0])) + { + // If there's a fatal error, go ahead and stop + return self::$errors; + } + } + + if (!empty(self::$errors['fatal'][0]) || !empty(self::$errors['warning'][0])) + { + return self::$errors; + } + else + { + return self::$success; + } + } + + /** + * Sync all users + * + * @return boolean + */ + public static function syncAllUsers() + { + // @TODO: chunk this to 1000 users at a time + + $db = \App::get('db'); + + $query = "SELECT id FROM `#__users`;"; + + $db->setQuery($query); + + $result = $db->loadColumn(); + + if ($result === false) + { + return false; + } + + foreach ($result as $row) + { + self::syncUser($row); + + if (is_array(self::$errors['fatal']) && !empty(self::$errors['fatal'][0])) + { + // If there's a fatal error, go ahead and stop + return self::$errors; + } + } + + if (!empty(self::$errors['fatal'][0]) || !empty(self::$errors['warning'][0])) + { + return self::$errors; + } + else + { + return self::$success; + } + } + + /** + * Remove all groups + * + * @return array + */ + public static function deleteAllGroups() + { + $conn = self::getLDO(); + + if (empty($conn)) + { + self::$errors['fatal'][] = 'LDAP connection failed'; + return self::$errors; + } + + // delete all old hubGroup schema based group entries + $ldap_params = \Component::params('com_system'); + $hubLDAPBaseDN = $ldap_params->get('ldap_basedn', ''); + + $dn = "ou=groups," . $hubLDAPBaseDN; + $filter = '(objectclass=hubGroup)'; + + $sr = ldap_search($conn, $dn, $filter, array('gid','cn'), 0, 0, 0); + + $gids = array(); + + if ($sr !== false) + { + if (ldap_count_entries($conn, $sr) !== false) + { + $entry = ldap_first_entry($conn, $sr); + + while ($entry !== false) + { + $attr = ldap_get_attributes($conn, $entry); + + if (array_key_exists('gid', $attr)) + { + $gids[] = "gid=" . $attr['gid'][0] . "," . "ou=groups," . $hubLDAPBaseDN; + } + else if (array_key_exists('cn', $attr)) + { + $gids[] = "cn=" . $attr['cn'][0] . "," . "ou=groups," . $hubLDAPBaseDN; + } + + $entry = ldap_next_entry($conn, $entry); + } + } + } + + foreach ($gids as $giddn) + { + $result = ldap_delete($conn, $giddn); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + } + else + { + ++self::$success['deleted']; + } + } + + // delete all entries that have mysql counterparts + // @TODO: chunk this to 1000 groups at a time + + $db = \App::get('db'); + + $query = "SELECT cn FROM `#__xgroups`;"; + + $db->setQuery($query); + + $result = $db->loadColumn(); + + if ($result === false) + { + return false; + } + + foreach ($result as $row) + { + $dn = "cn=$row," . "ou=groups," . $hubLDAPBaseDN; + + // Added this search because the delete will error on a delete of a non existent object + $sr = ldap_search($conn, "ou=groups," . $hubLDAPBaseDN, "cn=$row", array('cn'), 0, 0, 0); + if (($sr !== false) and ldap_count_entries($conn, $sr) == 1) + { + $result = ldap_delete($conn, $dn); + + if ($result !== true) + { + // Don't report errors for "not such object" warnings + if (ldap_errno($conn) != 32) + { + self::$errors['warning'][] = ldap_error($conn); + } + } + else + { + ++self::$success['deleted']; + } + } + } + + // Delete any remaining items with gid > 1000 + $dn = "ou=groups," . $hubLDAPBaseDN; + $filter = '(&(objectclass=posixGroup)(gidNumber>=1000))'; + + $sr = ldap_search($conn, $dn, $filter, array('cn'), 0, 0, 0); + + $gids = array(); + + if ($sr !== false) + { + if (ldap_count_entries($conn, $sr) !== false) + { + $entry = ldap_first_entry($conn, $sr); + + while ($entry !== false) + { + $attr = ldap_get_attributes($conn, $entry); + + $gids[] = $attr['cn'][0]; + + $entry = ldap_next_entry($conn, $entry); + } + } + } + + foreach ($gids as $gid) + { + $dn = "cn=$gid," . "ou=groups," . $hubLDAPBaseDN; + $result = ldap_delete($conn, $dn); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + } + else + { + ++self::$success['deleted']; + } + } + + if (!empty(self::$errors['fatal'][0]) || !empty(self::$errors['warning'][0])) + { + return self::$errors; + } + else + { + return self::$success; + } + } + + /** + * Remove all users + * + * @return array + */ + public static function deleteAllUsers() + { + $conn = self::getLDO(); + + if (empty($conn)) + { + self::$errors['fatal'][] = 'LDAP connection failed'; + return self::$errors; + } + + // delete all old hubAccount schema based user entries + $ldap_params = \Component::params('com_system'); + $hubLDAPBaseDN = $ldap_params->get('ldap_basedn', ''); + + $dn = "ou=users," . $hubLDAPBaseDN; + $filter = '(objectclass=hubAccount)'; + + $sr = ldap_search($conn, $dn, $filter, array('uid'), 0, 0, 0); + + $uids = array(); + + if ($sr !== false) + { + if (ldap_count_entries($conn, $sr) !== false) + { + $entry = ldap_first_entry($conn, $sr); + + while ($entry !== false) + { + $attr = ldap_get_attributes($conn, $entry); + + $uids[] = $attr['uid'][0]; + + $entry = ldap_next_entry($conn, $entry); + } + } + } + + foreach ($uids as $uid) + { + $dn = "uid=$uid," . "ou=users," . $hubLDAPBaseDN; + $result = ldap_delete($conn, $dn); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + } + else + { + ++self::$success['deleted']; + } + } + + // delete all entries that have mysql counterparts + // @TODO: chunk this to 1000 groups at a time + $db = \App::get('db'); + + // Negative numbers exist as usernames for placeholders, these aren't in ldap + // In fact we can't even search for them without causing an error + $query = "SELECT username FROM `#__users` where (username not REGEXP '^-?\d+$' and username REGEXP '^[a-zA-Z]')"; + + $db->setQuery($query); + + $result = $db->loadColumn(); + + if ($result === false) + { + return false; + } + + foreach ($result as $row) + { + $dn = "uid=$row," . "ou=users," . $hubLDAPBaseDN; + + // see if item to be deleted is there + $count = 0; + $sr = ldap_search($conn, "ou=users," . $hubLDAPBaseDN, "uid=$row"); + + if ($sr !== false) + { + if (ldap_count_entries($conn, $sr) !== false) + { + $count = ldap_count_entries($conn, $sr); + } + } + + if ($count > 0) + { + $result = ldap_delete($conn, $dn); + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + } + else + { + ++self::$success['deleted']; + } + } + } + + // delete any remaining items with gid > 1000 + $dn = "ou=users," . $hubLDAPBaseDN; + $filter = '(&(objectclass=posixAccoiunt)(uidNumber>=1000))'; + + $sr = ldap_search($conn, $dn, $filter, array('uid'), 0, 0, 0); + + $uids = array(); + + if ($sr !== false) + { + if (ldap_count_entries($conn, $sr) !== false) + { + $entry = ldap_first_entry($conn, $sr); + + while ($entry !== false) + { + $attr = ldap_get_attributes($conn, $firstentry); + + $uids[] = $attr['uid'][0]; + + $entry = ldap_next_entry($conn, $entry); + } + } + } + + foreach ($uids as $uid) + { + $dn = "uid=$uid," . "ou=users," . $hubLDAPBaseDN; + $result = ldap_delete($conn, $dn); + + if ($result !== true) + { + self::$errors['warning'][] = ldap_error($conn); + } + else + { + ++self::$success['deleted']; + } + } + + if (!empty(self::$errors['fatal'][0]) || !empty(self::$errors['warning'][0])) + { + return self::$errors; + } + else + { + return self::$success; + } + } +} diff --git a/core/libraries/Hubzero/Utility/Number.php b/core/libraries/Hubzero/Utility/Number.php new file mode 100644 index 00000000000..a63d69ea648 --- /dev/null +++ b/core/libraries/Hubzero/Utility/Number.php @@ -0,0 +1,135 @@ + 1099511627776, // pow(1024, 4) + 'GB' => 1073741824, // pow(1024, 3) + 'MB' => 1048576, // pow(1024, 2) + 'KB' => 1024, // pow(1024, 1) + 'B ' => 1, // pow(1024, 0) + ); + + foreach ($quant as $unit => $mag) + { + if (doubleval($bytes) >= $mag) + { + return sprintf('%01.' . $decimals . 'f', ($bytes / $mag)) . ' ' . $unit; + } + } + + return '0 B'; + } + + /** + * Converts a number into a more readable human-type number. + * + * Usage: + * + * echo Number::quantity(7000); // 7K + * echo Number::quantity(7500); // 8K + * echo Number::quantity(7500, 1); // 7.5K + * + * + * @param integer $num + * @param integer $decimals + * @return string + */ + public static function quantity($num, $decimals = 0) + { + if ($num >= 1000 && $num < 1000000) + { + return sprintf('%01.'.$decimals.'f', (sprintf('%01.0f', $num) / 1000)).'K'; + } + elseif ($num >= 1000000 && $num < 1000000000) + { + return sprintf('%01.'.$decimals.'f', (sprintf('%01.0f', $num) / 1000000)).'M'; + } + elseif ($num >= 1000000000) + { + return sprintf('%01.'.$decimals.'f', (sprintf('%01.0f', $num) / 1000000000)).'B'; + } + + return $num; + } + + /** + * Formats a number by injecting non-numeric characters in a specified + * format into the string in the positions they appear in the format. + * + * Usage: + * + * echo Number::format('1234567890', '(000) 000-0000'); // (123) 456-7890 + * echo Number::format('1234567890', '000.000.0000'); // 123.456.7890 + * + * + * @link http://snippets.symfony-project.org/snippet/157 + * @param string the string to format + * @param string the format to apply + * @return string + */ + public static function format($string = '', $format = '') + { + if (empty($format) || empty($string)) + { + return $string; + } + + if ($format == 'bytes') + { + return self::formatBytes($string); + } + + $result = ''; + $fpos = 0; + $spos = 0; + + while ((strlen($format) - 1) >= $fpos) + { + if (ctype_alnum(substr($format, $fpos, 1))) + { + $result .= substr($string, $spos, 1); + $spos++; + } + else + { + $result .= substr($format, $fpos, 1); + } + + $fpos++; + } + + return $result; + } +} diff --git a/core/libraries/Hubzero/Utility/Sanitize.php b/core/libraries/Hubzero/Utility/Sanitize.php new file mode 100644 index 00000000000..cc0431ebafd --- /dev/null +++ b/core/libraries/Hubzero/Utility/Sanitize.php @@ -0,0 +1,585 @@ + $clean) + { + $cleaned[$key] = preg_replace("/[^{$allow}a-zA-Z0-9]/", '', $clean); + } + + return $cleaned; + } + + /** + * Strips extra whitespace from output + * + * @param string $str String to sanitize + * @return string whitespace sanitized string + */ + public static function stripWhitespace($str) + { + return preg_replace('/\s{2,}/u', ' ', preg_replace('/[\n\r\t]+/', '', $str)); + } + + /** + * Strips image tags from output + * + * @param string $str String to sanitize + * @return string String with images stripped. + */ + public static function stripImages($str) + { + $preg = array( + '/(]*>)(]+alt=")([^"]*)("[^>]*>)(<\/a>)/i' => '$1$3$5
    ', + '/(]+alt=")([^"]*)("[^>]*>)/i' => '$2
    ', + '/]*>/i' => '' + ); + + return preg_replace(array_keys($preg), array_values($preg), $str); + } + + /** + * Strips given text of all links (]+>|im', '', preg_replace('|<\/a>|im', '', $text)); + } + + /** + * Strips scripts and stylesheets from output + * + * @param string $str String to sanitize + * @return string String with , ,