diff options
Diffstat (limited to 'engine/classes')
91 files changed, 14821 insertions, 0 deletions
diff --git a/engine/classes/APIException.php b/engine/classes/APIException.php new file mode 100644 index 000000000..b6e1c347b --- /dev/null +++ b/engine/classes/APIException.php @@ -0,0 +1,11 @@ +<?php + +/** + * API Exception Stub + * + * Generic parent class for API exceptions. + * + * @package Elgg.Core + * @subpackage Exceptions.Stub + */ +class APIException extends Exception {} diff --git a/engine/classes/CallException.php b/engine/classes/CallException.php new file mode 100644 index 000000000..22b8f14f5 --- /dev/null +++ b/engine/classes/CallException.php @@ -0,0 +1,10 @@ +<?php +/** + * Call Exception Stub + * + * Generic parent class for Call exceptions + * + * @package Elgg.Core + * @subpackage Exceptions.Stub + */ +class CallException extends Exception {} diff --git a/engine/classes/ClassException.php b/engine/classes/ClassException.php new file mode 100644 index 000000000..7544f0ec9 --- /dev/null +++ b/engine/classes/ClassException.php @@ -0,0 +1,10 @@ +<?php +/** + * Class Exception + * + * A generic parent class for Class exceptions + * + * @package Elgg.Core + * @subpackage Exceptions.Stub + */ +class ClassException extends Exception {} diff --git a/engine/classes/ClassNotFoundException.php b/engine/classes/ClassNotFoundException.php new file mode 100644 index 000000000..6a9bcd327 --- /dev/null +++ b/engine/classes/ClassNotFoundException.php @@ -0,0 +1,10 @@ +<?php +/** + * Class not found + * + * Thrown when trying to load a class that doesn't exist. + * + * @package Elgg.Core + * @subpackage Exceptions + */ +class ClassNotFoundException extends ClassException {} diff --git a/engine/classes/ConfigurationException.php b/engine/classes/ConfigurationException.php new file mode 100644 index 000000000..3ace5dd4b --- /dev/null +++ b/engine/classes/ConfigurationException.php @@ -0,0 +1,10 @@ +<?php +/** + * Configuration exception + * + * A generic parent class for Configuration exceptions + * + * @package Elgg + * @subpackage Exceptions.Stub + */ +class ConfigurationException extends Exception {} diff --git a/engine/classes/CronException.php b/engine/classes/CronException.php new file mode 100644 index 000000000..86370ef31 --- /dev/null +++ b/engine/classes/CronException.php @@ -0,0 +1,10 @@ +<?php +/** + * Cron exception + * + * A generic parent class for cron exceptions + * + * @package Elgg + * @subpackage Exceptions.Stub + */ +class CronException extends Exception {} diff --git a/engine/classes/DataFormatException.php b/engine/classes/DataFormatException.php new file mode 100644 index 000000000..0f28a0902 --- /dev/null +++ b/engine/classes/DataFormatException.php @@ -0,0 +1,9 @@ +<?php +/** + * Data format exception + * An exception thrown when there is a problem in the format of some data. + * + * @package Elgg.Core + * @subpackage Exceptions.Stub + */ +class DataFormatException extends Exception {} diff --git a/engine/classes/DatabaseException.php b/engine/classes/DatabaseException.php new file mode 100644 index 000000000..6c8f57d7d --- /dev/null +++ b/engine/classes/DatabaseException.php @@ -0,0 +1,10 @@ +<?php +/** + * Database Exception + * + * A generic parent class for database exceptions + * + * @package Elgg.Core + * @subpackage Exceptions.Stub + */ +class DatabaseException extends Exception {} diff --git a/engine/classes/ElggAccess.php b/engine/classes/ElggAccess.php new file mode 100644 index 000000000..0aed477fc --- /dev/null +++ b/engine/classes/ElggAccess.php @@ -0,0 +1,70 @@ +<?php +/** + * Class used to determine if access is being ignored. + * + * @package Elgg.Core + * @subpackage Access + * @access private + * @see elgg_get_ignore_access() + * + * @todo I don't remember why this was required beyond scope concerns. + */ +class ElggAccess { + /** + * Bypass Elgg's access control if true. + * @var bool + */ + private $ignore_access; + + // @codingStandardsIgnoreStart + /** + * Get current ignore access setting. + * + * @return bool + * @deprecated 1.8 Use ElggAccess::getIgnoreAccess() + */ + public function get_ignore_access() { + elgg_deprecated_notice('ElggAccess::get_ignore_access() is deprecated by ElggAccess::getIgnoreAccess()', 1.8); + return $this->getIgnoreAccess(); + } + // @codingStandardsIgnoreEnd + + /** + * Get current ignore access setting. + * + * @return bool + */ + public function getIgnoreAccess() { + return $this->ignore_access; + } + + // @codingStandardsIgnoreStart + /** + * Set ignore access. + * + * @param bool $ignore Ignore access + * + * @return bool Previous setting + * + * @deprecated 1.8 Use ElggAccess:setIgnoreAccess() + */ + public function set_ignore_access($ignore = true) { + elgg_deprecated_notice('ElggAccess::set_ignore_access() is deprecated by ElggAccess::setIgnoreAccess()', 1.8); + return $this->setIgnoreAccess($ignore); + } + // @codingStandardsIgnoreEnd + + /** + * Set ignore access. + * + * @param bool $ignore Ignore access + * + * @return bool Previous setting + */ + public function setIgnoreAccess($ignore = true) { + $prev = $this->ignore_access; + $this->ignore_access = $ignore; + + return $prev; + } +} diff --git a/engine/classes/ElggAnnotation.php b/engine/classes/ElggAnnotation.php new file mode 100644 index 000000000..175e7049d --- /dev/null +++ b/engine/classes/ElggAnnotation.php @@ -0,0 +1,133 @@ +<?php +/** + * Elgg Annotations + * + * Annotations allow you to attach bits of information to entities. + * They are essentially the same as metadata, but with additional + * helper functions. + * + * @internal Annotations are stored in the annotations table. + * + * @package Elgg.Core + * @subpackage DataModel.Annotations + * @link http://docs.elgg.org/DataModel/Annotations + * + * @property string $value_type + * @property string $enabled + */ +class ElggAnnotation extends ElggExtender { + + /** + * (non-PHPdoc) + * + * @see ElggData::initializeAttributes() + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['type'] = 'annotation'; + } + + /** + * Construct a new annotation object + * + * @param mixed $id The annotation ID or a database row as stdClass object + */ + function __construct($id = null) { + $this->initializeAttributes(); + + if (!empty($id)) { + // Create from db row + if ($id instanceof stdClass) { + $annotation = $id; + + $objarray = (array) $annotation; + foreach ($objarray as $key => $value) { + $this->attributes[$key] = $value; + } + } else { + // get an ElggAnnotation object and copy its attributes + $annotation = elgg_get_annotation_from_id($id); + $this->attributes = $annotation->attributes; + } + } + } + + /** + * Save this instance + * + * @return int an object id + * + * @throws IOException + */ + function save() { + if ($this->id > 0) { + return update_annotation($this->id, $this->name, $this->value, $this->value_type, + $this->owner_guid, $this->access_id); + } else { + $this->id = create_annotation($this->entity_guid, $this->name, $this->value, + $this->value_type, $this->owner_guid, $this->access_id); + + if (!$this->id) { + throw new IOException(elgg_echo('IOException:UnableToSaveNew', array(get_class()))); + } + return $this->id; + } + } + + /** + * Delete the annotation. + * + * @return bool + */ + function delete() { + elgg_delete_river(array('annotation_id' => $this->id)); + return elgg_delete_metastring_based_object_by_id($this->id, 'annotations'); + } + + /** + * Disable the annotation. + * + * @return bool + * @since 1.8 + */ + function disable() { + return elgg_set_metastring_based_object_enabled_by_id($this->id, 'no', 'annotations'); + } + + /** + * Enable the annotation. + * + * @return bool + * @since 1.8 + */ + function enable() { + return elgg_set_metastring_based_object_enabled_by_id($this->id, 'yes', 'annotations'); + } + + /** + * Get a url for this annotation. + * + * @return string + */ + public function getURL() { + return get_annotation_url($this->id); + } + + // SYSTEM LOG INTERFACE + + /** + * For a given ID, return the object associated with it. + * This is used by the river functionality primarily. + * This is useful for checking access permissions etc on objects. + * + * @param int $id An annotation ID. + * + * @return ElggAnnotation + */ + public function getObjectFromID($id) { + return elgg_get_annotation_from_id($id); + } +} diff --git a/engine/classes/ElggAttributeLoader.php b/engine/classes/ElggAttributeLoader.php new file mode 100644 index 000000000..ffc80b02d --- /dev/null +++ b/engine/classes/ElggAttributeLoader.php @@ -0,0 +1,248 @@ +<?php + +/** + * Loads ElggEntity attributes from DB or validates those passed in via constructor + * + * @access private + * + * @package Elgg.Core + * @subpackage DataModel + */ +class ElggAttributeLoader { + + /** + * @var array names of attributes in all entities + */ + protected static $primary_attr_names = array( + 'guid', + 'type', + 'subtype', + 'owner_guid', + 'container_guid', + 'site_guid', + 'access_id', + 'time_created', + 'time_updated', + 'last_action', + 'enabled', + ); + + /** + * @var array names of secondary attributes required for the entity + */ + protected $secondary_attr_names = array(); + + /** + * @var string entity type (not class) required for fetched primaries + */ + protected $required_type; + + /** + * @var array + */ + protected $initialized_attributes; + + /** + * @var string class of object being loaded + */ + protected $class; + + /** + * @var bool should access control be considered when fetching entity? + */ + public $requires_access_control = true; + + /** + * @var callable function used to load attributes from {prefix}entities table + */ + public $primary_loader = 'get_entity_as_row'; + + /** + * @var callable function used to load attributes from secondary table + */ + public $secondary_loader = ''; + + /** + * @var callable function used to load all necessary attributes + */ + public $full_loader = ''; + + /** + * Constructor + * + * @param string $class class of object being loaded + * @param string $required_type entity type this is being used to populate + * @param array $initialized_attrs attributes after initializeAttributes() has been run + * @throws InvalidArgumentException + */ + public function __construct($class, $required_type, array $initialized_attrs) { + if (!is_string($class)) { + throw new InvalidArgumentException('$class must be a class name.'); + } + $this->class = $class; + + if (!is_string($required_type)) { + throw new InvalidArgumentException('$requiredType must be a system entity type.'); + } + $this->required_type = $required_type; + + $this->initialized_attributes = $initialized_attrs; + unset($initialized_attrs['tables_split'], $initialized_attrs['tables_loaded']); + $all_attr_names = array_keys($initialized_attrs); + $this->secondary_attr_names = array_diff($all_attr_names, self::$primary_attr_names); + } + + /** + * Get primary attributes missing that are missing + * + * @param stdClass $row Database row + * @return array + */ + protected function isMissingPrimaries($row) { + return array_diff(self::$primary_attr_names, array_keys($row)) !== array(); + } + + /** + * Get secondary attributes that are missing + * + * @param stdClass $row Database row + * @return array + */ + protected function isMissingSecondaries($row) { + return array_diff($this->secondary_attr_names, array_keys($row)) !== array(); + } + + /** + * Check that the type is correct + * + * @param stdClass $row Database row + * @return void + * @throws InvalidClassException + */ + protected function checkType($row) { + if ($row['type'] !== $this->required_type) { + $msg = elgg_echo('InvalidClassException:NotValidElggStar', array($row['guid'], $this->class)); + throw new InvalidClassException($msg); + } + } + + /** + * Get all required attributes for the entity, validating any that are passed in. Returns empty array + * if can't be loaded (Check $failure_reason). + * + * This function splits loading between "primary" attributes (those in {prefix}entities table) and + * "secondary" attributes (e.g. those in {prefix}objects_entity), but can load all at once if a + * combined loader is available. + * + * @param mixed $row a row loaded from DB (array or stdClass) or a GUID + * @return array will be empty if failed to load all attributes (access control or entity doesn't exist) + * + * @throws InvalidArgumentException|LogicException|IncompleteEntityException + */ + public function getRequiredAttributes($row) { + if (!is_array($row) && !($row instanceof stdClass)) { + // assume row is the GUID + $row = array('guid' => $row); + } + $row = (array) $row; + if (empty($row['guid'])) { + throw new InvalidArgumentException('$row must be or contain a GUID'); + } + + // these must be present to support isFullyLoaded() + foreach (array('tables_split', 'tables_loaded') as $key) { + if (isset($this->initialized_attributes[$key])) { + $row[$key] = $this->initialized_attributes[$key]; + } + } + + $was_missing_primaries = $this->isMissingPrimaries($row); + $was_missing_secondaries = $this->isMissingSecondaries($row); + + // some types have a function to load all attributes at once, it should be faster + if (($was_missing_primaries || $was_missing_secondaries) && is_callable($this->full_loader)) { + $fetched = (array) call_user_func($this->full_loader, $row['guid']); + if (!$fetched) { + return array(); + } + $row = array_merge($row, $fetched); + $this->checkType($row); + } else { + if ($was_missing_primaries) { + if (!is_callable($this->primary_loader)) { + throw new LogicException('Primary attribute loader must be callable'); + } + if ($this->requires_access_control) { + $fetched = (array) call_user_func($this->primary_loader, $row['guid']); + } else { + $ignoring_access = elgg_set_ignore_access(); + $fetched = (array) call_user_func($this->primary_loader, $row['guid']); + elgg_set_ignore_access($ignoring_access); + } + if (!$fetched) { + return array(); + } + $row = array_merge($row, $fetched); + } + + // We must test type before trying to load the secondaries so that InvalidClassException + // gets thrown. Otherwise the secondary loader will fail and return false. + $this->checkType($row); + + if ($was_missing_secondaries) { + if (!is_callable($this->secondary_loader)) { + throw new LogicException('Secondary attribute loader must be callable'); + } + $fetched = (array) call_user_func($this->secondary_loader, $row['guid']); + if (!$fetched) { + if ($row['type'] === 'site') { + // A special case is needed for sites: When vanilla ElggEntities are created and + // saved, these are stored w/ type "site", but with no sites_entity row. These + // are probably only created in the unit tests. + // @todo Don't save vanilla ElggEntities with type "site" + + $row = $this->filterAddedColumns($row); + $row['guid'] = (int) $row['guid']; + return $row; + } + throw new IncompleteEntityException("Secondary loader failed to return row for {$row['guid']}"); + } + $row = array_merge($row, $fetched); + } + } + + $row = $this->filterAddedColumns($row); + + // Note: If there are still missing attributes, we're running on a 1.7 or earlier schema. We let + // this pass so the upgrades can run. + + // guid needs to be an int https://github.com/elgg/elgg/issues/4111 + $row['guid'] = (int) $row['guid']; + + return $row; + } + + /** + * Filter out keys returned by the query which should not appear in the entity's attributes + * + * @param array $row All columns from the query + * @return array Columns acceptable for the entity's attributes + */ + protected function filterAddedColumns($row) { + // make an array with keys as acceptable attribute names + $acceptable_attrs = self::$primary_attr_names; + array_splice($acceptable_attrs, count($acceptable_attrs), 0, $this->secondary_attr_names); + $acceptable_attrs = array_combine($acceptable_attrs, $acceptable_attrs); + + // @todo remove these when #4584 is in place + $acceptable_attrs['tables_split'] = true; + $acceptable_attrs['tables_loaded'] = true; + + foreach ($row as $key => $val) { + if (!isset($acceptable_attrs[$key])) { + unset($row[$key]); + } + } + return $row; + } +} diff --git a/engine/classes/ElggAutoP.php b/engine/classes/ElggAutoP.php new file mode 100644 index 000000000..05842d1b2 --- /dev/null +++ b/engine/classes/ElggAutoP.php @@ -0,0 +1,336 @@ +<?php + +/** + * Create wrapper P and BR elements in HTML depending on newlines. Useful when + * users use newlines to signal line and paragraph breaks. In all cases output + * should be well-formed markup. + * + * In DIV elements, Ps are only added when there would be at + * least two of them. + * + * @package Elgg.Core + * @subpackage Output + */ +class ElggAutoP { + + public $encoding = 'UTF-8'; + + /** + * @var DOMDocument + */ + protected $_doc = null; + + /** + * @var DOMXPath + */ + protected $_xpath = null; + + protected $_blocks = 'address article area aside blockquote caption col colgroup dd + details div dl dt fieldset figure figcaption footer form h1 h2 h3 h4 h5 h6 header + hr hgroup legend map math menu nav noscript p pre section select style summary + table tbody td tfoot th thead tr ul ol option li'; + + /** + * @var array + */ + protected $_inlines = 'a abbr audio b button canvas caption cite code command datalist + del dfn em embed i iframe img input ins kbd keygen label map mark meter object + output progress q rp rt ruby s samp script select small source span strong style + sub sup textarea time var video wbr'; + + /** + * Descend into these elements to add Ps + * + * @var array + */ + protected $_descendList = 'article aside blockquote body details div footer form + header section'; + + /** + * Add Ps inside these elements + * + * @var array + */ + protected $_alterList = 'article aside blockquote body details div footer header + section'; + + /** @var string */ + protected $_unique = ''; + + /** + * Constructor + */ + public function __construct() { + $this->_blocks = preg_split('@\\s+@', $this->_blocks); + $this->_descendList = preg_split('@\\s+@', $this->_descendList); + $this->_alterList = preg_split('@\\s+@', $this->_alterList); + $this->_inlines = preg_split('@\\s+@', $this->_inlines); + $this->_unique = md5(__FILE__); + } + + /** + * Intance of class for singleton pattern. + * @var ElggAutoP + */ + private static $instance; + + /** + * Singleton pattern. + * @return ElggAutoP + */ + public static function getInstance() { + $className = __CLASS__; + if (!(self::$instance instanceof $className)) { + self::$instance = new $className(); + } + return self::$instance; + } + + /** + * Create wrapper P and BR elements in HTML depending on newlines. Useful when + * users use newlines to signal line and paragraph breaks. In all cases output + * should be well-formed markup. + * + * In DIV, LI, TD, and TH elements, Ps are only added when their would be at + * least two of them. + * + * @param string $html snippet + * @return string|false output or false if parse error occurred + */ + public function process($html) { + // normalize whitespace + $html = str_replace(array("\r\n", "\r"), "\n", $html); + + // allows preserving entities untouched + $html = str_replace('&', $this->_unique . 'AMP', $html); + + $this->_doc = new DOMDocument(); + + // parse to DOM, suppressing loadHTML warnings + // http://www.php.net/manual/en/domdocument.loadhtml.php#95463 + libxml_use_internal_errors(true); + + // Do not load entities. May be unnecessary, better safe than sorry + $disable_load_entities = libxml_disable_entity_loader(true); + + if (!$this->_doc->loadHTML("<html><meta http-equiv='content-type' " + . "content='text/html; charset={$this->encoding}'><body>{$html}</body>" + . "</html>")) { + + libxml_disable_entity_loader($disable_load_entities); + return false; + } + + libxml_disable_entity_loader($disable_load_entities); + + $this->_xpath = new DOMXPath($this->_doc); + // start processing recursively at the BODY element + $nodeList = $this->_xpath->query('//body[1]'); + $this->addParagraphs($nodeList->item(0)); + + // serialize back to HTML + $html = $this->_doc->saveHTML(); + + // Note: we create <autop> elements, which will later be converted to paragraphs + + // split AUTOPs into multiples at /\n\n+/ + $html = preg_replace('/(' . $this->_unique . 'NL){2,}/', '</autop><autop>', $html); + $html = str_replace(array($this->_unique . 'BR', $this->_unique . 'NL', '<br>'), + '<br />', + $html); + $html = str_replace('<br /></autop>', '</autop>', $html); + + // re-parse so we can handle new AUTOP elements + + // Do not load entities. May be unnecessary, better safe than sorry + $disable_load_entities = libxml_disable_entity_loader(true); + + if (!$this->_doc->loadHTML($html)) { + libxml_disable_entity_loader($disable_load_entities); + return false; + } + + libxml_disable_entity_loader($disable_load_entities); + + // must re-create XPath object after DOM load + $this->_xpath = new DOMXPath($this->_doc); + + // strip AUTOPs that only have comments/whitespace + foreach ($this->_xpath->query('//autop') as $autop) { + /* @var DOMElement $autop */ + $hasContent = false; + if (trim($autop->textContent) !== '') { + $hasContent = true; + } else { + foreach ($autop->childNodes as $node) { + if ($node->nodeType === XML_ELEMENT_NODE) { + $hasContent = true; + break; + } + } + } + if (!$hasContent) { + // mark to be later replaced w/ preg_replace (faster than moving nodes out) + $autop->setAttribute("r", "1"); + } + } + + // If a DIV contains a single AUTOP, remove it + foreach ($this->_xpath->query('//div') as $el) { + /* @var DOMElement $el */ + $autops = $this->_xpath->query('./autop', $el); + if ($autops->length === 1) { + $firstAutop = $autops->item(0); + /* @var DOMElement $firstAutop */ + $firstAutop->setAttribute("r", "1"); + } + } + + $html = $this->_doc->saveHTML(); + + // trim to the contents of BODY + $bodyStart = strpos($html, '<body>'); + $bodyEnd = strpos($html, '</body>', $bodyStart + 6); + $html = substr($html, $bodyStart + 6, $bodyEnd - $bodyStart - 6); + + // strip AUTOPs that should be removed + $html = preg_replace('@<autop r="1">(.*?)</autop>@', '\\1', $html); + + // commit to converting AUTOPs to Ps + $html = str_replace('<autop>', "\n<p>", $html); + $html = str_replace('</autop>', "</p>\n", $html); + + $html = str_replace('<br>', '<br />', $html); + $html = str_replace($this->_unique . 'AMP', '&', $html); + return $html; + } + + /** + * Add P and BR elements as necessary + * + * @param DOMElement $el DOM element + * @return void + */ + protected function addParagraphs(DOMElement $el) { + // no need to call recursively, just queue up + $elsToProcess = array($el); + $inlinesToProcess = array(); + while ($el = array_shift($elsToProcess)) { + // if true, we can alter all child nodes, if not, we'll just call + // addParagraphs on each element in the descendInto list + $alterInline = in_array($el->nodeName, $this->_alterList); + + // inside affected elements, we want to trim leading whitespace from + // the first text node + $ltrimFirstTextNode = true; + + // should we open a new AUTOP element to move inline elements into? + $openP = true; + $autop = null; + + // after BR, ignore a newline + $isFollowingBr = false; + + $node = $el->firstChild; + while (null !== $node) { + if ($alterInline) { + if ($openP) { + $openP = false; + // create a P to move inline content into (this may be removed later) + $autop = $el->insertBefore($this->_doc->createElement('autop'), $node); + } + } + + $isElement = ($node->nodeType === XML_ELEMENT_NODE); + if ($isElement) { + $isBlock = in_array($node->nodeName, $this->_blocks); + } else { + $isBlock = false; + } + + if ($alterInline) { + $isText = ($node->nodeType === XML_TEXT_NODE); + $isLastInline = (! $node->nextSibling + || ($node->nextSibling->nodeType === XML_ELEMENT_NODE + && in_array($node->nextSibling->nodeName, $this->_blocks))); + if ($isElement) { + $isFollowingBr = ($node->nodeName === 'br'); + } + + if ($isText) { + $nodeText = $node->nodeValue; + if ($ltrimFirstTextNode) { + $nodeText = ltrim($nodeText); + $ltrimFirstTextNode = false; + } + if ($isFollowingBr && preg_match('@^[ \\t]*\\n[ \\t]*@', $nodeText, $m)) { + // if a user ends a line with <br>, don't add a second BR + $nodeText = substr($nodeText, strlen($m[0])); + } + if ($isLastInline) { + $nodeText = rtrim($nodeText); + } + $nodeText = str_replace("\n", $this->_unique . 'NL', $nodeText); + $tmpNode = $node; + $node = $node->nextSibling; // move loop to next node + + // alter node in place, then move into AUTOP + $tmpNode->nodeValue = $nodeText; + $autop->appendChild($tmpNode); + + continue; + } + } + if ($isBlock || ! $node->nextSibling) { + if ($isBlock) { + if (in_array($node->nodeName, $this->_descendList)) { + $elsToProcess[] = $node; + //$this->addParagraphs($node); + } + } + $openP = true; + $ltrimFirstTextNode = true; + } + if ($alterInline) { + if (! $isBlock) { + $tmpNode = $node; + if ($isElement && false !== strpos($tmpNode->textContent, "\n")) { + $inlinesToProcess[] = $tmpNode; + } + $node = $node->nextSibling; + $autop->appendChild($tmpNode); + continue; + } + } + + $node = $node->nextSibling; + } + } + + // handle inline nodes + // no need to recurse, just queue up + while ($el = array_shift($inlinesToProcess)) { + $ignoreLeadingNewline = false; + foreach ($el->childNodes as $node) { + if ($node->nodeType === XML_ELEMENT_NODE) { + if ($node->nodeValue === 'BR') { + $ignoreLeadingNewline = true; + } else { + $ignoreLeadingNewline = false; + if (false !== strpos($node->textContent, "\n")) { + $inlinesToProcess[] = $node; + } + } + continue; + } elseif ($node->nodeType === XML_TEXT_NODE) { + $text = $node->nodeValue; + if ($text[0] === "\n" && $ignoreLeadingNewline) { + $text = substr($text, 1); + $ignoreLeadingNewline = false; + } + $node->nodeValue = str_replace("\n", $this->_unique . 'BR', $text); + } + } + } + } +} diff --git a/engine/classes/ElggBatch.php b/engine/classes/ElggBatch.php new file mode 100644 index 000000000..d810ea066 --- /dev/null +++ b/engine/classes/ElggBatch.php @@ -0,0 +1,433 @@ +<?php +/** + * Efficiently run operations on batches of results for any function + * that supports an options array. + * + * This is usually used with elgg_get_entities() and friends, + * elgg_get_annotations(), and elgg_get_metadata(). + * + * If you pass a valid PHP callback, all results will be run through that + * callback. You can still foreach() through the result set after. Valid + * PHP callbacks can be a string, an array, or a closure. + * {@link http://php.net/manual/en/language.pseudo-types.php} + * + * The callback function must accept 3 arguments: an entity, the getter + * used, and the options used. + * + * Results from the callback are stored in callbackResult. If the callback + * returns only booleans, callbackResults will be the combined result of + * all calls. If no entities are processed, callbackResults will be null. + * + * If the callback returns anything else, callbackresult will be an indexed + * array of whatever the callback returns. If returning error handling + * information, you should include enough information to determine which + * result you're referring to. + * + * Don't combine returning bools and returning something else. + * + * Note that returning false will not stop the foreach. + * + * @warning If your callback or foreach loop deletes or disable entities + * you MUST call setIncrementOffset(false) or set that when instantiating. + * This forces the offset to stay what it was in the $options array. + * + * @example + * <code> + * // using foreach + * $batch = new ElggBatch('elgg_get_entities', array()); + * $batch->setIncrementOffset(false); + * + * foreach ($batch as $entity) { + * $entity->disable(); + * } + * + * // using both a callback + * $callback = function($result, $getter, $options) { + * var_dump("Looking at annotation id: $result->id"); + * return true; + * } + * + * $batch = new ElggBatch('elgg_get_annotations', array('guid' => 2), $callback); + * </code> + * + * @package Elgg.Core + * @subpackage DataModel + * @link http://docs.elgg.org/DataModel/ElggBatch + * @since 1.8 + */ +class ElggBatch + implements Iterator { + + /** + * The objects to interator over. + * + * @var array + */ + private $results = array(); + + /** + * The function used to get results. + * + * @var mixed A string, array, or closure, or lamda function + */ + private $getter = null; + + /** + * The number of results to grab at a time. + * + * @var int + */ + private $chunkSize = 25; + + /** + * A callback function to pass results through. + * + * @var mixed A string, array, or closure, or lamda function + */ + private $callback = null; + + /** + * Start after this many results. + * + * @var int + */ + private $offset = 0; + + /** + * Stop after this many results. + * + * @var int + */ + private $limit = 0; + + /** + * Number of processed results. + * + * @var int + */ + private $retrievedResults = 0; + + /** + * The index of the current result within the current chunk + * + * @var int + */ + private $resultIndex = 0; + + /** + * The index of the current chunk + * + * @var int + */ + private $chunkIndex = 0; + + /** + * The number of results iterated through + * + * @var int + */ + private $processedResults = 0; + + /** + * Is the getter a valid callback + * + * @var bool + */ + private $validGetter = null; + + /** + * The result of running all entities through the callback function. + * + * @var mixed + */ + public $callbackResult = null; + + /** + * If false, offset will not be incremented. This is used for callbacks/loops that delete. + * + * @var bool + */ + private $incrementOffset = true; + + /** + * Entities that could not be instantiated during a fetch + * + * @var stdClass[] + */ + private $incompleteEntities = array(); + + /** + * Total number of incomplete entities fetched + * + * @var int + */ + private $totalIncompletes = 0; + + /** + * Batches operations on any elgg_get_*() or compatible function that supports + * an options array. + * + * Instead of returning all objects in memory, it goes through $chunk_size + * objects, then requests more from the server. This avoids OOM errors. + * + * @param string $getter The function used to get objects. Usually + * an elgg_get_*() function, but can be any valid PHP callback. + * @param array $options The options array to pass to the getter function. If limit is + * not set, 10 is used as the default. In most cases that is not + * what you want. + * @param mixed $callback An optional callback function that all results will be passed + * to upon load. The callback needs to accept $result, $getter, + * $options. + * @param int $chunk_size The number of entities to pull in before requesting more. + * You have to balance this between running out of memory in PHP + * and hitting the db server too often. + * @param bool $inc_offset Increment the offset on each fetch. This must be false for + * callbacks that delete rows. You can set this after the + * object is created with {@see ElggBatch::setIncrementOffset()}. + */ + public function __construct($getter, $options, $callback = null, $chunk_size = 25, + $inc_offset = true) { + + $this->getter = $getter; + $this->options = $options; + $this->callback = $callback; + $this->chunkSize = $chunk_size; + $this->setIncrementOffset($inc_offset); + + if ($this->chunkSize <= 0) { + $this->chunkSize = 25; + } + + // store these so we can compare later + $this->offset = elgg_extract('offset', $options, 0); + $this->limit = elgg_extract('limit', $options, 10); + + // if passed a callback, create a new ElggBatch with the same options + // and pass each to the callback. + if ($callback && is_callable($callback)) { + $batch = new ElggBatch($getter, $options, null, $chunk_size, $inc_offset); + + $all_results = null; + + foreach ($batch as $result) { + if (is_string($callback)) { + $result = $callback($result, $getter, $options); + } else { + $result = call_user_func_array($callback, array($result, $getter, $options)); + } + + if (!isset($all_results)) { + if ($result === true || $result === false || $result === null) { + $all_results = $result; + } else { + $all_results = array(); + } + } + + if (($result === true || $result === false || $result === null) && !is_array($all_results)) { + $all_results = $result && $all_results; + } else { + $all_results[] = $result; + } + } + + $this->callbackResult = $all_results; + } + } + + /** + * Tell the process that an entity was incomplete during a fetch + * + * @param stdClass $row + * + * @access private + */ + public function reportIncompleteEntity(stdClass $row) { + $this->incompleteEntities[] = $row; + } + + /** + * Fetches the next chunk of results + * + * @return bool + */ + private function getNextResultsChunk() { + + // always reset results. + $this->results = array(); + + if (!isset($this->validGetter)) { + $this->validGetter = is_callable($this->getter); + } + + if (!$this->validGetter) { + return false; + } + + $limit = $this->chunkSize; + + // if someone passed limit = 0 they want everything. + if ($this->limit != 0) { + if ($this->retrievedResults >= $this->limit) { + return false; + } + + // if original limit < chunk size, set limit to original limit + // else if the number of results we'll fetch if greater than the original limit + if ($this->limit < $this->chunkSize) { + $limit = $this->limit; + } elseif ($this->retrievedResults + $this->chunkSize > $this->limit) { + // set the limit to the number of results remaining in the original limit + $limit = $this->limit - $this->retrievedResults; + } + } + + if ($this->incrementOffset) { + $offset = $this->offset + $this->retrievedResults; + } else { + $offset = $this->offset + $this->totalIncompletes; + } + + $current_options = array( + 'limit' => $limit, + 'offset' => $offset, + '__ElggBatch' => $this, + ); + + $options = array_merge($this->options, $current_options); + + $this->incompleteEntities = array(); + $this->results = call_user_func_array($this->getter, array($options)); + + $num_results = count($this->results); + $num_incomplete = count($this->incompleteEntities); + + $this->totalIncompletes += $num_incomplete; + + if ($this->incompleteEntities) { + // pad the front of the results with nulls representing the incompletes + array_splice($this->results, 0, 0, array_pad(array(), $num_incomplete, null)); + // ...and skip past them + reset($this->results); + for ($i = 0; $i < $num_incomplete; $i++) { + next($this->results); + } + } + + if ($this->results) { + $this->chunkIndex++; + + // let the system know we've jumped past the nulls + $this->resultIndex = $num_incomplete; + + $this->retrievedResults += ($num_results + $num_incomplete); + if ($num_results == 0) { + // This fetch was *all* incompletes! We need to fetch until we can either + // offer at least one row to iterate over, or give up. + return $this->getNextResultsChunk(); + } + return true; + } else { + return false; + } + } + + /** + * Increment the offset from the original options array? Setting to + * false is required for callbacks that delete rows. + * + * @param bool $increment Set to false when deleting data + * @return void + */ + public function setIncrementOffset($increment = true) { + $this->incrementOffset = (bool) $increment; + } + + /** + * Implements Iterator + */ + + /** + * PHP Iterator Interface + * + * @see Iterator::rewind() + * @return void + */ + public function rewind() { + $this->resultIndex = 0; + $this->retrievedResults = 0; + $this->processedResults = 0; + + // only grab results if we haven't yet or we're crossing chunks + if ($this->chunkIndex == 0 || $this->limit > $this->chunkSize) { + $this->chunkIndex = 0; + $this->getNextResultsChunk(); + } + } + + /** + * PHP Iterator Interface + * + * @see Iterator::current() + * @return mixed + */ + public function current() { + return current($this->results); + } + + /** + * PHP Iterator Interface + * + * @see Iterator::key() + * @return int + */ + public function key() { + return $this->processedResults; + } + + /** + * PHP Iterator Interface + * + * @see Iterator::next() + * @return mixed + */ + public function next() { + // if we'll be at the end. + if (($this->processedResults + 1) >= $this->limit && $this->limit > 0) { + $this->results = array(); + return false; + } + + // if we'll need new results. + if (($this->resultIndex + 1) >= $this->chunkSize) { + if (!$this->getNextResultsChunk()) { + $this->results = array(); + return false; + } + + $result = current($this->results); + } else { + // the function above resets the indexes, so only inc if not + // getting new set + $this->resultIndex++; + $result = next($this->results); + } + + $this->processedResults++; + return $result; + } + + /** + * PHP Iterator Interface + * + * @see Iterator::valid() + * @return bool + */ + public function valid() { + if (!is_array($this->results)) { + return false; + } + $key = key($this->results); + return ($key !== NULL && $key !== FALSE); + } +} diff --git a/engine/classes/ElggCache.php b/engine/classes/ElggCache.php new file mode 100644 index 000000000..909eab39b --- /dev/null +++ b/engine/classes/ElggCache.php @@ -0,0 +1,247 @@ +<?php +/** + * ElggCache The elgg cache superclass. + * This defines the interface for a cache (wherever that cache is stored). + * + * @package Elgg.Core + * @subpackage Cache + */ +abstract class ElggCache implements ArrayAccess { + /** + * Variables for the cache object. + * + * @var array + */ + private $variables; + + /** + * Set the constructor. + */ + function __construct() { + $this->variables = array(); + } + + // @codingStandardsIgnoreStart + /** + * Set a cache variable. + * + * @param string $variable Name + * @param string $value Value + * + * @return void + * + * @deprecated 1.8 Use ElggCache:setVariable() + */ + public function set_variable($variable, $value) { + elgg_deprecated_notice('ElggCache::set_variable() is deprecated by ElggCache::setVariable()', 1.8); + $this->setVariable($variable, $value); + } + // @codingStandardsIgnoreEnd + + /** + * Set a cache variable. + * + * @param string $variable Name + * @param string $value Value + * + * @return void + */ + public function setVariable($variable, $value) { + if (!is_array($this->variables)) { + $this->variables = array(); + } + + $this->variables[$variable] = $value; + } + + // @codingStandardsIgnoreStart + /** + * Get variables for this cache. + * + * @param string $variable Name + * + * @return mixed The value or null; + * + * @deprecated 1.8 Use ElggCache::getVariable() + */ + public function get_variable($variable) { + elgg_deprecated_notice('ElggCache::get_variable() is deprecated by ElggCache::getVariable()', 1.8); + return $this->getVariable($variable); + } + // @codingStandardsIgnoreEnd + + /** + * Get variables for this cache. + * + * @param string $variable Name + * + * @return mixed The variable or null; + */ + public function getVariable($variable) { + if (isset($this->variables[$variable])) { + return $this->variables[$variable]; + } + + return null; + } + + /** + * Class member get overloading, returning key using $this->load defaults. + * + * @param string $key Name + * + * @return mixed + */ + function __get($key) { + return $this->load($key); + } + + /** + * Class member set overloading, setting a key using $this->save defaults. + * + * @param string $key Name + * @param mixed $value Value + * + * @return mixed + */ + function __set($key, $value) { + return $this->save($key, $value); + } + + /** + * Supporting isset, using $this->load() with default values. + * + * @param string $key The name of the attribute or metadata. + * + * @return bool + */ + function __isset($key) { + return (bool)$this->load($key); + } + + /** + * Supporting unsetting of magic attributes. + * + * @param string $key The name of the attribute or metadata. + * + * @return bool + */ + function __unset($key) { + return $this->delete($key); + } + + /** + * Save data in a cache. + * + * @param string $key Name + * @param string $data Value + * + * @return bool + */ + abstract public function save($key, $data); + + /** + * Load data from the cache using a given key. + * + * @todo $offset is a horrible variable name because it creates confusion + * with the ArrayAccess methods + * + * @param string $key Name + * @param int $offset Offset + * @param int $limit Limit + * + * @return mixed The stored data or false. + */ + abstract public function load($key, $offset = 0, $limit = null); + + /** + * Invalidate a key + * + * @param string $key Name + * + * @return bool + */ + abstract public function delete($key); + + /** + * Clear out all the contents of the cache. + * + * @return bool + */ + abstract public function clear(); + + /** + * Add a key only if it doesn't already exist. + * Implemented simply here, if you extend this class and your caching engine + * provides a better way then override this accordingly. + * + * @param string $key Name + * @param string $data Value + * + * @return bool + */ + public function add($key, $data) { + if (!isset($this[$key])) { + return $this->save($key, $data); + } + + return false; + } + + // ARRAY ACCESS INTERFACE ////////////////////////////////////////////////////////// + + /** + * Assigns a value for the specified key + * + * @see ArrayAccess::offsetSet() + * + * @param mixed $key The key (offset) to assign the value to. + * @param mixed $value The value to set. + * + * @return void + */ + function offsetSet($key, $value) { + $this->save($key, $value); + } + + /** + * Get the value for specified key + * + * @see ArrayAccess::offsetGet() + * + * @param mixed $key The key (offset) to retrieve. + * + * @return mixed + */ + function offsetGet($key) { + return $this->load($key); + } + + /** + * Unsets a key. + * + * @see ArrayAccess::offsetUnset() + * + * @param mixed $key The key (offset) to unset. + * + * @return void + */ + function offsetUnset($key) { + if (isset($this->$key)) { + unset($this->$key); + } + } + + /** + * Does key exist + * + * @see ArrayAccess::offsetExists() + * + * @param mixed $key A key (offset) to check for. + * + * @return bool + */ + function offsetExists($key) { + return isset($this->$key); + } +} diff --git a/engine/classes/ElggCrypto.php b/engine/classes/ElggCrypto.php new file mode 100644 index 000000000..317d371e4 --- /dev/null +++ b/engine/classes/ElggCrypto.php @@ -0,0 +1,208 @@ +<?php +/** + * ElggCrypto + * + * @package Elgg.Core + * @subpackage Crypto + * + * @access private + */ +class ElggCrypto { + + /** + * Character set for temp passwords (no risk of embedded profanity/glyphs that look similar) + */ + const CHARS_PASSWORD = 'bcdfghjklmnpqrstvwxyz2346789'; + + /** + * Generate a string of highly randomized bytes (over the full 8-bit range). + * + * @param int $length Number of bytes needed + * @return string Random bytes + * + * @author George Argyros <argyros.george@gmail.com> + * @copyright 2012, George Argyros. All rights reserved. + * @license Modified BSD + * @link https://github.com/GeorgeArgyros/Secure-random-bytes-in-PHP/blob/master/srand.php Original + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the <organization> nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GEORGE ARGYROS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + public function getRandomBytes($length) { + /** + * Our primary choice for a cryptographic strong randomness function is + * openssl_random_pseudo_bytes. + */ + $SSLstr = '4'; // http://xkcd.com/221/ + if (function_exists('openssl_random_pseudo_bytes') + && (version_compare(PHP_VERSION, '5.3.4') >= 0 || substr(PHP_OS, 0, 3) !== 'WIN')) { + $SSLstr = openssl_random_pseudo_bytes($length, $strong); + if ($strong) { + return $SSLstr; + } + } + + /** + * If mcrypt extension is available then we use it to gather entropy from + * the operating system's PRNG. This is better than reading /dev/urandom + * directly since it avoids reading larger blocks of data than needed. + * Older versions of mcrypt_create_iv may be broken or take too much time + * to finish so we only use this function with PHP 5.3.7 and above. + * @see https://bugs.php.net/bug.php?id=55169 + */ + if (function_exists('mcrypt_create_iv') + && (version_compare(PHP_VERSION, '5.3.7') >= 0 || substr(PHP_OS, 0, 3) !== 'WIN')) { + $str = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + if ($str !== false) { + return $str; + } + } + + /** + * No build-in crypto randomness function found. We collect any entropy + * available in the PHP core PRNGs along with some filesystem info and memory + * stats. To make this data cryptographically strong we add data either from + * /dev/urandom or if its unavailable, we gather entropy by measuring the + * time needed to compute a number of SHA-1 hashes. + */ + $str = ''; + $bits_per_round = 2; // bits of entropy collected in each clock drift round + $msec_per_round = 400; // expected running time of each round in microseconds + $hash_len = 20; // SHA-1 Hash length + $total = $length; // total bytes of entropy to collect + + $handle = @fopen('/dev/urandom', 'rb'); + if ($handle && function_exists('stream_set_read_buffer')) { + @stream_set_read_buffer($handle, 0); + } + + do { + $bytes = ($total > $hash_len) ? $hash_len : $total; + $total -= $bytes; + + //collect any entropy available from the PHP system and filesystem + $entropy = rand() . uniqid(mt_rand(), true) . $SSLstr; + $entropy .= implode('', @fstat(@fopen(__FILE__, 'r'))); + $entropy .= memory_get_usage() . getmypid(); + $entropy .= serialize($_ENV) . serialize($_SERVER); + if (function_exists('posix_times')) { + $entropy .= serialize(posix_times()); + } + if (function_exists('zend_thread_id')) { + $entropy .= zend_thread_id(); + } + + if ($handle) { + $entropy .= @fread($handle, $bytes); + } else { + // Measure the time that the operations will take on average + for ($i = 0; $i < 3; $i++) { + $c1 = microtime(true); + $var = sha1(mt_rand()); + for ($j = 0; $j < 50; $j++) { + $var = sha1($var); + } + $c2 = microtime(true); + $entropy .= $c1 . $c2; + } + + // Based on the above measurement determine the total rounds + // in order to bound the total running time. + $rounds = (int) ($msec_per_round * 50 / (int) (($c2 - $c1) * 1000000)); + + // Take the additional measurements. On average we can expect + // at least $bits_per_round bits of entropy from each measurement. + $iter = $bytes * (int) (ceil(8 / $bits_per_round)); + + for ($i = 0; $i < $iter; $i++) { + $c1 = microtime(); + $var = sha1(mt_rand()); + for ($j = 0; $j < $rounds; $j++) { + $var = sha1($var); + } + $c2 = microtime(); + $entropy .= $c1 . $c2; + } + } + + // We assume sha1 is a deterministic extractor for the $entropy variable. + $str .= sha1($entropy, true); + + } while ($length > strlen($str)); + + if ($handle) { + @fclose($handle); + } + + return substr($str, 0, $length); + } + + /** + * Generate a random string of specified length. + * + * Uses supplied character list for generating the new string. + * If no character list provided - uses Base64 URL character set. + * + * @param int $length Desired length of the string + * @param string|null $chars Characters to be chosen from randomly. If not given, the Base64 URL + * charset will be used. + * + * @return string The random string + * + * @throws InvalidArgumentException + * + * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * + * @see https://github.com/zendframework/zf2/blob/master/library/Zend/Math/Rand.php#L179 + */ + public static function getRandomString($length, $chars = null) { + if ($length < 1) { + throw new InvalidArgumentException('Length should be >= 1'); + } + + if (empty($chars)) { + $numBytes = ceil($length * 0.75); + $bytes = self::getRandomBytes($numBytes); + $string = substr(rtrim(base64_encode($bytes), '='), 0, $length); + + // Base64 URL + return strtr($string, '+/', '-_'); + } + + $listLen = strlen($chars); + + if ($listLen == 1) { + return str_repeat($chars, $length); + } + + $bytes = self::getRandomBytes($length); + $pos = 0; + $result = ''; + for ($i = 0; $i < $length; $i++) { + $pos = ($pos + ord($bytes[$i])) % $listLen; + $result .= $chars[$pos]; + } + + return $result; + } +} diff --git a/engine/classes/ElggData.php b/engine/classes/ElggData.php new file mode 100644 index 000000000..4f843cde4 --- /dev/null +++ b/engine/classes/ElggData.php @@ -0,0 +1,309 @@ +<?php +/** + * A generic class that contains shared code b/w + * ElggExtender, ElggEntity, and ElggRelationship + * + * @package Elgg.Core + * @subpackage DataModel + * + * @property int $owner_guid + * @property int $time_created + */ +abstract class ElggData implements + Loggable, // Can events related to this object class be logged + Iterator, // Override foreach behaviour + ArrayAccess, // Override for array access + Exportable +{ + + /** + * The main attributes of an entity. + * Holds attributes to save to database + * This contains the site's main properties (id, etc) + * Blank entries for all database fields should be created by the constructor. + * Subclasses should add to this in their constructors. + * Any field not appearing in this will be viewed as a + */ + protected $attributes = array(); + + // @codingStandardsIgnoreStart + /** + * Initialise the attributes array. + * + * This is vital to distinguish between metadata and base parameters. + * + * @param bool $pre18_api Compatibility for subclassing in 1.7 -> 1.8 change. + * Passing true (default) emits a deprecation notice. + * Passing false returns false. Core constructors always pass false. + * Does nothing either way since attributes are initialized by the time + * this is called. + * @return void + * @deprecated 1.8 Use initializeAttributes() + */ + protected function initialise_attributes($pre18_api = true) { + if ($pre18_api) { + elgg_deprecated_notice('initialise_attributes() is deprecated by initializeAttributes()', 1.8); + } + } + // @codingStandardsIgnoreEnd + + /** + * Initialize the attributes array. + * + * This is vital to distinguish between metadata and base parameters. + * + * @return void + */ + protected function initializeAttributes() { + // Create attributes array if not already created + if (!is_array($this->attributes)) { + $this->attributes = array(); + } + + $this->attributes['time_created'] = NULL; + } + + /** + * Return an attribute or a piece of metadata. + * + * @param string $name Name + * + * @return mixed + */ + public function __get($name) { + return $this->get($name); + } + + /** + * Set an attribute or a piece of metadata. + * + * @param string $name Name + * @param mixed $value Value + * + * @return mixed + */ + public function __set($name, $value) { + return $this->set($name, $value); + } + + /** + * Test if property is set either as an attribute or metadata. + * + * @tip Use isset($entity->property) + * + * @param string $name The name of the attribute or metadata. + * + * @return bool + */ + function __isset($name) { + return $this->$name !== NULL; + } + + /** + * Fetch the specified attribute + * + * @param string $name The attribute to fetch + * + * @return mixed The attribute, if it exists. Otherwise, null. + */ + abstract protected function get($name); + + /** + * Set the specified attribute + * + * @param string $name The attribute to set + * @param mixed $value The value to set it to + * + * @return bool The success of your set function? + */ + abstract protected function set($name, $value); + + /** + * Get a URL for this object + * + * @return string + */ + abstract public function getURL(); + + /** + * Save this data to the appropriate database table. + * + * @return bool + */ + abstract public function save(); + + /** + * Delete this data. + * + * @return bool + */ + abstract public function delete(); + + /** + * Returns the UNIX epoch time that this entity was created + * + * @return int UNIX epoch time + */ + public function getTimeCreated() { + return $this->time_created; + } + + /* + * SYSTEM LOG INTERFACE + */ + + /** + * Return the class name of the object. + * + * @return string + */ + public function getClassName() { + return get_class($this); + } + + /** + * Return the GUID of the owner of this object. + * + * @return int + * @deprecated 1.8 Use getOwnerGUID() instead + */ + public function getObjectOwnerGUID() { + elgg_deprecated_notice("getObjectOwnerGUID() was deprecated. Use getOwnerGUID().", 1.8); + return $this->owner_guid; + } + + /* + * ITERATOR INTERFACE + */ + + /* + * This lets an entity's attributes be displayed using foreach as a normal array. + * Example: http://www.sitepoint.com/print/php5-standard-library + */ + protected $valid = FALSE; + + /** + * Iterator interface + * + * @see Iterator::rewind() + * + * @return void + */ + public function rewind() { + $this->valid = (FALSE !== reset($this->attributes)); + } + + /** + * Iterator interface + * + * @see Iterator::current() + * + * @return mixed + */ + public function current() { + return current($this->attributes); + } + + /** + * Iterator interface + * + * @see Iterator::key() + * + * @return string + */ + public function key() { + return key($this->attributes); + } + + /** + * Iterator interface + * + * @see Iterator::next() + * + * @return void + */ + public function next() { + $this->valid = (FALSE !== next($this->attributes)); + } + + /** + * Iterator interface + * + * @see Iterator::valid() + * + * @return bool + */ + public function valid() { + return $this->valid; + } + + /* + * ARRAY ACCESS INTERFACE + */ + + /* + * This lets an entity's attributes be accessed like an associative array. + * Example: http://www.sitepoint.com/print/php5-standard-library + */ + + /** + * Array access interface + * + * @see ArrayAccess::offsetSet() + * + * @param mixed $key Name + * @param mixed $value Value + * + * @return void + */ + public function offsetSet($key, $value) { + if (array_key_exists($key, $this->attributes)) { + $this->attributes[$key] = $value; + } + } + + /** + * Array access interface + * + * @see ArrayAccess::offsetGet() + * + * @param mixed $key Name + * + * @return mixed + */ + public function offsetGet($key) { + if (array_key_exists($key, $this->attributes)) { + return $this->attributes[$key]; + } + return null; + } + + /** + * Array access interface + * + * @see ArrayAccess::offsetUnset() + * + * @param mixed $key Name + * + * @return void + */ + public function offsetUnset($key) { + if (array_key_exists($key, $this->attributes)) { + // Full unsetting is dangerous for our objects + $this->attributes[$key] = ""; + } + } + + /** + * Array access interface + * + * @see ArrayAccess::offsetExists() + * + * @param int $offset Offset + * + * @return int + */ + public function offsetExists($offset) { + return array_key_exists($offset, $this->attributes); + } +} diff --git a/engine/classes/ElggDiskFilestore.php b/engine/classes/ElggDiskFilestore.php new file mode 100644 index 000000000..6e2354012 --- /dev/null +++ b/engine/classes/ElggDiskFilestore.php @@ -0,0 +1,417 @@ +<?php +/** + * A filestore that uses disk as storage. + * + * @warning This should be used by a wrapper class + * like {@link ElggFile}. + * + * @package Elgg.Core + * @subpackage FileStore.Disk + * @link http://docs.elgg.org/DataModel/FileStore/Disk + */ +class ElggDiskFilestore extends ElggFilestore { + /** + * Directory root. + */ + private $dir_root; + + /** + * Default depth of file directory matrix + */ + private $matrix_depth = 5; + + /** + * Construct a disk filestore using the given directory root. + * + * @param string $directory_root Root directory, must end in "/" + */ + public function __construct($directory_root = "") { + global $CONFIG; + + if ($directory_root) { + $this->dir_root = $directory_root; + } else { + $this->dir_root = $CONFIG->dataroot; + } + } + + /** + * Open a file for reading, writing, or both. + * + * @note All files are opened binary safe. + * @warning This will try to create the a directory if it doesn't exist, + * even in read-only mode. + * + * @param ElggFile $file The file to open + * @param string $mode read, write, or append. + * + * @throws InvalidParameterException + * @return resource File pointer resource + * @todo This really shouldn't try to create directories if not writing. + */ + public function open(ElggFile $file, $mode) { + $fullname = $this->getFilenameOnFilestore($file); + + // Split into path and name + $ls = strrpos($fullname, "/"); + if ($ls === false) { + $ls = 0; + } + + $path = substr($fullname, 0, $ls); + $name = substr($fullname, $ls); + // @todo $name is unused, remove it or do we need to fix something? + + // Try and create the directory + try { + $this->makeDirectoryRoot($path); + } catch (Exception $e) { + + } + + if (($mode != 'write') && (!file_exists($fullname))) { + return false; + } + + switch ($mode) { + case "read" : + $mode = "rb"; + break; + case "write" : + $mode = "w+b"; + break; + case "append" : + $mode = "a+b"; + break; + default: + $msg = elgg_echo('InvalidParameterException:UnrecognisedFileMode', array($mode)); + throw new InvalidParameterException($msg); + } + + return fopen($fullname, $mode); + + } + + /** + * Write data to a file. + * + * @param resource $f File pointer resource + * @param mixed $data The data to write. + * + * @return bool + */ + public function write($f, $data) { + return fwrite($f, $data); + } + + /** + * Read data from a file. + * + * @param resource $f File pointer resource + * @param int $length The number of bytes to read + * @param int $offset The number of bytes to start after + * + * @return mixed Contents of file or false on fail. + */ + public function read($f, $length, $offset = 0) { + if ($offset) { + $this->seek($f, $offset); + } + + return fread($f, $length); + } + + /** + * Close a file pointer + * + * @param resource $f A file pointer resource + * + * @return bool + */ + public function close($f) { + return fclose($f); + } + + /** + * Delete an ElggFile file. + * + * @param ElggFile $file File to delete + * + * @return bool + */ + public function delete(ElggFile $file) { + $filename = $this->getFilenameOnFilestore($file); + if (file_exists($filename)) { + return unlink($filename); + } else { + return true; + } + } + + /** + * Seek to the specified position. + * + * @param resource $f File resource + * @param int $position Position in bytes + * + * @return bool + */ + public function seek($f, $position) { + return fseek($f, $position); + } + + /** + * Return the current location of the internal pointer + * + * @param resource $f File pointer resource + * + * @return int|false + */ + public function tell($f) { + return ftell($f); + } + + /** + * Tests for end of file on a file pointer + * + * @param resource $f File pointer resource + * + * @return bool + */ + public function eof($f) { + return feof($f); + } + + /** + * Returns the file size of an ElggFile file. + * + * @param ElggFile $file File object + * + * @return int The file size + */ + public function getFileSize(ElggFile $file) { + return filesize($this->getFilenameOnFilestore($file)); + } + + /** + * Get the filename as saved on disk for an ElggFile object + * + * Returns an empty string if no filename set + * + * @param ElggFile $file File object + * + * @return string The full path of where the file is stored + * @throws InvalidParameterException + */ + public function getFilenameOnFilestore(ElggFile $file) { + $owner_guid = $file->getOwnerGuid(); + if (!$owner_guid) { + $owner_guid = elgg_get_logged_in_user_guid(); + } + + if (!$owner_guid) { + $msg = elgg_echo('InvalidParameterException:MissingOwner', + array($file->getFilename(), $file->guid)); + throw new InvalidParameterException($msg); + } + + $filename = $file->getFilename(); + if (!$filename) { + return ''; + } + + return $this->dir_root . $this->makeFileMatrix($owner_guid) . $filename; + } + + /** + * Returns the contents of the ElggFile file. + * + * @param ElggFile $file File object + * + * @return string + */ + public function grabFile(ElggFile $file) { + return file_get_contents($file->getFilenameOnFilestore()); + } + + /** + * Tests if an ElggFile file exists. + * + * @param ElggFile $file File object + * + * @return bool + */ + public function exists(ElggFile $file) { + if (!$file->getFilename()) { + return false; + } + return file_exists($this->getFilenameOnFilestore($file)); + } + + /** + * Returns the size of all data stored under a directory in the disk store. + * + * @param string $prefix Optional/ The prefix to check under. + * @param string $container_guid The guid of the entity whose data you want to check. + * + * @return int|false + */ + public function getSize($prefix = '', $container_guid) { + if ($container_guid) { + return get_dir_size($this->dir_root . $this->makeFileMatrix($container_guid) . $prefix); + } else { + return false; + } + } + + // @codingStandardsIgnoreStart + /** + * Create a directory $dirroot + * + * @param string $dirroot The full path of the directory to create + * + * @throws IOException + * @return true + * @deprecated 1.8 Use ElggDiskFilestore::makeDirectoryRoot() + */ + protected function make_directory_root($dirroot) { + elgg_deprecated_notice('ElggDiskFilestore::make_directory_root() is deprecated by ::makeDirectoryRoot()', 1.8); + + return $this->makeDirectoryRoot($dirroot); + } + // @codingStandardsIgnoreEnd + + /** + * Create a directory $dirroot + * + * @param string $dirroot The full path of the directory to create + * + * @throws IOException + * @return true + */ + protected function makeDirectoryRoot($dirroot) { + if (!file_exists($dirroot)) { + if (!@mkdir($dirroot, 0700, true)) { + throw new IOException(elgg_echo('IOException:CouldNotMake', array($dirroot))); + } + } + + return true; + } + + // @codingStandardsIgnoreStart + /** + * Multibyte string tokeniser. + * + * Splits a string into an array. Will fail safely if mbstring is + * not installed. + * + * @param string $string String + * @param string $charset The charset, defaults to UTF8 + * + * @return array + * @deprecated 1.8 Files are stored by date and guid; no need for this. + */ + private function mb_str_split($string, $charset = 'UTF8') { + elgg_deprecated_notice('ElggDiskFilestore::mb_str_split() is deprecated.', 1.8); + + if (is_callable('mb_substr')) { + $length = mb_strlen($string); + $array = array(); + + while ($length) { + $array[] = mb_substr($string, 0, 1, $charset); + $string = mb_substr($string, 1, $length, $charset); + + $length = mb_strlen($string); + } + + return $array; + } else { + return str_split($string); + } + } + // @codingStandardsIgnoreEnd + + // @codingStandardsIgnoreStart + /** + * Construct a file path matrix for an entity. + * + * @param int $identifier The guide of the entity to store the data under. + * + * @return string The path where the entity's data will be stored. + * @deprecated 1.8 Use ElggDiskFilestore::makeFileMatrix() + */ + protected function make_file_matrix($identifier) { + elgg_deprecated_notice('ElggDiskFilestore::make_file_matrix() is deprecated by ::makeFileMatrix()', 1.8); + + return $this->makeFileMatrix($identifier); + } + // @codingStandardsIgnoreEnd + + /** + * Construct a file path matrix for an entity. + * + * @param int $guid The guide of the entity to store the data under. + * + * @return string The path where the entity's data will be stored. + */ + protected function makeFileMatrix($guid) { + $entity = get_entity($guid); + + if (!($entity instanceof ElggEntity) || !$entity->time_created) { + return false; + } + + $time_created = date('Y/m/d', $entity->time_created); + + return "$time_created/$entity->guid/"; + } + + // @codingStandardsIgnoreStart + /** + * Construct a filename matrix. + * + * Generates a matrix using the entity's creation time and + * unique guid. + * + * File path matrixes are: + * YYYY/MM/DD/guid/ + * + * @param int $guid The entity to contrust a matrix for + * + * @return string The + */ + protected function user_file_matrix($guid) { + elgg_deprecated_notice('ElggDiskFilestore::user_file_matrix() is deprecated by ::makeFileMatrix()', 1.8); + + return $this->makeFileMatrix($guid); + } + // @codingStandardsIgnoreEnd + + /** + * Returns a list of attributes to save to the database when saving + * the ElggFile object using this file store. + * + * @return array + */ + public function getParameters() { + return array("dir_root" => $this->dir_root); + } + + /** + * Sets parameters that should be saved to database. + * + * @param array $parameters Set parameters to save to DB for this filestore. + * + * @return bool + */ + public function setParameters(array $parameters) { + if (isset($parameters['dir_root'])) { + $this->dir_root = $parameters['dir_root']; + return true; + } + + return false; + } +} diff --git a/engine/classes/ElggEntity.php b/engine/classes/ElggEntity.php new file mode 100644 index 000000000..a563f6fad --- /dev/null +++ b/engine/classes/ElggEntity.php @@ -0,0 +1,1770 @@ +<?php +/** + * The parent class for all Elgg Entities. + * + * An ElggEntity is one of the basic data models in Elgg. It is the primary + * means of storing and retrieving data from the database. An ElggEntity + * represents one row of the entities table. + * + * The ElggEntity class handles CRUD operations for the entities table. + * ElggEntity should always be extended by another class to handle CRUD + * operations on the type-specific table. + * + * ElggEntity uses magic methods for get and set, so any property that isn't + * declared will be assumed to be metadata and written to the database + * as metadata on the object. All children classes must declare which + * properties are columns of the type table or they will be assumed + * to be metadata. See ElggObject::initialise_entities() for examples. + * + * Core supports 4 types of entities: ElggObject, ElggUser, ElggGroup, and + * ElggSite. + * + * @tip Most plugin authors will want to extend the ElggObject class + * instead of this class. + * + * @package Elgg.Core + * @subpackage DataModel.Entities + * + * @property string $type object, user, group, or site (read-only after save) + * @property string $subtype Further clarifies the nature of the entity (read-only after save) + * @property int $guid The unique identifier for this entity (read only) + * @property int $owner_guid The GUID of the creator of this entity + * @property int $container_guid The GUID of the entity containing this entity + * @property int $site_guid The GUID of the website this entity is associated with + * @property int $access_id Specifies the visibility level of this entity + * @property int $time_created A UNIX timestamp of when the entity was created (read-only, set on first save) + * @property int $time_updated A UNIX timestamp of when the entity was last updated (automatically updated on save) + * @property-read string $enabled + */ +abstract class ElggEntity extends ElggData implements + Notable, // Calendar interface + Locatable, // Geocoding interface + Importable // Allow import of data +{ + + /** + * If set, overrides the value of getURL() + */ + protected $url_override; + + /** + * Icon override, overrides the value of getIcon(). + */ + protected $icon_override; + + /** + * Holds metadata until entity is saved. Once the entity is saved, + * metadata are written immediately to the database. + */ + protected $temp_metadata = array(); + + /** + * Holds annotations until entity is saved. Once the entity is saved, + * annotations are written immediately to the database. + */ + protected $temp_annotations = array(); + + /** + * Holds private settings until entity is saved. Once the entity is saved, + * private settings are written immediately to the database. + */ + protected $temp_private_settings = array(); + + /** + * Volatile data structure for this object, allows for storage of data + * in-memory that isn't sync'd back to the metadata table. + */ + protected $volatile = array(); + + /** + * Initialize the attributes array. + * + * This is vital to distinguish between metadata and base parameters. + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['guid'] = NULL; + $this->attributes['type'] = NULL; + $this->attributes['subtype'] = NULL; + + $this->attributes['owner_guid'] = elgg_get_logged_in_user_guid(); + $this->attributes['container_guid'] = elgg_get_logged_in_user_guid(); + + $this->attributes['site_guid'] = NULL; + $this->attributes['access_id'] = ACCESS_PRIVATE; + $this->attributes['time_created'] = NULL; + $this->attributes['time_updated'] = NULL; + $this->attributes['last_action'] = NULL; + $this->attributes['enabled'] = "yes"; + + // There now follows a bit of a hack + /* Problem: To speed things up, some objects are split over several tables, + * this means that it requires n number of database reads to fully populate + * an entity. This causes problems for caching and create events + * since it is not possible to tell whether a subclassed entity is complete. + * + * Solution: We have two counters, one 'tables_split' which tells whatever is + * interested how many tables are going to need to be searched in order to fully + * populate this object, and 'tables_loaded' which is how many have been + * loaded thus far. + * + * If the two are the same then this object is complete. + * + * Use: isFullyLoaded() to check + */ + $this->attributes['tables_split'] = 1; + $this->attributes['tables_loaded'] = 0; + } + + /** + * Clone an entity + * + * Resets the guid so that the entity can be saved as a distinct entity from + * the original. Creation time will be set when this new entity is saved. + * The owner and container guids come from the original entity. The clone + * method copies metadata but does not copy annotations or private settings. + * + * @note metadata will have its owner and access id set when the entity is saved + * and it will be the same as that of the entity. + * + * @return void + */ + public function __clone() { + $orig_entity = get_entity($this->guid); + if (!$orig_entity) { + elgg_log("Failed to clone entity with GUID $this->guid", "ERROR"); + return; + } + + $metadata_array = elgg_get_metadata(array( + 'guid' => $this->guid, + 'limit' => 0 + )); + + $this->attributes['guid'] = ""; + + $this->attributes['subtype'] = $orig_entity->getSubtype(); + + // copy metadata over to new entity - slightly convoluted due to + // handling of metadata arrays + if (is_array($metadata_array)) { + // create list of metadata names + $metadata_names = array(); + foreach ($metadata_array as $metadata) { + $metadata_names[] = $metadata['name']; + } + // arrays are stored with multiple enties per name + $metadata_names = array_unique($metadata_names); + + // move the metadata over + foreach ($metadata_names as $name) { + $this->set($name, $orig_entity->$name); + } + } + } + + /** + * Return the value of a property. + * + * If $name is defined in $this->attributes that value is returned, otherwise it will + * pull from the entity's metadata. + * + * Q: Why are we not using __get overload here? + * A: Because overload operators cause problems during subclassing, so we put the code here and + * create overloads in subclasses. + * + * @todo What problems are these? + * + * @warning Subtype is returned as an id rather than the subtype string. Use getSubtype() + * to get the subtype string. + * + * @param string $name Name + * + * @return mixed Returns the value of a given value, or null. + */ + public function get($name) { + // See if its in our base attributes + if (array_key_exists($name, $this->attributes)) { + return $this->attributes[$name]; + } + + // No, so see if its in the meta data for this entity + $meta = $this->getMetaData($name); + + // getMetaData returns NULL if $name is not found + return $meta; + } + + /** + * Sets the value of a property. + * + * If $name is defined in $this->attributes that value is set, otherwise it is + * saved as metadata. + * + * @warning Metadata set this way will inherit the entity's owner and access ID. If you want + * to set metadata with a different owner, use create_metadata(). + * + * @warning It is important that your class populates $this->attributes with keys + * for all base attributes, anything not in their gets set as METADATA. + * + * Q: Why are we not using __set overload here? + * A: Because overload operators cause problems during subclassing, so we put the code here and + * create overloads in subclasses. + * + * @todo What problems? + * + * @param string $name Name + * @param mixed $value Value + * + * @return bool + */ + public function set($name, $value) { + if (array_key_exists($name, $this->attributes)) { + // Certain properties should not be manually changed! + switch ($name) { + case 'guid': + case 'time_updated': + case 'last_action': + return FALSE; + break; + default: + $this->attributes[$name] = $value; + break; + } + } else { + return $this->setMetaData($name, $value); + } + + return TRUE; + } + + /** + * Return the value of a piece of metadata. + * + * @param string $name Name + * + * @return mixed The value, or NULL if not found. + */ + public function getMetaData($name) { + $guid = $this->getGUID(); + + if (! $guid) { + if (isset($this->temp_metadata[$name])) { + // md is returned as an array only if more than 1 entry + if (count($this->temp_metadata[$name]) == 1) { + return $this->temp_metadata[$name][0]; + } else { + return $this->temp_metadata[$name]; + } + } else { + return null; + } + } + + // upon first cache miss, just load/cache all the metadata and retry. + // if this works, the rest of this function may not be needed! + $cache = elgg_get_metadata_cache(); + if ($cache->isKnown($guid, $name)) { + return $cache->load($guid, $name); + } else { + $cache->populateFromEntities(array($guid)); + // in case ignore_access was on, we have to check again... + if ($cache->isKnown($guid, $name)) { + return $cache->load($guid, $name); + } + } + + $md = elgg_get_metadata(array( + 'guid' => $guid, + 'metadata_name' => $name, + 'limit' => 0, + )); + + $value = null; + + if ($md && !is_array($md)) { + $value = $md->value; + } elseif (count($md) == 1) { + $value = $md[0]->value; + } else if ($md && is_array($md)) { + $value = metadata_array_to_values($md); + } + + $cache->save($guid, $name, $value); + + return $value; + } + + /** + * Unset a property from metadata or attribute. + * + * @warning If you use this to unset an attribute, you must save the object! + * + * @param string $name The name of the attribute or metadata. + * + * @return void + */ + function __unset($name) { + if (array_key_exists($name, $this->attributes)) { + $this->attributes[$name] = ""; + } else { + $this->deleteMetadata($name); + } + } + + /** + * Set a piece of metadata. + * + * Plugin authors should use the magic methods or create_metadata(). + * + * @warning The metadata will inherit the parent entity's owner and access ID. + * If you want to write metadata with a different owner, use create_metadata(). + * + * @access private + * + * @param string $name Name of the metadata + * @param mixed $value Value of the metadata (doesn't support assoc arrays) + * @param string $value_type Types supported: integer and string. Will auto-identify if not set + * @param bool $multiple Allow multiple values for a single name (doesn't support assoc arrays) + * + * @return bool + */ + public function setMetaData($name, $value, $value_type = null, $multiple = false) { + + // normalize value to an array that we will loop over + // remove indexes if value already an array. + if (is_array($value)) { + $value = array_values($value); + } else { + $value = array($value); + } + + // saved entity. persist md to db. + if ($this->guid) { + // if overwriting, delete first. + if (!$multiple) { + $options = array( + 'guid' => $this->getGUID(), + 'metadata_name' => $name, + 'limit' => 0 + ); + // @todo in 1.9 make this return false if can't add metadata + // https://github.com/elgg/elgg/issues/4520 + // + // need to remove access restrictions right now to delete + // because this is the expected behavior + $ia = elgg_set_ignore_access(true); + if (false === elgg_delete_metadata($options)) { + return false; + } + elgg_set_ignore_access($ia); + } + + // add new md + $result = true; + foreach ($value as $value_tmp) { + // at this point $value should be appended because it was cleared above if needed. + $md_id = create_metadata($this->getGUID(), $name, $value_tmp, $value_type, + $this->getOwnerGUID(), $this->getAccessId(), true); + if (!$md_id) { + return false; + } + } + + return $result; + } else { + // unsaved entity. store in temp array + // returning single entries instead of an array of 1 element is decided in + // getMetaData(), just like pulling from the db. + // + // if overwrite, delete first + if (!$multiple || !isset($this->temp_metadata[$name])) { + $this->temp_metadata[$name] = array(); + } + + // add new md + $this->temp_metadata[$name] = array_merge($this->temp_metadata[$name], $value); + return true; + } + } + + /** + * Deletes all metadata on this object (metadata.entity_guid = $this->guid). + * If you pass a name, only metadata matching that name will be deleted. + * + * @warning Calling this with no $name will clear all metadata on the entity. + * + * @param null|string $name The name of the metadata to remove. + * @return bool + * @since 1.8 + */ + public function deleteMetadata($name = null) { + + if (!$this->guid) { + return false; + } + + $options = array( + 'guid' => $this->guid, + 'limit' => 0 + ); + if ($name) { + $options['metadata_name'] = $name; + } + + return elgg_delete_metadata($options); + } + + /** + * Deletes all metadata owned by this object (metadata.owner_guid = $this->guid). + * If you pass a name, only metadata matching that name will be deleted. + * + * @param null|string $name The name of metadata to delete. + * @return bool + * @since 1.8 + */ + public function deleteOwnedMetadata($name = null) { + // access is turned off for this because they might + // no longer have access to an entity they created metadata on. + $ia = elgg_set_ignore_access(true); + $options = array( + 'metadata_owner_guid' => $this->guid, + 'limit' => 0 + ); + if ($name) { + $options['metadata_name'] = $name; + } + + $r = elgg_delete_metadata($options); + elgg_set_ignore_access($ia); + return $r; + } + + /** + * Remove metadata + * + * @warning Calling this with no or empty arguments will clear all metadata on the entity. + * + * @param string $name The name of the metadata to clear + * @return mixed bool + * @deprecated 1.8 Use deleteMetadata() + */ + public function clearMetaData($name = '') { + elgg_deprecated_notice('ElggEntity->clearMetadata() is deprecated by ->deleteMetadata()', 1.8); + return $this->deleteMetadata($name); + } + + /** + * Disables metadata for this entity, optionally based on name. + * + * @param string $name An options name of metadata to disable. + * @return bool + * @since 1.8 + */ + public function disableMetadata($name = '') { + $options = array( + 'guid' => $this->guid, + 'limit' => 0 + ); + if ($name) { + $options['metadata_name'] = $name; + } + + return elgg_disable_metadata($options); + } + + /** + * Enables metadata for this entity, optionally based on name. + * + * @warning Before calling this, you must use {@link access_show_hidden_entities()} + * + * @param string $name An options name of metadata to enable. + * @return bool + * @since 1.8 + */ + public function enableMetadata($name = '') { + $options = array( + 'guid' => $this->guid, + 'limit' => 0 + ); + if ($name) { + $options['metadata_name'] = $name; + } + + return elgg_enable_metadata($options); + } + + /** + * Get a piece of volatile (non-persisted) data on this entity. + * + * @param string $name The name of the volatile data + * + * @return mixed The value or NULL if not found. + */ + public function getVolatileData($name) { + if (!is_array($this->volatile)) { + $this->volatile = array(); + } + + if (array_key_exists($name, $this->volatile)) { + return $this->volatile[$name]; + } else { + return NULL; + } + } + + /** + * Set a piece of volatile (non-persisted) data on this entity + * + * @param string $name Name + * @param mixed $value Value + * + * @return void + */ + public function setVolatileData($name, $value) { + if (!is_array($this->volatile)) { + $this->volatile = array(); + } + + $this->volatile[$name] = $value; + } + + /** + * Remove all relationships to and from this entity. + * + * @return true + * @todo This should actually return if it worked. + * @see ElggEntity::addRelationship() + * @see ElggEntity::removeRelationship() + */ + public function deleteRelationships() { + remove_entity_relationships($this->getGUID()); + remove_entity_relationships($this->getGUID(), "", true); + return true; + } + + /** + * Remove all relationships to and from this entity. + * + * @return bool + * @see ElggEntity::addRelationship() + * @see ElggEntity::removeRelationship() + * @deprecated 1.8 Use ->deleteRelationship() + */ + public function clearRelationships() { + elgg_deprecated_notice('ElggEntity->clearRelationships() is deprecated by ->deleteRelationships()', 1.8); + return $this->deleteRelationships(); + } + + /** + * Add a relationship between this an another entity. + * + * @tip Read the relationship like "$guid is a $relationship of this entity." + * + * @param int $guid Entity to link to. + * @param string $relationship The type of relationship. + * + * @return bool + * @see ElggEntity::removeRelationship() + * @see ElggEntity::clearRelationships() + */ + public function addRelationship($guid, $relationship) { + return add_entity_relationship($this->getGUID(), $relationship, $guid); + } + + /** + * Remove a relationship + * + * @param int $guid GUID of the entity to make a relationship with + * @param str $relationship Name of relationship + * + * @return bool + * @see ElggEntity::addRelationship() + * @see ElggEntity::clearRelationships() + */ + public function removeRelationship($guid, $relationship) { + return remove_entity_relationship($this->getGUID(), $relationship, $guid); + } + + /** + * Adds a private setting to this entity. + * + * Private settings are similar to metadata but will not + * be searched and there are fewer helper functions for them. + * + * @param string $name Name of private setting + * @param mixed $value Value of private setting + * + * @return bool + */ + function setPrivateSetting($name, $value) { + if ((int) $this->guid > 0) { + return set_private_setting($this->getGUID(), $name, $value); + } else { + $this->temp_private_settings[$name] = $value; + return true; + } + } + + /** + * Returns a private setting value + * + * @param string $name Name of the private setting + * + * @return mixed + */ + function getPrivateSetting($name) { + if ((int) ($this->guid) > 0) { + return get_private_setting($this->getGUID(), $name); + } else { + if (isset($this->temp_private_settings[$name])) { + return $this->temp_private_settings[$name]; + } + } + return null; + } + + /** + * Removes private setting + * + * @param string $name Name of the private setting + * + * @return bool + */ + function removePrivateSetting($name) { + return remove_private_setting($this->getGUID(), $name); + } + + /** + * Deletes all annotations on this object (annotations.entity_guid = $this->guid). + * If you pass a name, only annotations matching that name will be deleted. + * + * @warning Calling this with no or empty arguments will clear all annotations on the entity. + * + * @param null|string $name The annotations name to remove. + * @return bool + * @since 1.8 + */ + public function deleteAnnotations($name = null) { + $options = array( + 'guid' => $this->guid, + 'limit' => 0 + ); + if ($name) { + $options['annotation_name'] = $name; + } + + return elgg_delete_annotations($options); + } + + /** + * Deletes all annotations owned by this object (annotations.owner_guid = $this->guid). + * If you pass a name, only annotations matching that name will be deleted. + * + * @param null|string $name The name of annotations to delete. + * @return bool + * @since 1.8 + */ + public function deleteOwnedAnnotations($name = null) { + // access is turned off for this because they might + // no longer have access to an entity they created annotations on. + $ia = elgg_set_ignore_access(true); + $options = array( + 'annotation_owner_guid' => $this->guid, + 'limit' => 0 + ); + if ($name) { + $options['annotation_name'] = $name; + } + + $r = elgg_delete_annotations($options); + elgg_set_ignore_access($ia); + return $r; + } + + /** + * Disables annotations for this entity, optionally based on name. + * + * @param string $name An options name of annotations to disable. + * @return bool + * @since 1.8 + */ + public function disableAnnotations($name = '') { + $options = array( + 'guid' => $this->guid, + 'limit' => 0 + ); + if ($name) { + $options['annotation_name'] = $name; + } + + return elgg_disable_annotations($options); + } + + /** + * Enables annotations for this entity, optionally based on name. + * + * @warning Before calling this, you must use {@link access_show_hidden_entities()} + * + * @param string $name An options name of annotations to enable. + * @return bool + * @since 1.8 + */ + public function enableAnnotations($name = '') { + $options = array( + 'guid' => $this->guid, + 'limit' => 0 + ); + if ($name) { + $options['annotation_name'] = $name; + } + + return elgg_enable_annotations($options); + } + + /** + * Helper function to return annotation calculation results + * + * @param string $name The annotation name. + * @param string $calculation A valid MySQL function to run its values through + * @return mixed + */ + private function getAnnotationCalculation($name, $calculation) { + $options = array( + 'guid' => $this->getGUID(), + 'annotation_name' => $name, + 'annotation_calculation' => $calculation + ); + + return elgg_get_annotations($options); + } + + /** + * Adds an annotation to an entity. + * + * @warning By default, annotations are private. + * + * @warning Annotating an unsaved entity more than once with the same name + * will only save the last annotation. + * + * @param string $name Annotation name + * @param mixed $value Annotation value + * @param int $access_id Access ID + * @param int $owner_id GUID of the annotation owner + * @param string $vartype The type of annotation value + * + * @return bool + */ + function annotate($name, $value, $access_id = ACCESS_PRIVATE, $owner_id = 0, $vartype = "") { + if ((int) $this->guid > 0) { + return create_annotation($this->getGUID(), $name, $value, $vartype, $owner_id, $access_id); + } else { + $this->temp_annotations[$name] = $value; + } + return true; + } + + /** + * Returns an array of annotations. + * + * @param string $name Annotation name + * @param int $limit Limit + * @param int $offset Offset + * @param string $order Order by time: asc or desc + * + * @return array + */ + function getAnnotations($name, $limit = 50, $offset = 0, $order = "asc") { + if ((int) ($this->guid) > 0) { + + $options = array( + 'guid' => $this->guid, + 'annotation_name' => $name, + 'limit' => $limit, + 'offset' => $offset, + ); + + if ($order != 'asc') { + $options['reverse_order_by'] = true; + } + + return elgg_get_annotations($options); + } else if (isset($this->temp_annotations[$name])) { + return array($this->temp_annotations[$name]); + } else { + return array(); + } + } + + /** + * Remove an annotation or all annotations for this entity. + * + * @warning Calling this method with no or an empty argument will remove + * all annotations on the entity. + * + * @param string $name Annotation name + * @return bool + * @deprecated 1.8 Use ->deleteAnnotations() + */ + function clearAnnotations($name = "") { + elgg_deprecated_notice('ElggEntity->clearAnnotations() is deprecated by ->deleteAnnotations()', 1.8); + return $this->deleteAnnotations($name); + } + + /** + * Count annotations. + * + * @param string $name The type of annotation. + * + * @return int + */ + function countAnnotations($name = "") { + return $this->getAnnotationCalculation($name, 'count'); + } + + /** + * Get the average of an integer type annotation. + * + * @param string $name Annotation name + * + * @return int + */ + function getAnnotationsAvg($name) { + return $this->getAnnotationCalculation($name, 'avg'); + } + + /** + * Get the sum of integer type annotations of a given name. + * + * @param string $name Annotation name + * + * @return int + */ + function getAnnotationsSum($name) { + return $this->getAnnotationCalculation($name, 'sum'); + } + + /** + * Get the minimum of integer type annotations of given name. + * + * @param string $name Annotation name + * + * @return int + */ + function getAnnotationsMin($name) { + return $this->getAnnotationCalculation($name, 'min'); + } + + /** + * Get the maximum of integer type annotations of a given name. + * + * @param string $name Annotation name + * + * @return int + */ + function getAnnotationsMax($name) { + return $this->getAnnotationCalculation($name, 'max'); + } + + /** + * Count the number of comments attached to this entity. + * + * @return int Number of comments + * @since 1.8.0 + */ + function countComments() { + $params = array('entity' => $this); + $num = elgg_trigger_plugin_hook('comments:count', $this->getType(), $params); + + if (is_int($num)) { + return $num; + } else { + return $this->getAnnotationCalculation('generic_comment', 'count'); + } + } + + /** + * Gets an array of entities with a relationship to this entity. + * + * @param string $relationship Relationship type (eg "friends") + * @param bool $inverse Is this an inverse relationship? + * @param int $limit Number of elements to return + * @param int $offset Indexing offset + * + * @return array|false An array of entities or false on failure + */ + function getEntitiesFromRelationship($relationship, $inverse = false, $limit = 50, $offset = 0) { + return elgg_get_entities_from_relationship(array( + 'relationship' => $relationship, + 'relationship_guid' => $this->getGUID(), + 'inverse_relationship' => $inverse, + 'limit' => $limit, + 'offset' => $offset + )); + } + + /** + * Gets the number of of entities from a specific relationship type + * + * @param string $relationship Relationship type (eg "friends") + * @param bool $inverse_relationship Invert relationship + * + * @return int|false The number of entities or false on failure + */ + function countEntitiesFromRelationship($relationship, $inverse_relationship = FALSE) { + return elgg_get_entities_from_relationship(array( + 'relationship' => $relationship, + 'relationship_guid' => $this->getGUID(), + 'inverse_relationship' => $inverse_relationship, + 'count' => TRUE + )); + } + + /** + * Can a user edit this entity. + * + * @param int $user_guid The user GUID, optionally (default: logged in user) + * + * @return bool + */ + function canEdit($user_guid = 0) { + return can_edit_entity($this->getGUID(), $user_guid); + } + + /** + * Can a user edit metadata on this entity + * + * @param ElggMetadata $metadata The piece of metadata to specifically check + * @param int $user_guid The user GUID, optionally (default: logged in user) + * + * @return bool + */ + function canEditMetadata($metadata = null, $user_guid = 0) { + return can_edit_entity_metadata($this->getGUID(), $user_guid, $metadata); + } + + /** + * Can a user add an entity to this container + * + * @param int $user_guid The user. + * @param string $type The type of entity we're looking to write + * @param string $subtype The subtype of the entity we're looking to write + * + * @return bool + */ + public function canWriteToContainer($user_guid = 0, $type = 'all', $subtype = 'all') { + return can_write_to_container($user_guid, $this->guid, $type, $subtype); + } + + /** + * Can a user comment on an entity? + * + * @tip Can be overridden by registering for the permissions_check:comment, + * <entity type> plugin hook. + * + * @param int $user_guid User guid (default is logged in user) + * + * @return bool + */ + public function canComment($user_guid = 0) { + if ($user_guid == 0) { + $user_guid = elgg_get_logged_in_user_guid(); + } + $user = get_entity($user_guid); + + // By default, we don't take a position of whether commenting is allowed + // because it is handled by the subclasses of ElggEntity + $params = array('entity' => $this, 'user' => $user); + return elgg_trigger_plugin_hook('permissions_check:comment', $this->type, $params, null); + } + + /** + * Can a user annotate an entity? + * + * @tip Can be overridden by registering for the permissions_check:annotate, + * <entity type> plugin hook. + * + * @tip If you want logged out users to annotate an object, do not call + * canAnnotate(). It's easier than using the plugin hook. + * + * @param int $user_guid User guid (default is logged in user) + * @param string $annotation_name The name of the annotation (default is unspecified) + * + * @return bool + */ + public function canAnnotate($user_guid = 0, $annotation_name = '') { + if ($user_guid == 0) { + $user_guid = elgg_get_logged_in_user_guid(); + } + $user = get_entity($user_guid); + + $return = true; + if (!$user) { + $return = false; + } + + $params = array( + 'entity' => $this, + 'user' => $user, + 'annotation_name' => $annotation_name, + ); + return elgg_trigger_plugin_hook('permissions_check:annotate', $this->type, $params, $return); + } + + /** + * Returns the access_id. + * + * @return int The access ID + */ + public function getAccessID() { + return $this->get('access_id'); + } + + /** + * Returns the guid. + * + * @return int|null GUID + */ + public function getGUID() { + return $this->get('guid'); + } + + /** + * Returns the entity type + * + * @return string Entity type + */ + public function getType() { + return $this->get('type'); + } + + /** + * Returns the entity subtype string + * + * @note This returns a string. If you want the id, use ElggEntity::subtype. + * + * @return string The entity subtype + */ + public function getSubtype() { + // If this object hasn't been saved, then return the subtype string. + if (!((int) $this->guid > 0)) { + return $this->get('subtype'); + } + + return get_subtype_from_id($this->get('subtype')); + } + + /** + * Get the guid of the entity's owner. + * + * @return int The owner GUID + */ + public function getOwnerGUID() { + return $this->owner_guid; + } + + /** + * Return the guid of the entity's owner. + * + * @return int The owner GUID + * @deprecated 1.8 Use getOwnerGUID() + */ + public function getOwner() { + elgg_deprecated_notice("ElggEntity::getOwner deprecated for ElggEntity::getOwnerGUID", 1.8); + return $this->getOwnerGUID(); + } + + /** + * Gets the ElggEntity that owns this entity. + * + * @return ElggEntity The owning entity + */ + public function getOwnerEntity() { + return get_entity($this->owner_guid); + } + + /** + * Set the container for this object. + * + * @param int $container_guid The ID of the container. + * + * @return bool + */ + public function setContainerGUID($container_guid) { + $container_guid = (int)$container_guid; + + return $this->set('container_guid', $container_guid); + } + + /** + * Set the container for this object. + * + * @param int $container_guid The ID of the container. + * + * @return bool + * @deprecated 1.8 use setContainerGUID() + */ + public function setContainer($container_guid) { + elgg_deprecated_notice("ElggObject::setContainer deprecated for ElggEntity::setContainerGUID", 1.8); + $container_guid = (int)$container_guid; + + return $this->set('container_guid', $container_guid); + } + + /** + * Gets the container GUID for this entity. + * + * @return int + */ + public function getContainerGUID() { + return $this->get('container_guid'); + } + + /** + * Gets the container GUID for this entity. + * + * @return int + * @deprecated 1.8 Use getContainerGUID() + */ + public function getContainer() { + elgg_deprecated_notice("ElggObject::getContainer deprecated for ElggEntity::getContainerGUID", 1.8); + return $this->get('container_guid'); + } + + /** + * Get the container entity for this object. + * + * @return ElggEntity + * @since 1.8.0 + */ + public function getContainerEntity() { + return get_entity($this->getContainerGUID()); + } + + /** + * Returns the UNIX epoch time that this entity was last updated + * + * @return int UNIX epoch time + */ + public function getTimeUpdated() { + return $this->get('time_updated'); + } + + /** + * Returns the URL for this entity + * + * @return string The URL + * @see register_entity_url_handler() + * @see ElggEntity::setURL() + */ + public function getURL() { + if (!empty($this->url_override)) { + return $this->url_override; + } + return get_entity_url($this->getGUID()); + } + + /** + * Overrides the URL returned by getURL() + * + * @warning This override exists only for the life of the object. + * + * @param string $url The new item URL + * + * @return string The URL + */ + public function setURL($url) { + $this->url_override = $url; + return $url; + } + + /** + * Get the URL for this entity's icon + * + * Plugins can register for the 'entity:icon:url', <type> plugin hook + * to customize the icon for an entity. + * + * @param string $size Size of the icon: tiny, small, medium, large + * + * @return string The URL + * @since 1.8.0 + */ + public function getIconURL($size = 'medium') { + $size = elgg_strtolower($size); + + if (isset($this->icon_override[$size])) { + elgg_deprecated_notice("icon_override on an individual entity is deprecated", 1.8); + return $this->icon_override[$size]; + } + + $type = $this->getType(); + $params = array( + 'entity' => $this, + 'size' => $size, + ); + + $url = elgg_trigger_plugin_hook('entity:icon:url', $type, $params, null); + if ($url == null) { + $url = "_graphics/icons/default/$size.png"; + } + + return elgg_normalize_url($url); + } + + /** + * Returns a URL for the entity's icon. + * + * @param string $size Either 'large', 'medium', 'small' or 'tiny' + * + * @return string The url or false if no url could be worked out. + * @deprecated Use getIconURL() + */ + public function getIcon($size = 'medium') { + elgg_deprecated_notice("getIcon() deprecated by getIconURL()", 1.8); + return $this->getIconURL($size); + } + + /** + * Set an icon override for an icon and size. + * + * @warning This override exists only for the life of the object. + * + * @param string $url The url of the icon. + * @param string $size The size its for. + * + * @return bool + * @deprecated 1.8 See getIconURL() for the plugin hook to use + */ + public function setIcon($url, $size = 'medium') { + elgg_deprecated_notice("icon_override on an individual entity is deprecated", 1.8); + + $url = sanitise_string($url); + $size = sanitise_string($size); + + if (!$this->icon_override) { + $this->icon_override = array(); + } + $this->icon_override[$size] = $url; + + return true; + } + + /** + * Tests to see whether the object has been fully loaded. + * + * @return bool + */ + public function isFullyLoaded() { + return ! ($this->attributes['tables_loaded'] < $this->attributes['tables_split']); + } + + /** + * Save an entity. + * + * @return bool|int + * @throws IOException + */ + public function save() { + $guid = $this->getGUID(); + if ($guid > 0) { + + // See #5600. This ensures the lower level can_edit_entity() check will use a + // fresh entity from the DB so it sees the persisted owner_guid + _elgg_disable_caching_for_entity($guid); + + $ret = update_entity( + $guid, + $this->get('owner_guid'), + $this->get('access_id'), + $this->get('container_guid'), + $this->get('time_created') + ); + + _elgg_enable_caching_for_entity($guid); + _elgg_cache_entity($this); + + return $ret; + } else { + // Create a new entity (nb: using attribute array directly + // 'cos set function does something special!) + $this->attributes['guid'] = create_entity($this->attributes['type'], + $this->attributes['subtype'], $this->attributes['owner_guid'], + $this->attributes['access_id'], $this->attributes['site_guid'], + $this->attributes['container_guid']); + + if (!$this->attributes['guid']) { + throw new IOException(elgg_echo('IOException:BaseEntitySaveFailed')); + } + + // Save any unsaved metadata + // @todo How to capture extra information (access id etc) + if (sizeof($this->temp_metadata) > 0) { + foreach ($this->temp_metadata as $name => $value) { + $this->$name = $value; + unset($this->temp_metadata[$name]); + } + } + + // Save any unsaved annotations. + if (sizeof($this->temp_annotations) > 0) { + foreach ($this->temp_annotations as $name => $value) { + $this->annotate($name, $value); + unset($this->temp_annotations[$name]); + } + } + + // Save any unsaved private settings. + if (sizeof($this->temp_private_settings) > 0) { + foreach ($this->temp_private_settings as $name => $value) { + $this->setPrivateSetting($name, $value); + unset($this->temp_private_settings[$name]); + } + } + + // set the subtype to id now rather than a string + $this->attributes['subtype'] = get_subtype_id($this->attributes['type'], + $this->attributes['subtype']); + + _elgg_cache_entity($this); + + return $this->attributes['guid']; + } + } + + /** + * Loads attributes from the entities table into the object. + * + * @param mixed $guid GUID of entity or stdClass object from entities table + * + * @return bool + */ + protected function load($guid) { + if ($guid instanceof stdClass) { + $row = $guid; + } else { + $row = get_entity_as_row($guid); + } + + if ($row) { + // Create the array if necessary - all subclasses should test before creating + if (!is_array($this->attributes)) { + $this->attributes = array(); + } + + // Now put these into the attributes array as core values + $objarray = (array) $row; + foreach ($objarray as $key => $value) { + $this->attributes[$key] = $value; + } + + // Increment the portion counter + if (!$this->isFullyLoaded()) { + $this->attributes['tables_loaded']++; + } + + // guid needs to be an int https://github.com/elgg/elgg/issues/4111 + $this->attributes['guid'] = (int)$this->attributes['guid']; + + // Cache object handle + if ($this->attributes['guid']) { + _elgg_cache_entity($this); + } + + return true; + } + + return false; + } + + /** + * Disable this entity. + * + * Disabled entities are not returned by getter functions. + * To enable an entity, use {@link enable_entity()}. + * + * Recursively disabling an entity will disable all entities + * owned or contained by the parent entity. + * + * @internal Disabling an entity sets the 'enabled' column to 'no'. + * + * @param string $reason Optional reason + * @param bool $recursive Recursively disable all contained entities? + * + * @return bool + * @see enable_entity() + * @see ElggEntity::enable() + */ + public function disable($reason = "", $recursive = true) { + if ($r = disable_entity($this->get('guid'), $reason, $recursive)) { + $this->attributes['enabled'] = 'no'; + } + + return $r; + } + + /** + * Enable an entity + * + * @warning Disabled entities can't be loaded unless + * {@link access_show_hidden_entities(true)} has been called. + * + * @see enable_entity() + * @see access_show_hiden_entities() + * @return bool + */ + public function enable() { + if ($r = enable_entity($this->get('guid'))) { + $this->attributes['enabled'] = 'yes'; + } + + return $r; + } + + /** + * Is this entity enabled? + * + * @return boolean + */ + public function isEnabled() { + if ($this->enabled == 'yes') { + return true; + } + + return false; + } + + /** + * Delete this entity. + * + * @param bool $recursive Whether to delete all the entities contained by this entity + * + * @return bool + */ + public function delete($recursive = true) { + return delete_entity($this->get('guid'), $recursive); + } + + /* + * LOCATABLE INTERFACE + */ + + /** + * Gets the 'location' metadata for the entity + * + * @return string The location + */ + public function getLocation() { + return $this->location; + } + + /** + * Sets the 'location' metadata for the entity + * + * @todo Unimplemented + * + * @param string $location String representation of the location + * + * @return bool + */ + public function setLocation($location) { + $this->location = $location; + return true; + } + + /** + * Set latitude and longitude metadata tags for a given entity. + * + * @param float $lat Latitude + * @param float $long Longitude + * + * @return bool + * @todo Unimplemented + */ + public function setLatLong($lat, $long) { + $this->set('geo:lat', $lat); + $this->set('geo:long', $long); + + return true; + } + + /** + * Return the entity's latitude. + * + * @return float + * @todo Unimplemented + */ + public function getLatitude() { + return (float)$this->get('geo:lat'); + } + + /** + * Return the entity's longitude + * + * @return float + */ + public function getLongitude() { + return (float)$this->get('geo:long'); + } + + /* + * NOTABLE INTERFACE + */ + + /** + * Set the time and duration of an object + * + * @param int $hour If ommitted, now is assumed. + * @param int $minute If ommitted, now is assumed. + * @param int $second If ommitted, now is assumed. + * @param int $day If ommitted, now is assumed. + * @param int $month If ommitted, now is assumed. + * @param int $year If ommitted, now is assumed. + * @param int $duration Duration of event, remainder of the day is assumed. + * + * @return true + * @todo Unimplemented + */ + public function setCalendarTimeAndDuration($hour = NULL, $minute = NULL, $second = NULL, + $day = NULL, $month = NULL, $year = NULL, $duration = NULL) { + + $start = mktime($hour, $minute, $second, $month, $day, $year); + $end = $start + abs($duration); + if (!$duration) { + $end = get_day_end($day, $month, $year); + } + + $this->calendar_start = $start; + $this->calendar_end = $end; + + return true; + } + + /** + * Returns the start timestamp. + * + * @return int + * @todo Unimplemented + */ + public function getCalendarStartTime() { + return (int)$this->calendar_start; + } + + /** + * Returns the end timestamp. + * + * @todo Unimplemented + * + * @return int + */ + public function getCalendarEndTime() { + return (int)$this->calendar_end; + } + + /* + * EXPORTABLE INTERFACE + */ + + /** + * Returns an array of fields which can be exported. + * + * @return array + */ + public function getExportableValues() { + return array( + 'guid', + 'type', + 'subtype', + 'time_created', + 'time_updated', + 'container_guid', + 'owner_guid', + 'site_guid' + ); + } + + /** + * Export this class into an array of ODD Elements containing all necessary fields. + * Override if you wish to return more information than can be found in + * $this->attributes (shouldn't happen) + * + * @return array + */ + public function export() { + $tmp = array(); + + // Generate uuid + $uuid = guid_to_uuid($this->getGUID()); + + // Create entity + $odd = new ODDEntity( + $uuid, + $this->attributes['type'], + get_subtype_from_id($this->attributes['subtype']) + ); + + $tmp[] = $odd; + + $exportable_values = $this->getExportableValues(); + + // Now add its attributes + foreach ($this->attributes as $k => $v) { + $meta = NULL; + + if (in_array($k, $exportable_values)) { + switch ($k) { + case 'guid': // Dont use guid in OpenDD + case 'type': // Type and subtype already taken care of + case 'subtype': + break; + + case 'time_created': // Created = published + $odd->setAttribute('published', date("r", $v)); + break; + + case 'site_guid': // Container + $k = 'site_uuid'; + $v = guid_to_uuid($v); + $meta = new ODDMetaData($uuid . "attr/$k/", $uuid, $k, $v); + break; + + case 'container_guid': // Container + $k = 'container_uuid'; + $v = guid_to_uuid($v); + $meta = new ODDMetaData($uuid . "attr/$k/", $uuid, $k, $v); + break; + + case 'owner_guid': // Convert owner guid to uuid, this will be stored in metadata + $k = 'owner_uuid'; + $v = guid_to_uuid($v); + $meta = new ODDMetaData($uuid . "attr/$k/", $uuid, $k, $v); + break; + + default: + $meta = new ODDMetaData($uuid . "attr/$k/", $uuid, $k, $v); + } + + // set the time of any metadata created + if ($meta) { + $meta->setAttribute('published', date("r", $this->time_created)); + $tmp[] = $meta; + } + } + } + + // Now we do something a bit special. + /* + * This provides a rendered view of the entity to foreign sites. + */ + + elgg_set_viewtype('default'); + $view = elgg_view_entity($this, array('full_view' => true)); + elgg_set_viewtype(); + + $tmp[] = new ODDMetaData($uuid . "volatile/renderedentity/", $uuid, + 'renderedentity', $view, 'volatile'); + + return $tmp; + } + + /* + * IMPORTABLE INTERFACE + */ + + /** + * Import data from an parsed ODD xml data array. + * + * @param ODD $data XML data + * + * @return true + * + * @throws InvalidParameterException + */ + public function import(ODD $data) { + if (!($data instanceof ODDEntity)) { + throw new InvalidParameterException(elgg_echo('InvalidParameterException:UnexpectedODDClass')); + } + + // Set type and subtype + $this->attributes['type'] = $data->getAttribute('class'); + $this->attributes['subtype'] = $data->getAttribute('subclass'); + + // Set owner + $this->attributes['owner_guid'] = elgg_get_logged_in_user_guid(); // Import as belonging to importer. + + // Set time + $this->attributes['time_created'] = strtotime($data->getAttribute('published')); + $this->attributes['time_updated'] = time(); + + return true; + } + + /* + * SYSTEM LOG INTERFACE + */ + + /** + * Return an identification for the object for storage in the system log. + * This id must be an integer. + * + * @return int + */ + public function getSystemLogID() { + return $this->getGUID(); + } + + /** + * For a given ID, return the object associated with it. + * This is used by the river functionality primarily. + * + * This is useful for checking access permissions etc on objects. + * + * @param int $id GUID. + * + * @todo How is this any different or more useful than get_entity($guid) + * or new ElggEntity($guid)? + * + * @return int GUID + */ + public function getObjectFromID($id) { + return get_entity($id); + } + + /** + * Returns tags for this entity. + * + * @warning Tags must be registered by {@link elgg_register_tag_metadata_name()}. + * + * @param array $tag_names Optionally restrict by tag metadata names. + * + * @return array + */ + public function getTags($tag_names = NULL) { + if ($tag_names && !is_array($tag_names)) { + $tag_names = array($tag_names); + } + + $valid_tags = elgg_get_registered_tag_metadata_names(); + $entity_tags = array(); + + foreach ($valid_tags as $tag_name) { + if (is_array($tag_names) && !in_array($tag_name, $tag_names)) { + continue; + } + + if ($tags = $this->$tag_name) { + // if a single tag, metadata returns a string. + // if multiple tags, metadata returns an array. + if (is_array($tags)) { + $entity_tags = array_merge($entity_tags, $tags); + } else { + $entity_tags[] = $tags; + } + } + } + + return $entity_tags; + } +} diff --git a/engine/classes/ElggExtender.php b/engine/classes/ElggExtender.php new file mode 100644 index 000000000..25aba354f --- /dev/null +++ b/engine/classes/ElggExtender.php @@ -0,0 +1,214 @@ +<?php +/** + * The base class for ElggEntity extenders. + * + * Extenders allow you to attach extended information to an + * ElggEntity. Core supports two: ElggAnnotation and ElggMetadata. + * + * Saving the extender data to database is handled by the child class. + * + * @tip Plugin authors would probably want to extend either ElggAnnotation + * or ElggMetadata instead of this class. + * + * @package Elgg.Core + * @subpackage DataModel.Extender + * @link http://docs.elgg.org/DataModel/Extenders + * @see ElggAnnotation + * @see ElggMetadata + * + * @property string $type annotation or metadata (read-only after save) + * @property int $id The unique identifier (read-only) + * @property int $entity_guid The GUID of the entity that this extender describes + * @property int $access_id Specifies the visibility level of this extender + * @property string $name The name of this extender + * @property mixed $value The value of the extender (int or string) + * @property int $time_created A UNIX timestamp of when the extender was created (read-only, set on first save) + */ +abstract class ElggExtender extends ElggData { + + /** + * (non-PHPdoc) + * + * @see ElggData::initializeAttributes() + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['type'] = NULL; + } + + /** + * Returns an attribute + * + * @param string $name Name + * + * @return mixed + */ + protected function get($name) { + if (array_key_exists($name, $this->attributes)) { + // Sanitise value if necessary + if ($name == 'value') { + switch ($this->attributes['value_type']) { + case 'integer' : + return (int)$this->attributes['value']; + break; + + //case 'tag' : + //case 'file' : + case 'text' : + return ($this->attributes['value']); + break; + + default : + $msg = elgg_echo('InstallationException:TypeNotSupported', array( + $this->attributes['value_type'])); + + throw new InstallationException($msg); + break; + } + } + + return $this->attributes[$name]; + } + return null; + } + + /** + * Set an attribute + * + * @param string $name Name + * @param mixed $value Value + * @param string $value_type Value type + * + * @return boolean + */ + protected function set($name, $value, $value_type = "") { + $this->attributes[$name] = $value; + if ($name == 'value') { + $this->attributes['value_type'] = detect_extender_valuetype($value, $value_type); + } + + return true; + } + + /** + * Get the GUID of the extender's owner entity. + * + * @return int The owner GUID + */ + public function getOwnerGUID() { + return $this->owner_guid; + } + + /** + * Return the guid of the entity's owner. + * + * @return int The owner GUID + * @deprecated 1.8 Use getOwnerGUID + */ + public function getOwner() { + elgg_deprecated_notice("ElggExtender::getOwner deprecated for ElggExtender::getOwnerGUID", 1.8); + return $this->getOwnerGUID(); + } + + /** + * Get the entity that owns this extender + * + * @return ElggEntity + */ + public function getOwnerEntity() { + return get_entity($this->owner_guid); + } + + /** + * Get the entity this describes. + * + * @return ElggEntity The entity + */ + public function getEntity() { + return get_entity($this->entity_guid); + } + + /** + * Returns if a user can edit this extended data. + * + * @param int $user_guid The GUID of the user (defaults to currently logged in user) + * + * @return bool + */ + public function canEdit($user_guid = 0) { + return can_edit_extender($this->id, $this->type, $user_guid); + } + + /* + * EXPORTABLE INTERFACE + */ + + /** + * Return an array of fields which can be exported. + * + * @return array + */ + public function getExportableValues() { + return array( + 'id', + 'entity_guid', + 'name', + 'value', + 'value_type', + 'owner_guid', + 'type', + ); + } + + /** + * Export this object + * + * @return array + */ + public function export() { + $uuid = get_uuid_from_object($this); + + $meta = new ODDMetaData($uuid, guid_to_uuid($this->entity_guid), $this->attributes['name'], + $this->attributes['value'], $this->attributes['type'], guid_to_uuid($this->owner_guid)); + $meta->setAttribute('published', date("r", $this->time_created)); + + return $meta; + } + + /* + * SYSTEM LOG INTERFACE + */ + + /** + * Return an identification for the object for storage in the system log. + * This id must be an integer. + * + * @return int + */ + public function getSystemLogID() { + return $this->id; + } + + /** + * Return a type of extension. + * + * @return string + */ + public function getType() { + return $this->type; + } + + /** + * Return a subtype. For metadata & annotations this is the 'name' and + * for relationship this is the relationship type. + * + * @return string + */ + public function getSubtype() { + return $this->name; + } + +} diff --git a/engine/classes/ElggFile.php b/engine/classes/ElggFile.php new file mode 100644 index 000000000..23080834b --- /dev/null +++ b/engine/classes/ElggFile.php @@ -0,0 +1,440 @@ +<?php + +/** + * This class represents a physical file. + * + * Create a new ElggFile object and specify a filename, and optionally a + * FileStore (if one isn't specified then the default is assumed.) + * + * Open the file using the appropriate mode, and you will be able to + * read and write to the file. + * + * Optionally, you can also call the file's save() method, this will + * turn the file into an entity in the system and permit you to do + * things like attach tags to the file etc. This is not done automatically + * since there are many occasions where you may want access to file data + * on datastores using the ElggFile interface but do not want to create + * an Entity reference to it in the system (temporary files for example). + * + * @class ElggFile + * @package Elgg.Core + * @subpackage DataModel.File + */ +class ElggFile extends ElggObject { + /** Filestore */ + private $filestore; + + /** File handle used to identify this file in a filestore. Created by open. */ + private $handle; + + /** + * Set subtype to 'file'. + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['subtype'] = "file"; + } + + /** + * Loads an ElggFile entity. + * + * @param int $guid GUID of the ElggFile object + */ + public function __construct($guid = null) { + parent::__construct($guid); + + // Set default filestore + $this->filestore = $this->getFilestore(); + } + + /** + * Set the filename of this file. + * + * @param string $name The filename. + * + * @return void + */ + public function setFilename($name) { + $this->filename = $name; + } + + /** + * Return the filename. + * + * @return string + */ + public function getFilename() { + return $this->filename; + } + + /** + * Return the filename of this file as it is/will be stored on the + * filestore, which may be different to the filename. + * + * @return string + */ + public function getFilenameOnFilestore() { + return $this->filestore->getFilenameOnFilestore($this); + } + + /** + * Return the size of the filestore associated with this file + * + * @param string $prefix Storage prefix + * @param int $container_guid The container GUID of the checked filestore + * + * @return int + */ + public function getFilestoreSize($prefix = '', $container_guid = 0) { + if (!$container_guid) { + $container_guid = $this->container_guid; + } + $fs = $this->getFilestore(); + // @todo add getSize() to ElggFilestore + return $fs->getSize($prefix, $container_guid); + } + + /** + * Get the mime type of the file. + * + * @return string + */ + public function getMimeType() { + if ($this->mimetype) { + return $this->mimetype; + } + + // @todo Guess mimetype if not here + } + + /** + * Set the mime type of the file. + * + * @param string $mimetype The mimetype + * + * @return bool + */ + public function setMimeType($mimetype) { + return $this->mimetype = $mimetype; + } + + /** + * Detects mime types based on filename or actual file. + * + * @param mixed $file The full path of the file to check. For uploaded files, use tmp_name. + * @param mixed $default A default. Useful to pass what the browser thinks it is. + * @since 1.7.12 + * + * @note If $file is provided, this may be called statically + * + * @return mixed Detected type on success, false on failure. + */ + public function detectMimeType($file = null, $default = null) { + if (!$file) { + if (isset($this) && $this->filename) { + $file = $this->filename; + } else { + return false; + } + } + + $mime = false; + + // for PHP5 folks. + if (function_exists('finfo_file') && defined('FILEINFO_MIME_TYPE')) { + $resource = finfo_open(FILEINFO_MIME_TYPE); + if ($resource) { + $mime = finfo_file($resource, $file); + } + } + + // for everyone else. + if (!$mime && function_exists('mime_content_type')) { + $mime = mime_content_type($file); + } + + // default + if (!$mime) { + return $default; + } + + return $mime; + } + + /** + * Set the optional file description. + * + * @param string $description The description. + * + * @return bool + */ + public function setDescription($description) { + $this->description = $description; + } + + /** + * Open the file with the given mode + * + * @param string $mode Either read/write/append + * + * @return resource File handler + * + * @throws IOException|InvalidParameterException + */ + public function open($mode) { + if (!$this->getFilename()) { + throw new IOException(elgg_echo('IOException:MissingFileName')); + } + + // See if file has already been saved + // seek on datastore, parameters and name? + + // Sanity check + if ( + ($mode != "read") && + ($mode != "write") && + ($mode != "append") + ) { + $msg = elgg_echo('InvalidParameterException:UnrecognisedFileMode', array($mode)); + throw new InvalidParameterException($msg); + } + + // Get the filestore + $fs = $this->getFilestore(); + + // Ensure that we save the file details to object store + //$this->save(); + + // Open the file handle + $this->handle = $fs->open($this, $mode); + + return $this->handle; + } + + /** + * Write data. + * + * @param string $data The data + * + * @return bool + */ + public function write($data) { + $fs = $this->getFilestore(); + + return $fs->write($this->handle, $data); + } + + /** + * Read data. + * + * @param int $length Amount to read. + * @param int $offset The offset to start from. + * + * @return mixed Data or false + */ + public function read($length, $offset = 0) { + $fs = $this->getFilestore(); + + return $fs->read($this->handle, $length, $offset); + } + + /** + * Gets the full contents of this file. + * + * @return mixed The file contents. + */ + public function grabFile() { + $fs = $this->getFilestore(); + return $fs->grabFile($this); + } + + /** + * Close the file and commit changes + * + * @return bool + */ + public function close() { + $fs = $this->getFilestore(); + + if ($fs->close($this->handle)) { + $this->handle = NULL; + + return true; + } + + return false; + } + + /** + * Delete this file. + * + * @return bool + */ + public function delete() { + $fs = $this->getFilestore(); + + $result = $fs->delete($this); + + if ($this->getGUID() && $result) { + $result = parent::delete(); + } + + return $result; + } + + /** + * Seek a position in the file. + * + * @param int $position Position in bytes + * + * @return bool + */ + public function seek($position) { + $fs = $this->getFilestore(); + + // @todo add seek() to ElggFilestore + return $fs->seek($this->handle, $position); + } + + /** + * Return the current position of the file. + * + * @return int The file position + */ + public function tell() { + $fs = $this->getFilestore(); + + return $fs->tell($this->handle); + } + + /** + * Return the size of the file in bytes. + * + * @return int + */ + public function size() { + return $this->filestore->getFileSize($this); + } + + /** + * Return a boolean value whether the file handle is at the end of the file + * + * @return bool + */ + public function eof() { + $fs = $this->getFilestore(); + + return $fs->eof($this->handle); + } + + /** + * Returns if the file exists + * + * @return bool + */ + public function exists() { + $fs = $this->getFilestore(); + + return $fs->exists($this); + } + + /** + * Set a filestore. + * + * @param ElggFilestore $filestore The file store. + * + * @return void + */ + public function setFilestore(ElggFilestore $filestore) { + $this->filestore = $filestore; + } + + /** + * Return a filestore suitable for saving this file. + * This filestore is either a pre-registered filestore, + * a filestore as recorded in metadata or the system default. + * + * @return ElggFilestore + * + * @throws ClassNotFoundException + */ + protected function getFilestore() { + // Short circuit if already set. + if ($this->filestore) { + return $this->filestore; + } + + // ask for entity specific filestore + // saved as filestore::className in metadata. + // need to get all filestore::* metadata because the rest are "parameters" that + // get passed to filestore::setParameters() + if ($this->guid) { + $options = array( + 'guid' => $this->guid, + 'where' => array("n.string LIKE 'filestore::%'"), + ); + + $mds = elgg_get_metadata($options); + + $parameters = array(); + foreach ($mds as $md) { + list($foo, $name) = explode("::", $md->name); + if ($name == 'filestore') { + $filestore = $md->value; + } + $parameters[$name] = $md->value; + } + } + + // need to check if filestore is set because this entity is loaded in save() + // before the filestore metadata is saved. + if (isset($filestore)) { + if (!class_exists($filestore)) { + $msg = elgg_echo('ClassNotFoundException:NotFoundNotSavedWithFile', + array($filestore, $this->guid)); + throw new ClassNotFoundException($msg); + } + + $this->filestore = new $filestore(); + $this->filestore->setParameters($parameters); + // @todo explain why $parameters will always be set here (PhpStorm complains) + } + + // this means the entity hasn't been saved so fallback to default + if (!$this->filestore) { + $this->filestore = get_default_filestore(); + } + + return $this->filestore; + } + + /** + * Save the file + * + * Write the file's data to the filestore and save + * the corresponding entity. + * + * @see ElggObject::save() + * + * @return bool + */ + public function save() { + if (!parent::save()) { + return false; + } + + // Save datastore metadata + $params = $this->filestore->getParameters(); + foreach ($params as $k => $v) { + $this->setMetaData("filestore::$k", $v); + } + + // Now make a note of the filestore class + $this->setMetaData("filestore::filestore", get_class($this->filestore)); + + return true; + } +} diff --git a/engine/classes/ElggFileCache.php b/engine/classes/ElggFileCache.php new file mode 100644 index 000000000..94143f777 --- /dev/null +++ b/engine/classes/ElggFileCache.php @@ -0,0 +1,230 @@ +<?php +/** + * ElggFileCache + * Store cached data in a file store. + * + * @package Elgg.Core + * @subpackage Caches + */ +class ElggFileCache extends ElggCache { + /** + * Set the Elgg cache. + * + * @param string $cache_path The cache path. + * @param int $max_age Maximum age in seconds, 0 if no limit. + * @param int $max_size Maximum size of cache in seconds, 0 if no limit. + * + * @throws ConfigurationException + */ + function __construct($cache_path, $max_age = 0, $max_size = 0) { + $this->setVariable("cache_path", $cache_path); + $this->setVariable("max_age", $max_age); + $this->setVariable("max_size", $max_size); + + if ($cache_path == "") { + throw new ConfigurationException(elgg_echo('ConfigurationException:NoCachePath')); + } + } + + // @codingStandardsIgnoreStart + /** + * Create and return a handle to a file. + * + * @deprecated 1.8 Use ElggFileCache::createFile() + * + * @param string $filename Filename to save as + * @param string $rw Write mode + * + * @return mixed + */ + protected function create_file($filename, $rw = "rb") { + elgg_deprecated_notice('ElggFileCache::create_file() is deprecated by ::createFile()', 1.8); + + return $this->createFile($filename, $rw); + } + // @codingStandardsIgnoreEnd + + /** + * Create and return a handle to a file. + * + * @param string $filename Filename to save as + * @param string $rw Write mode + * + * @return mixed + */ + protected function createFile($filename, $rw = "rb") { + // Create a filename matrix + $matrix = ""; + $depth = strlen($filename); + if ($depth > 5) { + $depth = 5; + } + + // Create full path + $path = $this->getVariable("cache_path") . $matrix; + if (!is_dir($path)) { + mkdir($path, 0700, true); + } + + // Open the file + if ((!file_exists($path . $filename)) && ($rw == "rb")) { + return false; + } + + return fopen($path . $filename, $rw); + } + + // @codingStandardsIgnoreStart + /** + * Create a sanitised filename for the file. + * + * @deprecated 1.8 Use ElggFileCache::sanitizeFilename() + * + * @param string $filename The filename + * + * @return string + */ + protected function sanitise_filename($filename) { + // @todo : Writeme + + return $filename; + } + // @codingStandardsIgnoreEnd + + /** + * Create a sanitised filename for the file. + * + * @param string $filename The filename + * + * @return string + */ + protected function sanitizeFilename($filename) { + // @todo : Writeme + + return $filename; + } + + /** + * Save a key + * + * @param string $key Name + * @param string $data Value + * + * @return boolean + */ + public function save($key, $data) { + $f = $this->createFile($this->sanitizeFilename($key), "wb"); + if ($f) { + $result = fwrite($f, $data); + fclose($f); + + return $result; + } + + return false; + } + + /** + * Load a key + * + * @param string $key Name + * @param int $offset Offset + * @param int $limit Limit + * + * @return string + */ + public function load($key, $offset = 0, $limit = null) { + $f = $this->createFile($this->sanitizeFilename($key)); + if ($f) { + if (!$limit) { + $limit = -1; + } + + $data = stream_get_contents($f, $limit, $offset); + + fclose($f); + + return $data; + } + + return false; + } + + /** + * Invalidate a given key. + * + * @param string $key Name + * + * @return bool + */ + public function delete($key) { + $dir = $this->getVariable("cache_path"); + + if (file_exists($dir . $key)) { + return unlink($dir . $key); + } + return TRUE; + } + + /** + * Delete all files in the directory of this file cache + * + * @return void + */ + public function clear() { + $dir = $this->getVariable("cache_path"); + + $exclude = array(".", ".."); + + $files = scandir($dir); + if (!$files) { + return; + } + + foreach ($files as $f) { + if (!in_array($f, $exclude)) { + unlink($dir . $f); + } + } + } + + /** + * Preform cleanup and invalidates cache upon object destruction + * + * @throws IOException + */ + public function __destruct() { + // @todo Check size and age, clean up accordingly + $size = 0; + $dir = $this->getVariable("cache_path"); + + // Short circuit if both size and age are unlimited + if (($this->getVariable("max_age") == 0) && ($this->getVariable("max_size") == 0)) { + return; + } + + $exclude = array(".", ".."); + + $files = scandir($dir); + if (!$files) { + throw new IOException(elgg_echo('IOException:NotDirectory', array($dir))); + } + + // Perform cleanup + foreach ($files as $f) { + if (!in_array($f, $exclude)) { + $stat = stat($dir . $f); + + // Add size + $size .= $stat['size']; + + // Is this older than my maximum date? + if (($this->getVariable("max_age") > 0) && (time() - $stat['mtime'] > $this->getVariable("max_age"))) { + unlink($dir . $f); + } + + // @todo Size + } + } + } +} diff --git a/engine/classes/ElggFilestore.php b/engine/classes/ElggFilestore.php new file mode 100644 index 000000000..16430feac --- /dev/null +++ b/engine/classes/ElggFilestore.php @@ -0,0 +1,139 @@ +<?php +/** + * This class defines the interface for all elgg data repositories. + * + * @package Elgg.Core + * @subpackage DataStorage + * @class ElggFilestore + */ +abstract class ElggFilestore { + /** + * Attempt to open the file $file for storage or writing. + * + * @param ElggFile $file A file + * @param string $mode "read", "write", "append" + * + * @return mixed A handle to the opened file or false on error. + */ + abstract public function open(ElggFile $file, $mode); + + /** + * Write data to a given file handle. + * + * @param mixed $f The file handle - exactly what this is depends on the file system + * @param string $data The binary string of data to write + * + * @return int Number of bytes written. + */ + abstract public function write($f, $data); + + /** + * Read data from a filestore. + * + * @param mixed $f The file handle + * @param int $length Length in bytes to read. + * @param int $offset The optional offset. + * + * @return mixed String of data or false on error. + */ + abstract public function read($f, $length, $offset = 0); + + /** + * Seek a given position within a file handle. + * + * @param mixed $f The file handle. + * @param int $position The position. + * + * @return void + */ + abstract public function seek($f, $position); + + /** + * Return a whether the end of a file has been reached. + * + * @param mixed $f The file handle. + * + * @return boolean + */ + abstract public function eof($f); + + /** + * Return the current position in an open file. + * + * @param mixed $f The file handle. + * + * @return int + */ + abstract public function tell($f); + + /** + * Close a given file handle. + * + * @param mixed $f The file handle + * + * @return bool + */ + abstract public function close($f); + + /** + * Delete the file associated with a given file handle. + * + * @param ElggFile $file The file + * + * @return bool + */ + abstract public function delete(ElggFile $file); + + /** + * Return the size in bytes for a given file. + * + * @param ElggFile $file The file + * + * @return int + */ + abstract public function getFileSize(ElggFile $file); + + /** + * Return the filename of a given file as stored on the filestore. + * + * @param ElggFile $file The file + * + * @return string + */ + abstract public function getFilenameOnFilestore(ElggFile $file); + + /** + * Get the filestore's creation parameters as an associative array. + * Used for serialisation and for storing the creation details along side a file object. + * + * @return array + */ + abstract public function getParameters(); + + /** + * Set the parameters from the associative array produced by $this->getParameters(). + * + * @param array $parameters A list of parameters + * + * @return bool + */ + abstract public function setParameters(array $parameters); + + /** + * Get the contents of the whole file. + * + * @param mixed $file The file handle. + * + * @return mixed The file contents. + */ + abstract public function grabFile(ElggFile $file); + + /** + * Return whether a file physically exists or not. + * + * @param ElggFile $file The file + * + * @return bool + */ + abstract public function exists(ElggFile $file); +} diff --git a/engine/classes/ElggGroup.php b/engine/classes/ElggGroup.php new file mode 100644 index 000000000..7e69b7a84 --- /dev/null +++ b/engine/classes/ElggGroup.php @@ -0,0 +1,393 @@ +<?php + +/** + * Class representing a container for other elgg entities. + * + * @package Elgg.Core + * @subpackage Groups + * + * @property string $name A short name that captures the purpose of the group + * @property string $description A longer body of content that gives more details about the group + */ +class ElggGroup extends ElggEntity + implements Friendable { + + /** + * Sets the type to group. + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['type'] = "group"; + $this->attributes['name'] = NULL; + $this->attributes['description'] = NULL; + $this->attributes['tables_split'] = 2; + } + + /** + * Construct a new group entity, optionally from a given guid value. + * + * @param mixed $guid If an int, load that GUID. + * If an entity table db row, then will load the rest of the data. + * + * @throws IOException|InvalidParameterException if there was a problem creating the group. + */ + function __construct($guid = null) { + $this->initializeAttributes(); + + // compatibility for 1.7 api. + $this->initialise_attributes(false); + + if (!empty($guid)) { + // Is $guid is a entity table DB row + if ($guid instanceof stdClass) { + // Load the rest + if (!$this->load($guid)) { + $msg = elgg_echo('IOException:FailedToLoadGUID', array(get_class(), $guid->guid)); + throw new IOException($msg); + } + } else if ($guid instanceof ElggGroup) { + // $guid is an ElggGroup so this is a copy constructor + elgg_deprecated_notice('This type of usage of the ElggGroup constructor was deprecated. Please use the clone method.', 1.7); + + foreach ($guid->attributes as $key => $value) { + $this->attributes[$key] = $value; + } + } else if ($guid instanceof ElggEntity) { + // @todo why separate from else + throw new InvalidParameterException(elgg_echo('InvalidParameterException:NonElggGroup')); + } else if (is_numeric($guid)) { + // $guid is a GUID so load entity + if (!$this->load($guid)) { + throw new IOException(elgg_echo('IOException:FailedToLoadGUID', array(get_class(), $guid))); + } + } else { + throw new InvalidParameterException(elgg_echo('InvalidParameterException:UnrecognisedValue')); + } + } + } + + /** + * Add an ElggObject to this group. + * + * @param ElggObject $object The object. + * + * @return bool + */ + public function addObjectToGroup(ElggObject $object) { + return add_object_to_group($this->getGUID(), $object->getGUID()); + } + + /** + * Remove an object from the containing group. + * + * @param int $guid The guid of the object. + * + * @return bool + */ + public function removeObjectFromGroup($guid) { + return remove_object_from_group($this->getGUID(), $guid); + } + + /** + * Returns an attribute or metadata. + * + * @see ElggEntity::get() + * + * @param string $name Name + * + * @return mixed + */ + public function get($name) { + if ($name == 'username') { + return 'group:' . $this->getGUID(); + } + return parent::get($name); + } + + /** + * Start friendable compatibility block: + * + * public function addFriend($friend_guid); + public function removeFriend($friend_guid); + public function isFriend(); + public function isFriendsWith($user_guid); + public function isFriendOf($user_guid); + public function getFriends($subtype = "", $limit = 10, $offset = 0); + public function getFriendsOf($subtype = "", $limit = 10, $offset = 0); + public function getObjects($subtype="", $limit = 10, $offset = 0); + public function getFriendsObjects($subtype = "", $limit = 10, $offset = 0); + public function countObjects($subtype = ""); + */ + + /** + * For compatibility with Friendable. + * + * Join a group when you friend ElggGroup. + * + * @param int $friend_guid The GUID of the user joining the group. + * + * @return bool + */ + public function addFriend($friend_guid) { + return $this->join(get_entity($friend_guid)); + } + + /** + * For compatibility with Friendable + * + * Leave group when you unfriend ElggGroup. + * + * @param int $friend_guid The GUID of the user leaving. + * + * @return bool + */ + public function removeFriend($friend_guid) { + return $this->leave(get_entity($friend_guid)); + } + + /** + * For compatibility with Friendable + * + * Friending a group adds you as a member + * + * @return bool + */ + public function isFriend() { + return $this->isMember(); + } + + /** + * For compatibility with Friendable + * + * @param int $user_guid The GUID of a user to check. + * + * @return bool + */ + public function isFriendsWith($user_guid) { + return $this->isMember($user_guid); + } + + /** + * For compatibility with Friendable + * + * @param int $user_guid The GUID of a user to check. + * + * @return bool + */ + public function isFriendOf($user_guid) { + return $this->isMember($user_guid); + } + + /** + * For compatibility with Friendable + * + * @param string $subtype The GUID of a user to check. + * @param int $limit Limit + * @param int $offset Offset + * + * @return bool + */ + public function getFriends($subtype = "", $limit = 10, $offset = 0) { + return get_group_members($this->getGUID(), $limit, $offset); + } + + /** + * For compatibility with Friendable + * + * @param string $subtype The GUID of a user to check. + * @param int $limit Limit + * @param int $offset Offset + * + * @return bool + */ + public function getFriendsOf($subtype = "", $limit = 10, $offset = 0) { + return get_group_members($this->getGUID(), $limit, $offset); + } + + /** + * Get objects contained in this group. + * + * @param string $subtype Entity subtype + * @param int $limit Limit + * @param int $offset Offset + * + * @return array|false + */ + public function getObjects($subtype = "", $limit = 10, $offset = 0) { + // @todo are we deprecating this method, too? + return get_objects_in_group($this->getGUID(), $subtype, 0, 0, "", $limit, $offset, false); + } + + /** + * For compatibility with Friendable + * + * @param string $subtype Entity subtype + * @param int $limit Limit + * @param int $offset Offset + * + * @return array|false + */ + public function getFriendsObjects($subtype = "", $limit = 10, $offset = 0) { + // @todo are we deprecating this method, too? + return get_objects_in_group($this->getGUID(), $subtype, 0, 0, "", $limit, $offset, false); + } + + /** + * For compatibility with Friendable + * + * @param string $subtype Subtype of entities + * + * @return array|false + */ + public function countObjects($subtype = "") { + // @todo are we deprecating this method, too? + return get_objects_in_group($this->getGUID(), $subtype, 0, 0, "", 10, 0, true); + } + + /** + * End friendable compatibility block + */ + + /** + * Get a list of group members. + * + * @param int $limit Limit + * @param int $offset Offset + * @param bool $count Count + * + * @return mixed + */ + public function getMembers($limit = 10, $offset = 0, $count = false) { + return get_group_members($this->getGUID(), $limit, $offset, 0, $count); + } + + /** + * Returns whether the current group is public membership or not. + * + * @return bool + */ + public function isPublicMembership() { + if ($this->membership == ACCESS_PUBLIC) { + return true; + } + + return false; + } + + /** + * Return whether a given user is a member of this group or not. + * + * @param ElggUser $user The user + * + * @return bool + */ + public function isMember($user = null) { + if (!($user instanceof ElggUser)) { + $user = elgg_get_logged_in_user_entity(); + } + if (!($user instanceof ElggUser)) { + return false; + } + return is_group_member($this->getGUID(), $user->getGUID()); + } + + /** + * Join an elgg user to this group. + * + * @param ElggUser $user User + * + * @return bool + */ + public function join(ElggUser $user) { + return join_group($this->getGUID(), $user->getGUID()); + } + + /** + * Remove a user from the group. + * + * @param ElggUser $user User + * + * @return bool + */ + public function leave(ElggUser $user) { + return leave_group($this->getGUID(), $user->getGUID()); + } + + /** + * Load the ElggGroup data from the database + * + * @param mixed $guid GUID of an ElggGroup entity or database row from entity table + * + * @return bool + */ + protected function load($guid) { + $attr_loader = new ElggAttributeLoader(get_class(), 'group', $this->attributes); + $attr_loader->requires_access_control = !($this instanceof ElggPlugin); + $attr_loader->secondary_loader = 'get_group_entity_as_row'; + + $attrs = $attr_loader->getRequiredAttributes($guid); + if (!$attrs) { + return false; + } + + $this->attributes = $attrs; + $this->attributes['tables_loaded'] = 2; + _elgg_cache_entity($this); + + return true; + } + + /** + * Override the save function. + * + * @return bool + */ + public function save() { + // Save generic stuff + if (!parent::save()) { + return false; + } + + // Now save specific stuff + + _elgg_disable_caching_for_entity($this->guid); + $ret = create_group_entity($this->get('guid'), $this->get('name'), $this->get('description')); + _elgg_enable_caching_for_entity($this->guid); + + return $ret; + } + + // EXPORTABLE INTERFACE //////////////////////////////////////////////////////////// + + /** + * Return an array of fields which can be exported. + * + * @return array + */ + public function getExportableValues() { + return array_merge(parent::getExportableValues(), array( + 'name', + 'description', + )); + } + + /** + * Can a user comment on this group? + * + * @see ElggEntity::canComment() + * + * @param int $user_guid User guid (default is logged in user) + * @return bool + * @since 1.8.0 + */ + public function canComment($user_guid = 0) { + $result = parent::canComment($user_guid); + if ($result !== null) { + return $result; + } + return false; + } +} diff --git a/engine/classes/ElggGroupItemVisibility.php b/engine/classes/ElggGroupItemVisibility.php new file mode 100644 index 000000000..2c7e2abb4 --- /dev/null +++ b/engine/classes/ElggGroupItemVisibility.php @@ -0,0 +1,93 @@ +<?php + +/** + * Determines if otherwise visible items should be hidden from a user due to group + * policy or visibility. + * + * @class ElggGroupItemVisibility + * @package Elgg.Core + * @subpackage Groups + * + * @access private + */ +class ElggGroupItemVisibility { + + const REASON_MEMBERSHIP = 'membershiprequired'; + const REASON_LOGGEDOUT = 'loggedinrequired'; + const REASON_NOACCESS = 'noaccess'; + + /** + * @var bool + */ + public $shouldHideItems = false; + + /** + * @var string + */ + public $reasonHidden = ''; + + /** + * Determine visibility of items within a container for the current user + * + * @param int $container_guid GUID of a container (may/may not be a group) + * + * @return ElggGroupItemVisibility + * + * @todo Make this faster, considering it must run for every river item. + */ + static public function factory($container_guid) { + // cache because this may be called repeatedly during river display, and + // due to need to check group visibility, cache will be disabled for some + // get_entity() calls + static $cache = array(); + + $ret = new ElggGroupItemVisibility(); + + if (!$container_guid) { + return $ret; + } + + $user = elgg_get_logged_in_user_entity(); + $user_guid = $user ? $user->guid : 0; + + $container_guid = (int) $container_guid; + + $cache_key = "$container_guid|$user_guid"; + if (empty($cache[$cache_key])) { + // compute + + $container = get_entity($container_guid); + $is_visible = (bool) $container; + + if (!$is_visible) { + // see if it *really* exists... + $prev_access = elgg_set_ignore_access(); + $container = get_entity($container_guid); + elgg_set_ignore_access($prev_access); + } + + if ($container && $container instanceof ElggGroup) { + /* @var ElggGroup $container */ + + if ($is_visible) { + if (!$container->isPublicMembership()) { + if ($user) { + if (!$container->isMember($user) && !$user->isAdmin()) { + $ret->shouldHideItems = true; + $ret->reasonHidden = self::REASON_MEMBERSHIP; + } + } else { + $ret->shouldHideItems = true; + $ret->reasonHidden = self::REASON_LOGGEDOUT; + } + } + } else { + $ret->shouldHideItems = true; + $ret->reasonHidden = self::REASON_NOACCESS; + } + } + $cache[$cache_key] = $ret; + } + return $cache[$cache_key]; + } +} diff --git a/engine/classes/ElggHMACCache.php b/engine/classes/ElggHMACCache.php new file mode 100644 index 000000000..c2f468815 --- /dev/null +++ b/engine/classes/ElggHMACCache.php @@ -0,0 +1,99 @@ +<?php +/** + * ElggHMACCache + * Store cached data in a temporary database, only used by the HMAC stuff. + * + * @package Elgg.Core + * @subpackage HMAC + */ +class ElggHMACCache extends ElggCache { + /** + * Set the Elgg cache. + * + * @param int $max_age Maximum age in seconds, 0 if no limit. + */ + function __construct($max_age = 0) { + $this->setVariable("max_age", $max_age); + } + + /** + * Save a key + * + * @param string $key Name + * @param string $data Value + * + * @return boolean + */ + public function save($key, $data) { + global $CONFIG; + + $key = sanitise_string($key); + $time = time(); + + $query = "INSERT into {$CONFIG->dbprefix}hmac_cache (hmac, ts) VALUES ('$key', '$time')"; + return insert_data($query); + } + + /** + * Load a key + * + * @param string $key Name + * @param int $offset Offset + * @param int $limit Limit + * + * @return string + */ + public function load($key, $offset = 0, $limit = null) { + global $CONFIG; + + $key = sanitise_string($key); + + $row = get_data_row("SELECT * from {$CONFIG->dbprefix}hmac_cache where hmac='$key'"); + if ($row) { + return $row->hmac; + } + + return false; + } + + /** + * Invalidate a given key. + * + * @param string $key Name + * + * @return bool + */ + public function delete($key) { + global $CONFIG; + + $key = sanitise_string($key); + + return delete_data("DELETE from {$CONFIG->dbprefix}hmac_cache where hmac='$key'"); + } + + /** + * Clear out all the contents of the cache. + * + * Not currently implemented in this cache type. + * + * @return true + */ + public function clear() { + return true; + } + + /** + * Clean out old stuff. + * + */ + public function __destruct() { + global $CONFIG; + + $time = time(); + $age = (int)$this->getVariable("max_age"); + + $expires = $time - $age; + + delete_data("DELETE from {$CONFIG->dbprefix}hmac_cache where ts<$expires"); + } +} diff --git a/engine/classes/ElggLRUCache.php b/engine/classes/ElggLRUCache.php new file mode 100644 index 000000000..f51af2ed7 --- /dev/null +++ b/engine/classes/ElggLRUCache.php @@ -0,0 +1,181 @@ +<?php + +/** + * Least Recently Used Cache + * + * A fixed sized cache that removes the element used last when it reaches its + * size limit. + * + * Based on https://github.com/cash/LRUCache + * + * @access private + * + * @package Elgg.Core + * @subpackage Cache + */ +class ElggLRUCache implements ArrayAccess { + /** @var int */ + protected $maximumSize; + + /** + * The front of the array contains the LRU element + * + * @var array + */ + protected $data = array(); + + /** + * Create a LRU Cache + * + * @param int $size The size of the cache + * @throws InvalidArgumentException + */ + public function __construct($size) { + if (!is_int($size) || $size <= 0) { + throw new InvalidArgumentException(); + } + $this->maximumSize = $size; + } + + /** + * Get the value cached with this key + * + * @param int|string $key The key. Strings that are ints are cast to ints. + * @param mixed $default The value to be returned if key not found. (Optional) + * @return mixed + */ + public function get($key, $default = null) { + if (isset($this->data[$key])) { + $this->recordAccess($key); + return $this->data[$key]; + } else { + return $default; + } + } + + /** + * Add something to the cache + * + * @param int|string $key The key. Strings that are ints are cast to ints. + * @param mixed $value The value to cache + * @return void + */ + public function set($key, $value) { + if (isset($this->data[$key])) { + $this->data[$key] = $value; + $this->recordAccess($key); + } else { + $this->data[$key] = $value; + if ($this->size() > $this->maximumSize) { + // remove least recently used element (front of array) + reset($this->data); + unset($this->data[key($this->data)]); + } + } + } + + /** + * Get the number of elements in the cache + * + * @return int + */ + public function size() { + return count($this->data); + } + + /** + * Does the cache contain an element with this key + * + * @param int|string $key The key + * @return boolean + */ + public function containsKey($key) { + return isset($this->data[$key]); + } + + /** + * Remove the element with this key. + * + * @param int|string $key The key + * @return mixed Value or null if not set + */ + public function remove($key) { + if (isset($this->data[$key])) { + $value = $this->data[$key]; + unset($this->data[$key]); + return $value; + } else { + return null; + } + } + + /** + * Clear the cache + * + * @return void + */ + public function clear() { + $this->data = array(); + } + + /** + * Moves the element from current position to end of array + * + * @param int|string $key The key + * @return void + */ + protected function recordAccess($key) { + $value = $this->data[$key]; + unset($this->data[$key]); + $this->data[$key] = $value; + } + + /** + * Assigns a value for the specified key + * + * @see ArrayAccess::offsetSet() + * + * @param int|string $key The key to assign the value to. + * @param mixed $value The value to set. + * @return void + */ + public function offsetSet($key, $value) { + $this->set($key, $value); + } + + /** + * Get the value for specified key + * + * @see ArrayAccess::offsetGet() + * + * @param int|string $key The key to retrieve. + * @return mixed + */ + public function offsetGet($key) { + return $this->get($key); + } + + /** + * Unsets a key. + * + * @see ArrayAccess::offsetUnset() + * + * @param int|string $key The key to unset. + * @return void + */ + public function offsetUnset($key) { + $this->remove($key); + } + + /** + * Does key exist? + * + * @see ArrayAccess::offsetExists() + * + * @param int|string $key A key to check for. + * @return boolean + */ + public function offsetExists($key) { + return $this->containsKey($key); + } +} diff --git a/engine/classes/ElggMemcache.php b/engine/classes/ElggMemcache.php new file mode 100644 index 000000000..91d50ab89 --- /dev/null +++ b/engine/classes/ElggMemcache.php @@ -0,0 +1,203 @@ +<?php +/** + * Memcache wrapper class. + * + * @package Elgg.Core + * @subpackage Memcache + */ +class ElggMemcache extends ElggSharedMemoryCache { + /** + * Minimum version of memcached needed to run + * + */ + private static $MINSERVERVERSION = '1.1.12'; + + /** + * Memcache object + */ + private $memcache; + + /** + * Expiry of saved items (default timeout after a day to prevent anything getting too stale) + */ + private $expires = 86400; + + /** + * The version of memcache running + */ + private $version = 0; + + /** + * Connect to memcache. + * + * @param string $namespace The namespace for this cache to write to - + * note, namespaces of the same name are shared! + * + * @throws ConfigurationException + */ + function __construct($namespace = 'default') { + global $CONFIG; + + $this->setNamespace($namespace); + + // Do we have memcache? + if (!class_exists('Memcache')) { + throw new ConfigurationException('PHP memcache module not installed, you must install php5-memcache'); + } + + // Create memcache object + $this->memcache = new Memcache; + + // Now add servers + if (!$CONFIG->memcache_servers) { + throw new ConfigurationException('No memcache servers defined, please populate the $CONFIG->memcache_servers variable'); + } + + if (is_callable(array($this->memcache, 'addServer'))) { + foreach ($CONFIG->memcache_servers as $server) { + if (is_array($server)) { + $this->memcache->addServer( + $server[0], + isset($server[1]) ? $server[1] : 11211, + isset($server[2]) ? $server[2] : FALSE, + isset($server[3]) ? $server[3] : 1, + isset($server[4]) ? $server[4] : 1, + isset($server[5]) ? $server[5] : 15, + isset($server[6]) ? $server[6] : TRUE + ); + + } else { + $this->memcache->addServer($server, 11211); + } + } + } else { + // don't use elgg_echo() here because most of the config hasn't been loaded yet + // and it caches the language, which is hard coded in $CONFIG->language as en. + // overriding it with real values later has no effect because it's already cached. + elgg_log("This version of the PHP memcache API doesn't support multiple servers.", 'ERROR'); + + $server = $CONFIG->memcache_servers[0]; + if (is_array($server)) { + $this->memcache->connect($server[0], $server[1]); + } else { + $this->memcache->addServer($server, 11211); + } + } + + // Get version + $this->version = $this->memcache->getVersion(); + if (version_compare($this->version, ElggMemcache::$MINSERVERVERSION, '<')) { + $msg = vsprintf('Memcache needs at least version %s to run, you are running %s', + array(ElggMemcache::$MINSERVERVERSION, + $this->version + )); + + throw new ConfigurationException($msg); + } + + // Set some defaults + if (isset($CONFIG->memcache_expires)) { + $this->expires = $CONFIG->memcache_expires; + } + } + + /** + * Set the default expiry. + * + * @param int $expires The lifetime as a unix timestamp or time from now. Defaults forever. + * + * @return void + */ + public function setDefaultExpiry($expires = 0) { + $this->expires = $expires; + } + + /** + * Combine a key with the namespace. + * Memcache can only accept <250 char key. If the given key is too long it is shortened. + * + * @param string $key The key + * + * @return string The new key. + */ + private function makeMemcacheKey($key) { + $prefix = $this->getNamespace() . ":"; + + if (strlen($prefix . $key) > 250) { + $key = md5($key); + } + + return $prefix . $key; + } + + /** + * Saves a name and value to the cache + * + * @param string $key Name + * @param string $data Value + * @param integer $expires Expires (in seconds) + * + * @return bool + */ + public function save($key, $data, $expires = null) { + $key = $this->makeMemcacheKey($key); + + if ($expires === null) { + $expires = $this->expires; + } + + $result = $this->memcache->set($key, $data, null, $expires); + if ($result === false) { + elgg_log("MEMCACHE: FAILED TO SAVE $key", 'ERROR'); + } + + return $result; + } + + /** + * Retrieves data. + * + * @param string $key Name of data to retrieve + * @param int $offset Offset + * @param int $limit Limit + * + * @return mixed + */ + public function load($key, $offset = 0, $limit = null) { + $key = $this->makeMemcacheKey($key); + + $result = $this->memcache->get($key); + if ($result === false) { + elgg_log("MEMCACHE: FAILED TO LOAD $key", 'ERROR'); + } + + return $result; + } + + /** + * Delete data + * + * @param string $key Name of data + * + * @return bool + */ + public function delete($key) { + $key = $this->makeMemcacheKey($key); + + return $this->memcache->delete($key, 0); + } + + /** + * Clears the entire cache? + * + * @todo write or remove. + * + * @return true + */ + public function clear() { + // DISABLE clearing for now - you must use delete on a specific key. + return true; + + // @todo Namespaces as in #532 + } +} diff --git a/engine/classes/ElggMenuBuilder.php b/engine/classes/ElggMenuBuilder.php new file mode 100644 index 000000000..b463143d8 --- /dev/null +++ b/engine/classes/ElggMenuBuilder.php @@ -0,0 +1,291 @@ +<?php +/** + * Elgg Menu Builder + * + * @package Elgg.Core + * @subpackage Navigation + * @since 1.8.0 + */ +class ElggMenuBuilder { + + /** + * @var ElggMenuItem[] + */ + protected $menu = array(); + + protected $selected = null; + + /** + * ElggMenuBuilder constructor + * + * @param ElggMenuItem[] $menu Array of ElggMenuItem objects + */ + public function __construct(array $menu) { + $this->menu = $menu; + } + + /** + * Get a prepared menu array + * + * @param mixed $sort_by Method to sort the menu by. @see ElggMenuBuilder::sort() + * @return array + */ + public function getMenu($sort_by = 'text') { + + $this->selectFromContext(); + + $this->selected = $this->findSelected(); + + $this->setupSections(); + + $this->setupTrees(); + + $this->sort($sort_by); + + return $this->menu; + } + + /** + * Get the selected menu item + * + * @return ElggMenuItem + */ + public function getSelected() { + return $this->selected; + } + + /** + * Select menu items for the current context + * + * @return void + */ + protected function selectFromContext() { + if (!isset($this->menu)) { + $this->menu = array(); + return; + } + + // get menu items for this context + $selected_menu = array(); + foreach ($this->menu as $menu_item) { + if (!is_object($menu_item)) { + elgg_log("A non-object was passed to ElggMenuBuilder", "ERROR"); + continue; + } + if ($menu_item->inContext()) { + $selected_menu[] = $menu_item; + } + } + + $this->menu = $selected_menu; + } + + /** + * Group the menu items into sections + * + * @return void + */ + protected function setupSections() { + $sectioned_menu = array(); + foreach ($this->menu as $menu_item) { + if (!isset($sectioned_menu[$menu_item->getSection()])) { + $sectioned_menu[$menu_item->getSection()] = array(); + } + $sectioned_menu[$menu_item->getSection()][] = $menu_item; + } + $this->menu = $sectioned_menu; + } + + /** + * Create trees for each menu section + * + * @internal The tree is doubly linked (parent and children links) + * @return void + */ + protected function setupTrees() { + $menu_tree = array(); + + foreach ($this->menu as $key => $section) { + $parents = array(); + $children = array(); + // divide base nodes from children + foreach ($section as $menu_item) { + /* @var ElggMenuItem $menu_item */ + $parent_name = $menu_item->getParentName(); + if (!$parent_name) { + $parents[$menu_item->getName()] = $menu_item; + } else { + $children[] = $menu_item; + } + } + + // attach children to parents + $iteration = 0; + $current_gen = $parents; + $next_gen = null; + while (count($children) && $iteration < 5) { + foreach ($children as $index => $menu_item) { + $parent_name = $menu_item->getParentName(); + if (array_key_exists($parent_name, $current_gen)) { + $next_gen[$menu_item->getName()] = $menu_item; + if (!in_array($menu_item, $current_gen[$parent_name]->getData('children'))) { + $current_gen[$parent_name]->addChild($menu_item); + $menu_item->setParent($current_gen[$parent_name]); + } + unset($children[$index]); + } + } + $current_gen = $next_gen; + $iteration += 1; + } + + // convert keys to indexes for first level of tree + $parents = array_values($parents); + + $menu_tree[$key] = $parents; + } + + $this->menu = $menu_tree; + } + + /** + * Find the menu item that is currently selected + * + * @return ElggMenuItem + */ + protected function findSelected() { + + // do we have a selected menu item already + foreach ($this->menu as $menu_item) { + if ($menu_item->getSelected()) { + return $menu_item; + } + } + + // scan looking for a selected item + foreach ($this->menu as $menu_item) { + if ($menu_item->getHref()) { + if (elgg_http_url_is_identical(current_page_url(), $menu_item->getHref())) { + $menu_item->setSelected(true); + return $menu_item; + } + } + } + + return null; + } + + /** + * Sort the menu sections and trees + * + * @param mixed $sort_by Sort type as string or php callback + * @return void + */ + protected function sort($sort_by) { + + // sort sections + ksort($this->menu); + + switch ($sort_by) { + case 'text': + $sort_callback = array('ElggMenuBuilder', 'compareByText'); + break; + case 'name': + $sort_callback = array('ElggMenuBuilder', 'compareByName'); + break; + case 'priority': + $sort_callback = array('ElggMenuBuilder', 'compareByWeight'); + break; + case 'register': + // use registration order - usort breaks this + return; + break; + default: + if (is_callable($sort_by)) { + $sort_callback = $sort_by; + } else { + return; + } + break; + } + + // sort each section + foreach ($this->menu as $index => $section) { + foreach ($section as $key => $node) { + $section[$key]->setData('original_order', $key); + } + usort($section, $sort_callback); + $this->menu[$index] = $section; + + // depth first traversal of tree + foreach ($section as $root) { + $stack = array(); + array_push($stack, $root); + while (!empty($stack)) { + $node = array_pop($stack); + /* @var ElggMenuItem $node */ + $node->sortChildren($sort_callback); + $children = $node->getChildren(); + if ($children) { + $stack = array_merge($stack, $children); + } + } + } + } + } + + /** + * Compare two menu items by their display text + * + * @param ElggMenuItem $a Menu item + * @param ElggMenuItem $b Menu item + * @return bool + */ + public static function compareByText($a, $b) { + $at = $a->getText(); + $bt = $b->getText(); + + $result = strnatcmp($at, $bt); + if ($result === 0) { + return $a->getData('original_order') - $b->getData('original_order'); + } + return $result; + } + + /** + * Compare two menu items by their identifiers + * + * @param ElggMenuItem $a Menu item + * @param ElggMenuItem $b Menu item + * @return bool + */ + public static function compareByName($a, $b) { + $an = $a->getName(); + $bn = $b->getName(); + + $result = strcmp($an, $bn); + if ($result === 0) { + return $a->getData('original_order') - $b->getData('original_order'); + } + return $result; + } + + /** + * Compare two menu items by their priority + * + * @param ElggMenuItem $a Menu item + * @param ElggMenuItem $b Menu item + * @return bool + * + * @todo change name to compareByPriority + */ + public static function compareByWeight($a, $b) { + $aw = $a->getWeight(); + $bw = $b->getWeight(); + + if ($aw == $bw) { + return $a->getData('original_order') - $b->getData('original_order'); + } + return $aw - $bw; + } +} diff --git a/engine/classes/ElggMenuItem.php b/engine/classes/ElggMenuItem.php new file mode 100644 index 000000000..81ce6c099 --- /dev/null +++ b/engine/classes/ElggMenuItem.php @@ -0,0 +1,590 @@ +<?php +/** + * Elgg Menu Item + * + * To create a menu item that is not a link, pass false for $href. + * + * @package Elgg.Core + * @subpackage Navigation + * @since 1.8.0 + */ +class ElggMenuItem { + + /** + * @var array Non-rendered data about the menu item + */ + protected $data = array( + // string Identifier of the menu + 'name' => '', + + // array Page contexts this menu item should appear on + 'contexts' => array('all'), + + // string Menu section identifier + 'section' => 'default', + + // int Smaller priorities float to the top + 'priority' => 100, + + // bool Is this the currently selected menu item + 'selected' => false, + + // string Identifier of this item's parent + 'parent_name' => '', + + // ElggMenuItem The parent object or null + 'parent' => null, + + // array Array of children objects or empty array + 'children' => array(), + + // array Classes to apply to the li tag + 'itemClass' => array(), + + // array Classes to apply to the anchor tag + 'linkClass' => array(), + ); + + /** + * @var string The menu display string + */ + protected $text; + + /** + * @var string The menu url + */ + protected $href = null; + + /** + * @var string Tooltip + */ + protected $title = false; + + /** + * @var string The string to display if link is clicked + */ + protected $confirm = ''; + + + /** + * ElggMenuItem constructor + * + * @param string $name Identifier of the menu item + * @param string $text Display text of the menu item + * @param string $href URL of the menu item (false if not a link) + */ + public function __construct($name, $text, $href) { + //$this->name = $name; + $this->text = $text; + if ($href) { + $this->href = elgg_normalize_url($href); + } else { + $this->href = $href; + } + + $this->data['name'] = $name; + } + + /** + * ElggMenuItem factory method + * + * This static method creates an ElggMenuItem from an associative array. + * Required keys are name, text, and href. + * + * @param array $options Option array of key value pairs + * + * @return ElggMenuItem or NULL on error + */ + public static function factory($options) { + if (!isset($options['name']) || !isset($options['text'])) { + return NULL; + } + if (!isset($options['href'])) { + $options['href'] = ''; + } + + $item = new ElggMenuItem($options['name'], $options['text'], $options['href']); + unset($options['name']); + unset($options['text']); + unset($options['href']); + + // special catch in case someone uses context rather than contexts + if (isset($options['context'])) { + $options['contexts'] = $options['context']; + unset($options['context']); + } + + // make sure contexts is set correctly + if (isset($options['contexts'])) { + $item->setContext($options['contexts']); + unset($options['contexts']); + } + + if (isset($options['link_class'])) { + $item->setLinkClass($options['link_class']); + unset($options['link_class']); + } + + if (isset($options['item_class'])) { + $item->setItemClass($options['item_class']); + unset($options['item_class']); + } + + if (isset($options['data']) && is_array($options['data'])) { + $item->setData($options['data']); + unset($options['data']); + } + + foreach ($options as $key => $value) { + if (isset($item->data[$key])) { + $item->data[$key] = $value; + } else { + $item->$key = $value; + } + } + + return $item; + } + + /** + * Set a data key/value pair or a set of key/value pairs + * + * This method allows storage of arbitrary data with this menu item. The + * data can be used for sorting, custom rendering, or any other use. + * + * @param mixed $key String key or an associative array of key/value pairs + * @param mixed $value The value if $key is a string + * @return void + */ + public function setData($key, $value = null) { + if (is_array($key)) { + $this->data += $key; + } else { + $this->data[$key] = $value; + } + } + + /** + * Get stored data + * + * @param string $key The key for the requested key/value pair + * @return mixed + */ + public function getData($key) { + if (isset($this->data[$key])) { + return $this->data[$key]; + } else { + return null; + } + } + + /** + * Set the identifier of the menu item + * + * @param string $name Unique identifier + * @return void + */ + public function setName($name) { + $this->data['name'] = $name; + } + + /** + * Get the identifier of the menu item + * + * @return string + */ + public function getName() { + return $this->data['name']; + } + + /** + * Set the display text of the menu item + * + * @param string $text The display text + * @return void + */ + public function setText($text) { + $this->text = $text; + } + + /** + * Get the display text of the menu item + * + * @return string + */ + public function getText() { + return $this->text; + } + + /** + * Set the URL of the menu item + * + * @param string $href URL or false if not a link + * @return void + */ + public function setHref($href) { + $this->href = $href; + } + + /** + * Get the URL of the menu item + * + * @return string + */ + public function getHref() { + return $this->href; + } + + /** + * Set the contexts that this menu item is available for + * + * @param array $contexts An array of context strings + * @return void + */ + public function setContext($contexts) { + if (is_string($contexts)) { + $contexts = array($contexts); + } + $this->data['contexts'] = $contexts; + } + + /** + * Get an array of context strings + * + * @return array + */ + public function getContext() { + return $this->data['contexts']; + } + + /** + * Should this menu item be used given the current context + * + * @param string $context A context string (default is empty string for + * current context stack). + * @return bool + */ + public function inContext($context = '') { + if ($context) { + return in_array($context, $this->data['contexts']); + } + + if (in_array('all', $this->data['contexts'])) { + return true; + } + + foreach ($this->data['contexts'] as $context) { + if (elgg_in_context($context)) { + return true; + } + } + return false; + } + + /** + * Set the selected flag + * + * @param bool $state Selected state (default is true) + * @return void + */ + public function setSelected($state = true) { + $this->data['selected'] = $state; + } + + /** + * Get selected state + * + * @return bool + */ + public function getSelected() { + return $this->data['selected']; + } + + /** + * Set the tool tip text + * + * @param string $text The text of the tool tip + * @return void + */ + public function setTooltip($text) { + $this->title = $text; + } + + /** + * Get the tool tip text + * + * @return string + */ + public function getTooltip() { + return $this->title; + } + + /** + * Set the confirm text shown when link is clicked + * + * @param string $text The text to show + * @return void + */ + public function setConfirmText($text) { + $this->confirm = $text; + } + + /** + * Get the confirm text + * + * @return string + */ + public function getConfirmText() { + return $this->confirm; + } + + /** + * Set the anchor class + * + * @param mixed $class An array of class names, or a single string class name. + * @return void + */ + public function setLinkClass($class) { + if (!is_array($class)) { + $this->data['linkClass'] = array($class); + } else { + $this->data['linkClass'] = $class; + } + } + + /** + * Get the anchor classes as text + * + * @return string + */ + public function getLinkClass() { + return implode(' ', $this->data['linkClass']); + } + + /** + * Add a link class + * + * @param mixed $class An array of class names, or a single string class name. + * @return void + */ + public function addLinkClass($class) { + if (!is_array($class)) { + $this->data['linkClass'][] = $class; + } else { + $this->data['linkClass'] += $class; + } + } + + /** + * Set the li classes + * + * @param mixed $class An array of class names, or a single string class name. + * @return void + */ + public function setItemClass($class) { + if (!is_array($class)) { + $this->data['itemClass'] = array($class); + } else { + $this->data['itemClass'] = $class; + } + } + + /** + * Get the li classes as text + * + * @return string + */ + public function getItemClass() { + // allow people to specify name with underscores and colons + $name = strtolower($this->getName()); + $name = str_replace('_', '-', $name); + $name = str_replace(':', '-', $name); + $name = str_replace(' ', '-', $name); + + $class = implode(' ', $this->data['itemClass']); + if ($class) { + return "elgg-menu-item-$name $class"; + } else { + return "elgg-menu-item-$name"; + } + } + + /** + * Set the priority of the menu item + * + * @param int $priority The smaller numbers mean higher priority (1 before 100) + * @return void + * @deprecated + */ + public function setWeight($priority) { + $this->data['priority'] = $priority; + } + + /** + * Get the priority of the menu item + * + * @return int + * @deprecated + */ + public function getWeight() { + return $this->data['priority']; + } + + /** + * Set the priority of the menu item + * + * @param int $priority The smaller numbers mean higher priority (1 before 100) + * @return void + */ + public function setPriority($priority) { + $this->data['priority'] = $priority; + } + + /** + * Get the priority of the menu item + * + * @return int + */ + public function getPriority() { + return $this->data['priority']; + } + + /** + * Set the section identifier + * + * @param string $section The identifier of the section + * @return void + */ + public function setSection($section) { + $this->data['section'] = $section; + } + + /** + * Get the section identifier + * + * @return string + */ + public function getSection() { + return $this->data['section']; + } + + /** + * Set the parent identifier + * + * @param string $name The identifier of the parent ElggMenuItem + * @return void + */ + public function setParentName($name) { + $this->data['parent_name'] = $name; + } + + /** + * Get the parent identifier + * + * @return string + */ + public function getParentName() { + return $this->data['parent_name']; + } + + /** + * Set the parent menu item + * + * @param ElggMenuItem $parent The parent of this menu item + * @return void + */ + public function setParent($parent) { + $this->data['parent'] = $parent; + } + + /** + * Get the parent menu item + * + * @return ElggMenuItem or null + */ + public function getParent() { + return $this->data['parent']; + } + + /** + * Add a child menu item + * + * @param ElggMenuItem $item A child menu item + * @return void + */ + public function addChild($item) { + $this->data['children'][] = $item; + } + + /** + * Set the menu item's children + * + * @param array $children Array of ElggMenuItems + * @return void + */ + public function setChildren($children) { + $this->data['children'] = $children; + } + + /** + * Get the children menu items + * + * @return array + */ + public function getChildren() { + return $this->data['children']; + } + + /** + * Sort the children + * + * @param string $sortFunction A function that is passed to usort() + * @return void + */ + public function sortChildren($sortFunction) { + foreach ($this->data['children'] as $key => $node) { + $this->data['children'][$key]->data['original_order'] = $key; + } + usort($this->data['children'], $sortFunction); + } + + /** + * Get the menu item content (usually a link) + * + * @param array $vars Options to pass to output/url if a link + * @return string + * @todo View code in a model. How do we feel about that? + */ + public function getContent(array $vars = array()) { + + if ($this->href === false) { + return $this->text; + } + + $defaults = get_object_vars($this); + unset($defaults['data']); + + $vars += $defaults; + + if ($this->data['linkClass']) { + if (isset($vars['class'])) { + $vars['class'] = $vars['class'] . ' ' . $this->getLinkClass(); + } else { + $vars['class'] = $this->getLinkClass(); + } + } + + if (!isset($vars['rel']) && !isset($vars['is_trusted'])) { + $vars['is_trusted'] = true; + } + + if ($this->confirm) { + $vars['confirm'] = $this->confirm; + return elgg_view('output/confirmlink', $vars); + } else { + unset($vars['confirm']); + } + + return elgg_view('output/url', $vars); + } +} diff --git a/engine/classes/ElggMetadata.php b/engine/classes/ElggMetadata.php new file mode 100644 index 000000000..3a8e2d817 --- /dev/null +++ b/engine/classes/ElggMetadata.php @@ -0,0 +1,158 @@ +<?php + +/** + * ElggMetadata + * This class describes metadata that can be attached to ElggEntities. + * + * @package Elgg.Core + * @subpackage Metadata + * + * @property string $value_type + * @property int $owner_guid + * @property string $enabled + */ +class ElggMetadata extends ElggExtender { + + /** + * (non-PHPdoc) + * + * @see ElggData::initializeAttributes() + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['type'] = "metadata"; + } + + /** + * Construct a metadata object + * + * @param mixed $id ID of metadata or a database row as stdClass object + */ + function __construct($id = null) { + $this->initializeAttributes(); + + if (!empty($id)) { + // Create from db row + if ($id instanceof stdClass) { + $metadata = $id; + + $objarray = (array) $metadata; + foreach ($objarray as $key => $value) { + $this->attributes[$key] = $value; + } + } else { + // get an ElggMetadata object and copy its attributes + $metadata = elgg_get_metadata_from_id($id); + $this->attributes = $metadata->attributes; + } + } + } + + /** + * Determines whether or not the user can edit this piece of metadata + * + * @param int $user_guid The GUID of the user (defaults to currently logged in user) + * + * @return bool Depending on permissions + */ + function canEdit($user_guid = 0) { + if ($entity = get_entity($this->get('entity_guid'))) { + return $entity->canEditMetadata($this, $user_guid); + } + return false; + } + + /** + * Save metadata object + * + * @return int|bool the metadata object id or true if updated + * + * @throws IOException + */ + function save() { + if ($this->id > 0) { + return update_metadata($this->id, $this->name, $this->value, + $this->value_type, $this->owner_guid, $this->access_id); + } else { + $this->id = create_metadata($this->entity_guid, $this->name, $this->value, + $this->value_type, $this->owner_guid, $this->access_id); + + if (!$this->id) { + throw new IOException(elgg_echo('IOException:UnableToSaveNew', array(get_class()))); + } + return $this->id; + } + } + + /** + * Delete the metadata + * + * @return bool + */ + function delete() { + $success = elgg_delete_metastring_based_object_by_id($this->id, 'metadata'); + if ($success) { + // we mark unknown here because this deletes only one value + // under this name, and there may be others remaining. + elgg_get_metadata_cache()->markUnknown($this->entity_guid, $this->name); + } + return $success; + } + + /** + * Disable the metadata + * + * @return bool + * @since 1.8 + */ + function disable() { + $success = elgg_set_metastring_based_object_enabled_by_id($this->id, 'no', 'metadata'); + if ($success) { + // we mark unknown here because this disables only one value + // under this name, and there may be others remaining. + elgg_get_metadata_cache()->markUnknown($this->entity_guid, $this->name); + } + return $success; + } + + /** + * Enable the metadata + * + * @return bool + * @since 1.8 + */ + function enable() { + $success = elgg_set_metastring_based_object_enabled_by_id($this->id, 'yes', 'metadata'); + if ($success) { + elgg_get_metadata_cache()->markUnknown($this->entity_guid, $this->name); + } + return $success; + } + + /** + * Get a url for this item of metadata. + * + * @return string + */ + public function getURL() { + return get_metadata_url($this->id); + } + + // SYSTEM LOG INTERFACE //////////////////////////////////////////////////////////// + + /** + * For a given ID, return the object associated with it. + * This is used by the river functionality primarily. + * This is useful for checking access permissions etc on objects. + * + * @param int $id Metadata ID + * + * @return ElggMetadata + */ + public function getObjectFromID($id) { + return elgg_get_metadata_from_id($id); + } +} diff --git a/engine/classes/ElggObject.php b/engine/classes/ElggObject.php new file mode 100644 index 000000000..aeaa3ba5c --- /dev/null +++ b/engine/classes/ElggObject.php @@ -0,0 +1,216 @@ +<?php +/** + * Elgg Object + * + * Elgg objects are the most common means of storing information in the database. + * They are a child class of ElggEntity, so receive all the benefits of the Entities, + * but also include a title and description field. + * + * An ElggObject represents a row from the objects_entity table, as well + * as the related row in the entities table as represented by the parent + * ElggEntity object. + * + * @internal Title and description are stored in the objects_entity table. + * + * @package Elgg.Core + * @subpackage DataModel.Object + * + * @property string $title The title, name, or summary of this object + * @property string $description The body, description, or content of the object + * @property array $tags Array of tags that describe the object + */ +class ElggObject extends ElggEntity { + + /** + * Initialise the attributes array to include the type, + * title, and description. + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['type'] = "object"; + $this->attributes['title'] = NULL; + $this->attributes['description'] = NULL; + $this->attributes['tables_split'] = 2; + } + + /** + * Load or create a new ElggObject. + * + * If no arguments are passed, create a new entity. + * + * If an argument is passed, attempt to load a full ElggObject entity. + * Arguments can be: + * - The GUID of an object entity. + * - A DB result object from the entities table with a guid property + * + * @param mixed $guid If an int, load that GUID. If a db row, then will attempt to + * load the rest of the data. + * + * @throws IOException If passed an incorrect guid + * @throws InvalidParameterException If passed an Elgg* Entity that isn't an ElggObject + */ + function __construct($guid = null) { + $this->initializeAttributes(); + + // compatibility for 1.7 api. + $this->initialise_attributes(false); + + if (!empty($guid)) { + // Is $guid is a DB row from the entity table + if ($guid instanceof stdClass) { + // Load the rest + if (!$this->load($guid)) { + $msg = elgg_echo('IOException:FailedToLoadGUID', array(get_class(), $guid->guid)); + throw new IOException($msg); + } + } else if ($guid instanceof ElggObject) { + // $guid is an ElggObject so this is a copy constructor + elgg_deprecated_notice('This type of usage of the ElggObject constructor was deprecated. Please use the clone method.', 1.7); + + foreach ($guid->attributes as $key => $value) { + $this->attributes[$key] = $value; + } + } else if ($guid instanceof ElggEntity) { + // @todo remove - do not need separate exception + throw new InvalidParameterException(elgg_echo('InvalidParameterException:NonElggObject')); + } else if (is_numeric($guid)) { + // $guid is a GUID so load + if (!$this->load($guid)) { + throw new IOException(elgg_echo('IOException:FailedToLoadGUID', array(get_class(), $guid))); + } + } else { + throw new InvalidParameterException(elgg_echo('InvalidParameterException:UnrecognisedValue')); + } + } + } + + /** + * Loads the full ElggObject when given a guid. + * + * @param mixed $guid GUID of an ElggObject or the stdClass object from entities table + * + * @return bool + * @throws InvalidClassException + */ + protected function load($guid) { + $attr_loader = new ElggAttributeLoader(get_class(), 'object', $this->attributes); + $attr_loader->requires_access_control = !($this instanceof ElggPlugin); + $attr_loader->secondary_loader = 'get_object_entity_as_row'; + + $attrs = $attr_loader->getRequiredAttributes($guid); + if (!$attrs) { + return false; + } + + $this->attributes = $attrs; + $this->attributes['tables_loaded'] = 2; + _elgg_cache_entity($this); + + return true; + } + + /** + * Saves object-specific attributes. + * + * @internal Object attributes are saved in the objects_entity table. + * + * @return bool + */ + public function save() { + // Save ElggEntity attributes + if (!parent::save()) { + return false; + } + + // Save ElggObject-specific attributes + + _elgg_disable_caching_for_entity($this->guid); + $ret = create_object_entity($this->get('guid'), $this->get('title'), $this->get('description')); + _elgg_enable_caching_for_entity($this->guid); + + return $ret; + } + + /** + * Return sites that this object is a member of + * + * Site membership is determined by relationships and not site_guid.d + * + * @todo This should be moved to ElggEntity + * @todo Unimplemented + * + * @param string $subtype Optionally, the subtype of result we want to limit to + * @param int $limit The number of results to return + * @param int $offset Any indexing offset + * + * @return array|false + */ + function getSites($subtype = "", $limit = 10, $offset = 0) { + return get_site_objects($this->getGUID(), $subtype, $limit, $offset); + } + + /** + * Add this object to a site + * + * @param int $site_guid The guid of the site to add it to + * + * @return bool + */ + function addToSite($site_guid) { + return add_site_object($this->getGUID(), $site_guid); + } + + /* + * EXPORTABLE INTERFACE + */ + + /** + * Return an array of fields which can be exported. + * + * @return array + */ + public function getExportableValues() { + return array_merge(parent::getExportableValues(), array( + 'title', + 'description', + )); + } + + /** + * Can a user comment on this object? + * + * @see ElggEntity::canComment() + * + * @param int $user_guid User guid (default is logged in user) + * @return bool + * @since 1.8.0 + */ + public function canComment($user_guid = 0) { + $result = parent::canComment($user_guid); + if ($result !== null) { + return $result; + } + + if ($user_guid == 0) { + $user_guid = elgg_get_logged_in_user_guid(); + } + + // must be logged in to comment + if (!$user_guid) { + return false; + } + + // must be member of group + if (elgg_instanceof($this->getContainerEntity(), 'group')) { + if (!$this->getContainerEntity()->canWriteToContainer($user_guid)) { + return false; + } + } + + // no checks on read access since a user cannot see entities outside his access + return true; + } +} diff --git a/engine/classes/ElggPAM.php b/engine/classes/ElggPAM.php new file mode 100644 index 000000000..f07095fc1 --- /dev/null +++ b/engine/classes/ElggPAM.php @@ -0,0 +1,105 @@ +<?php +/** + * ElggPAM Pluggable Authentication Module + * + * @package Elgg.Core + * @subpackage Authentication + */ +class ElggPAM { + /** + * @var string PAM policy type: user, api or plugin-defined policies + */ + protected $policy; + + /** + * @var array Failure mesages + */ + protected $messages; + + /** + * ElggPAM constructor + * + * @param string $policy PAM policy type: user, api, or plugin-defined policies + */ + public function __construct($policy) { + $this->policy = $policy; + $this->messages = array('sufficient' => array(), 'required' => array()); + } + + /** + * Authenticate a set of credentials against a policy + * This function will process all registered PAM handlers or stop when the first + * handler fails. A handler fails by either returning false or throwing an + * exception. The advantage of throwing an exception is that it returns a message + * that can be passed to the user. The processing order of the handlers is + * determined by the order that they were registered. + * + * If $credentials are provided, the PAM handler should authenticate using the + * provided credentials. If not, then credentials should be prompted for or + * otherwise retrieved (eg from the HTTP header or $_SESSION). + * + * @param array $credentials Credentials array dependant on policy type + * @return bool + */ + public function authenticate($credentials = array()) { + global $_PAM_HANDLERS; + + if (!isset($_PAM_HANDLERS[$this->policy]) || + !is_array($_PAM_HANDLERS[$this->policy])) { + return false; + } + + $authenticated = false; + + foreach ($_PAM_HANDLERS[$this->policy] as $k => $v) { + $handler = $v->handler; + if (!is_callable($handler)) { + continue; + } + /* @var callable $handler */ + + $importance = $v->importance; + + try { + // Execute the handler + // @todo don't assume $handler is a global function + $result = call_user_func($handler, $credentials); + if ($result) { + $authenticated = true; + } elseif ($result === false) { + if ($importance == 'required') { + $this->messages['required'][] = "$handler:failed"; + return false; + } else { + $this->messages['sufficient'][] = "$handler:failed"; + } + } + } catch (Exception $e) { + if ($importance == 'required') { + $this->messages['required'][] = $e->getMessage(); + return false; + } else { + $this->messages['sufficient'][] = $e->getMessage(); + } + } + } + + return $authenticated; + } + + /** + * Get a failure message to display to user + * + * @return string + */ + public function getFailureMessage() { + $message = elgg_echo('auth:nopams'); + if (!empty($this->messages['required'])) { + $message = $this->messages['required'][0]; + } elseif (!empty($this->messages['sufficient'])) { + $message = $this->messages['sufficient'][0]; + } + + return elgg_trigger_plugin_hook('fail', 'auth', $this->messages, $message); + } +} diff --git a/engine/classes/ElggPlugin.php b/engine/classes/ElggPlugin.php new file mode 100644 index 000000000..545b9a53c --- /dev/null +++ b/engine/classes/ElggPlugin.php @@ -0,0 +1,1006 @@ +<?php +/** + * Stores site-side plugin settings as private data. + * + * This class is currently a stub, allowing a plugin to + * save settings in an object's private settings for each site. + * + * @package Elgg.Core + * @subpackage Plugins.Settings + */ +class ElggPlugin extends ElggObject { + private $package; + private $manifest; + + private $path; + private $pluginID; + private $errorMsg = ''; + + /** + * Set subtype to 'plugin' + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['subtype'] = "plugin"; + + // plugins must be public. + $this->access_id = ACCESS_PUBLIC; + } + + /** + * Loads the plugin by GUID or path. + * + * @warning Unlike other ElggEntity objects, you cannot null instantiate + * ElggPlugin. You must point it to an actual plugin GUID or location. + * + * @param mixed $plugin The GUID of the ElggPlugin object or the path of the plugin to load. + * + * @throws PluginException + */ + public function __construct($plugin) { + if (!$plugin) { + throw new PluginException(elgg_echo('PluginException:NullInstantiated')); + } + + // ElggEntity can be instantiated with a guid or an object. + // @todo plugins w/id 12345 + if (is_numeric($plugin) || is_object($plugin)) { + parent::__construct($plugin); + $this->path = elgg_get_plugins_path() . $this->getID(); + } else { + $plugin_path = elgg_get_plugins_path(); + + // not a full path, so assume an id + // use the default path + if (strpos($plugin, $plugin_path) !== 0) { + $plugin = $plugin_path . $plugin; + } + + // path checking is done in the package + $plugin = sanitise_filepath($plugin); + $this->path = $plugin; + $path_parts = explode('/', rtrim($plugin, '/')); + $plugin_id = array_pop($path_parts); + $this->pluginID = $plugin_id; + + // check if we're loading an existing plugin + $existing_plugin = elgg_get_plugin_from_id($this->pluginID); + $existing_guid = null; + + if ($existing_plugin) { + $existing_guid = $existing_plugin->guid; + } + + // load the rest of the plugin + parent::__construct($existing_guid); + } + + _elgg_cache_plugin_by_id($this); + } + + /** + * Save the plugin object. Make sure required values exist. + * + * @see ElggObject::save() + * @return bool + */ + public function save() { + // own by the current site so users can be deleted without affecting plugins + $site = get_config('site'); + $this->attributes['site_guid'] = $site->guid; + $this->attributes['owner_guid'] = $site->guid; + $this->attributes['container_guid'] = $site->guid; + $this->attributes['title'] = $this->pluginID; + + if (parent::save()) { + // make sure we have a priority + $priority = $this->getPriority(); + if ($priority === FALSE || $priority === NULL) { + return $this->setPriority('last'); + } + } else { + return false; + } + } + + + // Plugin ID and path + + /** + * Returns the ID (dir name) of this plugin + * + * @return string + */ + public function getID() { + return $this->title; + } + + /** + * Returns the manifest's name if available, otherwise the ID. + * + * @return string + * @since 1.8.1 + */ + public function getFriendlyName() { + $manifest = $this->getManifest(); + if ($manifest) { + return $manifest->getName(); + } + + return $this->getID(); + } + + /** + * Returns the plugin's full path with trailing slash. + * + * @return string + */ + public function getPath() { + return sanitise_filepath($this->path); + } + + /** + * Sets the location of this plugin. + * + * @param string $id The path to the plugin's dir. + * @return bool + */ + public function setID($id) { + return $this->attributes['title'] = $id; + } + + /** + * Returns an array of available markdown files for this plugin + * + * @return array + */ + public function getAvailableTextFiles() { + $filenames = $this->getPackage()->getTextFilenames(); + + $files = array(); + foreach ($filenames as $filename) { + if ($this->canReadFile($filename)) { + $files[$filename] = "$this->path/$filename"; + } + } + + return $files; + } + + // Load Priority + + /** + * Gets the plugin's load priority. + * + * @return int + */ + public function getPriority() { + $name = elgg_namespace_plugin_private_setting('internal', 'priority'); + return $this->$name; + } + + /** + * Sets the priority of the plugin + * + * @param mixed $priority The priority to set. One of +1, -1, first, last, or a number. + * If given a number, this will displace all plugins at that number + * and set their priorities +1 + * @param mixed $site_guid Optional site GUID. + * @return bool + */ + public function setPriority($priority, $site_guid = null) { + if (!$this->guid) { + return false; + } + + $db_prefix = get_config('dbprefix'); + $name = elgg_namespace_plugin_private_setting('internal', 'priority'); + // if no priority assume a priority of 1 + $old_priority = (int) $this->getPriority(); + $old_priority = (!$old_priority) ? 1 : $old_priority; + $max_priority = elgg_get_max_plugin_priority(); + + // can't use switch here because it's not strict and + // php evaluates +1 == 1 + if ($priority === '+1') { + $priority = $old_priority + 1; + } elseif ($priority === '-1') { + $priority = $old_priority - 1; + } elseif ($priority === 'first') { + $priority = 1; + } elseif ($priority === 'last') { + $priority = $max_priority; + } + + // should be a number by now + if ($priority > 0) { + if (!is_numeric($priority)) { + return false; + } + + // there's nothing above the max. + if ($priority > $max_priority) { + $priority = $max_priority; + } + + // there's nothing below 1. + if ($priority < 1) { + $priority = 1; + } + + if ($priority > $old_priority) { + $op = '-'; + $where = "CAST(value as unsigned) BETWEEN $old_priority AND $priority"; + } else { + $op = '+'; + $where = "CAST(value as unsigned) BETWEEN $priority AND $old_priority"; + } + + // displace the ones affected by this change + $q = "UPDATE {$db_prefix}private_settings + SET value = CAST(value as unsigned) $op 1 + WHERE entity_guid != $this->guid + AND name = '$name' + AND $where"; + + if (!update_data($q)) { + return false; + } + + // set this priority + if ($this->set($name, $priority)) { + return true; + } else { + return false; + } + } + + return false; + } + + + // Plugin settings + + /** + * Returns a plugin setting + * + * @param string $name The setting name + * @return mixed + */ + public function getSetting($name) { + return $this->$name; + } + + /** + * Returns an array of all settings saved for this plugin. + * + * @note Unlike user settings, plugin settings are not namespaced. + * + * @return array An array of key/value pairs. + */ + public function getAllSettings() { + if (!$this->guid) { + return false; + } + + $db_prefix = elgg_get_config('dbprefix'); + // need to remove all namespaced private settings. + $us_prefix = elgg_namespace_plugin_private_setting('user_setting', '', $this->getID()); + $is_prefix = elgg_namespace_plugin_private_setting('internal', '', $this->getID()); + + // Get private settings for user + $q = "SELECT * FROM {$db_prefix}private_settings + WHERE entity_guid = $this->guid + AND name NOT LIKE '$us_prefix%' + AND name NOT LIKE '$is_prefix%'"; + + $private_settings = get_data($q); + + $return = array(); + + if ($private_settings) { + foreach ($private_settings as $setting) { + $return[$setting->name] = $setting->value; + } + } + + return $return; + } + + /** + * Set a plugin setting for the plugin + * + * @todo This will only work once the plugin has a GUID. + * + * @param string $name The name to set + * @param string $value The value to set + * + * @return bool + */ + public function setSetting($name, $value) { + if (!$this->guid) { + return false; + } + + return $this->set($name, $value); + } + + /** + * Removes a plugin setting name and value. + * + * @param string $name The setting name to remove + * + * @return bool + */ + public function unsetSetting($name) { + return remove_private_setting($this->guid, $name); + } + + /** + * Removes all settings for this plugin. + * + * @todo Should be a better way to do this without dropping to raw SQL. + * @todo If we could namespace the plugin settings this would be cleaner. + * @return bool + */ + public function unsetAllSettings() { + $db_prefix = get_config('dbprefix'); + + $us_prefix = elgg_namespace_plugin_private_setting('user_setting', '', $this->getID()); + $is_prefix = elgg_namespace_plugin_private_setting('internal', '', $this->getID()); + + $q = "DELETE FROM {$db_prefix}private_settings + WHERE entity_guid = $this->guid + AND name NOT LIKE '$us_prefix%' + AND name NOT LIKE '$is_prefix%'"; + + return delete_data($q); + } + + + // User settings + + /** + * Returns a user's setting for this plugin + * + * @param string $name The setting name + * @param int $user_guid The user GUID + * + * @return mixed The setting string value or false + */ + public function getUserSetting($name, $user_guid = null) { + $user_guid = (int)$user_guid; + + if ($user_guid) { + $user = get_entity($user_guid); + } else { + $user = elgg_get_logged_in_user_entity(); + } + + if (!($user instanceof ElggUser)) { + return false; + } + + $name = elgg_namespace_plugin_private_setting('user_setting', $name, $this->getID()); + return get_private_setting($user->guid, $name); + } + + /** + * Returns an array of all user settings saved for this plugin for the user. + * + * @note Plugin settings are saved with a prefix. This removes that prefix. + * + * @param int $user_guid The user GUID. Defaults to logged in. + * @return array An array of key/value pairs. + */ + public function getAllUserSettings($user_guid = null) { + $user_guid = (int)$user_guid; + + if ($user_guid) { + $user = get_entity($user_guid); + } else { + $user = elgg_get_logged_in_user_entity(); + } + + if (!($user instanceof ElggUser)) { + return false; + } + + $db_prefix = elgg_get_config('dbprefix'); + // send an empty name so we just get the first part of the namespace + $ps_prefix = elgg_namespace_plugin_private_setting('user_setting', '', $this->getID()); + $ps_prefix_len = strlen($ps_prefix); + + // Get private settings for user + $q = "SELECT * FROM {$db_prefix}private_settings + WHERE entity_guid = {$user->guid} + AND name LIKE '$ps_prefix%'"; + + $private_settings = get_data($q); + + $return = array(); + + if ($private_settings) { + foreach ($private_settings as $setting) { + $name = substr($setting->name, $ps_prefix_len); + $value = $setting->value; + + $return[$name] = $value; + } + } + + return $return; + } + + /** + * Sets a user setting for a plugin + * + * @param string $name The setting name + * @param string $value The setting value + * @param int $user_guid The user GUID + * + * @return mixed The new setting ID or false + */ + public function setUserSetting($name, $value, $user_guid = null) { + $user_guid = (int)$user_guid; + + if ($user_guid) { + $user = get_entity($user_guid); + } else { + $user = elgg_get_logged_in_user_entity(); + } + + if (!($user instanceof ElggUser)) { + return false; + } + + // Hook to validate setting + // note: this doesn't pass the namespaced name + $value = elgg_trigger_plugin_hook('usersetting', 'plugin', array( + 'user' => $user, + 'plugin' => $this, + 'plugin_id' => $this->getID(), + 'name' => $name, + 'value' => $value + ), $value); + + // set the namespaced name. + $name = elgg_namespace_plugin_private_setting('user_setting', $name, $this->getID()); + + return set_private_setting($user->guid, $name, $value); + } + + + /** + * Removes a user setting name and value. + * + * @param string $name The user setting name + * @param int $user_guid The user GUID + * @return bool + */ + public function unsetUserSetting($name, $user_guid = null) { + $user_guid = (int)$user_guid; + + if ($user_guid) { + $user = get_entity($user_guid); + } else { + $user = elgg_get_logged_in_user_entity(); + } + + if (!($user instanceof ElggUser)) { + return false; + } + + // set the namespaced name. + $name = elgg_namespace_plugin_private_setting('user_setting', $name, $this->getID()); + + return remove_private_setting($user->guid, $name); + } + + /** + * Removes all User Settings for this plugin + * + * Use {@link removeAllUsersSettings()} to remove all user + * settings for all users. (Note the plural 'Users'.) + * + * @param int $user_guid The user GUID to remove user settings. + * @return bool + */ + public function unsetAllUserSettings($user_guid) { + $db_prefix = get_config('dbprefix'); + $ps_prefix = elgg_namespace_plugin_private_setting('user_setting', '', $this->getID()); + + $q = "DELETE FROM {$db_prefix}private_settings + WHERE entity_guid = $user_guid + AND name LIKE '$ps_prefix%'"; + + return delete_data($q); + } + + /** + * Removes this plugin's user settings for all users. + * + * Use {@link removeAllUserSettings()} if you just want to remove + * settings for a single user. + * + * @return bool + */ + public function unsetAllUsersSettings() { + $db_prefix = get_config('dbprefix'); + $ps_prefix = elgg_namespace_plugin_private_setting('user_setting', '', $this->getID()); + + $q = "DELETE FROM {$db_prefix}private_settings + WHERE name LIKE '$ps_prefix%'"; + + return delete_data($q); + } + + + // validation + + /** + * Returns if the plugin is complete, meaning has all required files + * and Elgg can read them and they make sense. + * + * @todo bad name? This could be confused with isValid() from ElggPluginPackage. + * + * @return bool + */ + public function isValid() { + if (!$this->getID()) { + $this->errorMsg = elgg_echo('ElggPlugin:NoId', array($this->guid)); + return false; + } + + if (!$this->getPackage() instanceof ElggPluginPackage) { + $this->errorMsg = elgg_echo('ElggPlugin:NoPluginPackagePackage', array($this->getID(), $this->guid)); + return false; + } + + if (!$this->getPackage()->isValid()) { + $this->errorMsg = $this->getPackage()->getError(); + return false; + } + + return true; + } + + /** + * Is this plugin active? + * + * @param int $site_guid Optional site guid. + * @return bool + */ + public function isActive($site_guid = null) { + if (!$this->guid) { + return false; + } + + if ($site_guid) { + $site = get_entity($site_guid); + } else { + $site = get_config('site'); + } + + if (!($site instanceof ElggSite)) { + return false; + } + + return check_entity_relationship($this->guid, 'active_plugin', $site->guid); + } + + /** + * Checks if this plugin can be activated on the current + * Elgg installation. + * + * @todo remove $site_guid param or implement it + * + * @param mixed $site_guid Optional site guid + * @return bool + */ + public function canActivate($site_guid = null) { + if ($this->getPackage()) { + $result = $this->getPackage()->isValid() && $this->getPackage()->checkDependencies(); + if (!$result) { + $this->errorMsg = $this->getPackage()->getError(); + } + + return $result; + } + + return false; + } + + + // activating and deactivating + + /** + * Actives the plugin for the current site. + * + * @param mixed $site_guid Optional site GUID. + * @return bool + */ + public function activate($site_guid = null) { + if ($this->isActive($site_guid)) { + return false; + } + + if (!$this->canActivate()) { + return false; + } + + // set in the db, now perform tasks and emit events + if ($this->setStatus(true, $site_guid)) { + // emit an event. returning false will make this not be activated. + // we need to do this after it's been fully activated + // or the deactivate will be confused. + $params = array( + 'plugin_id' => $this->pluginID, + 'plugin_entity' => $this + ); + + $return = elgg_trigger_event('activate', 'plugin', $params); + + // if there are any on_enable functions, start the plugin now and run them + // Note: this will not run re-run the init hooks! + if ($return) { + if ($this->canReadFile('activate.php')) { + $flags = ELGG_PLUGIN_INCLUDE_START | ELGG_PLUGIN_REGISTER_CLASSES | + ELGG_PLUGIN_REGISTER_LANGUAGES | ELGG_PLUGIN_REGISTER_VIEWS; + + $this->start($flags); + + $return = $this->includeFile('activate.php'); + } + } + + if ($return === false) { + $this->deactivate($site_guid); + } + + return $return; + } + + return false; + } + + /** + * Deactivates the plugin. + * + * @param mixed $site_guid Optional site GUID. + * @return bool + */ + public function deactivate($site_guid = null) { + if (!$this->isActive($site_guid)) { + return false; + } + + // emit an event. returning false will cause this to not be deactivated. + $params = array( + 'plugin_id' => $this->pluginID, + 'plugin_entity' => $this + ); + + $return = elgg_trigger_event('deactivate', 'plugin', $params); + + // run any deactivate code + if ($return) { + if ($this->canReadFile('deactivate.php')) { + $return = $this->includeFile('deactivate.php'); + } + } + + if ($return === false) { + return false; + } else { + return $this->setStatus(false, $site_guid); + } + } + + /** + * Start the plugin. + * + * @param int $flags Start flags for the plugin. See the constants in lib/plugins.php for details. + * @return true + * @throws PluginException + */ + public function start($flags) { + //if (!$this->canActivate()) { + // return false; + //} + + // include classes + if ($flags & ELGG_PLUGIN_REGISTER_CLASSES) { + $this->registerClasses(); + } + + // include start file + if ($flags & ELGG_PLUGIN_INCLUDE_START) { + $this->includeFile('start.php'); + } + + // include views + if ($flags & ELGG_PLUGIN_REGISTER_VIEWS) { + $this->registerViews(); + } + + // include languages + if ($flags & ELGG_PLUGIN_REGISTER_LANGUAGES) { + $this->registerLanguages(); + } + + return true; + } + + + // start helpers + + /** + * Includes one of the plugins files + * + * @param string $filename The name of the file + * + * @throws PluginException + * @return mixed The return value of the included file (or 1 if there is none) + */ + protected function includeFile($filename) { + // This needs to be here to be backwards compatible for 1.0-1.7. + // They expect the global config object to be available in start.php. + if ($filename == 'start.php') { + global $CONFIG; + } + + $filepath = "$this->path/$filename"; + + if (!$this->canReadFile($filename)) { + $msg = elgg_echo('ElggPlugin:Exception:CannotIncludeFile', + array($filename, $this->getID(), $this->guid, $this->path)); + throw new PluginException($msg); + } + + return include $filepath; + } + + /** + * Checks whether a plugin file with the given name exists + * + * @param string $filename The name of the file + * @return bool + */ + protected function canReadFile($filename) { + return is_readable($this->path . '/' . $filename); + } + + /** + * Registers the plugin's views + * + * @throws PluginException + * @return true + */ + protected function registerViews() { + $view_dir = "$this->path/views/"; + + // plugins don't have to have views. + if (!is_dir($view_dir)) { + return true; + } + + // but if they do, they have to be readable + $handle = opendir($view_dir); + if (!$handle) { + $msg = elgg_echo('ElggPlugin:Exception:CannotRegisterViews', + array($this->getID(), $this->guid, $view_dir)); + throw new PluginException($msg); + } + + while (FALSE !== ($view_type = readdir($handle))) { + $view_type_dir = $view_dir . $view_type; + + if ('.' !== substr($view_type, 0, 1) && is_dir($view_type_dir)) { + if (autoregister_views('', $view_type_dir, $view_dir, $view_type)) { + elgg_register_viewtype($view_type); + } else { + $msg = elgg_echo('ElggPlugin:Exception:CannotRegisterViews', + array($this->getID(), $view_type_dir)); + throw new PluginException($msg); + } + } + } + + return true; + } + + /** + * Registers the plugin's languages + * + * @throws PluginException + * @return true + */ + protected function registerLanguages() { + $languages_path = "$this->path/languages"; + + // don't need to have classes + if (!is_dir($languages_path)) { + return true; + } + + // but need to have working ones. + if (!register_translations($languages_path)) { + $msg = elgg_echo('ElggPlugin:Exception:CannotRegisterLanguages', + array($this->getID(), $this->guid, $languages_path)); + throw new PluginException($msg); + } + + return true; + } + + /** + * Registers the plugin's classes + * + * @throws PluginException + * @return true + */ + protected function registerClasses() { + $classes_path = "$this->path/classes"; + + // don't need to have classes + if (!is_dir($classes_path)) { + return true; + } + + elgg_register_classes($classes_path); + + return true; + } + + + // generic helpers and overrides + + /** + * Get a value from private settings. + * + * @param string $name Name + * + * @return mixed + */ + public function get($name) { + // rewrite for old and inaccurate plugin:setting + if (strstr($name, 'plugin:setting:')) { + $msg = 'Direct access of user settings is deprecated. Use ElggPlugin->getUserSetting()'; + elgg_deprecated_notice($msg, 1.8); + $name = str_replace('plugin:setting:', '', $name); + $name = elgg_namespace_plugin_private_setting('user_setting', $name); + } + + // See if its in our base attribute + if (array_key_exists($name, $this->attributes)) { + return $this->attributes[$name]; + } + + // No, so see if its in the private data store. + // get_private_setting() returns false if it doesn't exist + $meta = $this->getPrivateSetting($name); + + if ($meta === false) { + // Can't find it, so return null + return NULL; + } + + return $meta; + } + + /** + * Save a value as private setting or attribute. + * + * Attributes include title and description. + * + * @param string $name Name + * @param mixed $value Value + * + * @return bool + */ + public function set($name, $value) { + if (array_key_exists($name, $this->attributes)) { + // Check that we're not trying to change the guid! + if ((array_key_exists('guid', $this->attributes)) && ($name == 'guid')) { + return false; + } + + $this->attributes[$name] = $value; + + return true; + } else { + // Hook to validate setting + $value = elgg_trigger_plugin_hook('setting', 'plugin', array( + 'plugin_id' => $this->pluginID, + 'plugin' => $this, + 'name' => $name, + 'value' => $value + ), $value); + + return $this->setPrivateSetting($name, $value); + } + } + + /** + * Sets the plugin to active or inactive for $site_guid. + * + * @param bool $active Set to active or inactive + * @param mixed $site_guid Int for specific site, null for current site. + * + * @return bool + */ + private function setStatus($active, $site_guid = null) { + if (!$this->guid) { + return false; + } + + if ($site_guid) { + $site = get_entity($site_guid); + + if (!($site instanceof ElggSite)) { + return false; + } + } else { + $site = get_config('site'); + } + + if ($active) { + return add_entity_relationship($this->guid, 'active_plugin', $site->guid); + } else { + return remove_entity_relationship($this->guid, 'active_plugin', $site->guid); + } + } + + /** + * Returns the last error message registered. + * + * @return string|null + */ + public function getError() { + return $this->errorMsg; + } + + /** + * Returns this plugin's ElggPluginManifest object + * + * @return ElggPluginManifest + */ + public function getManifest() { + if ($this->manifest instanceof ElggPluginManifest) { + return $this->manifest; + } + + try { + $this->manifest = $this->getPackage()->getManifest(); + } catch (Exception $e) { + elgg_log("Failed to load manifest for plugin $this->guid. " . $e->getMessage(), 'WARNING'); + $this->errorMsg = $e->getmessage(); + } + + return $this->manifest; + } + + /** + * Returns this plugin's ElggPluginPackage object + * + * @return ElggPluginPackage + */ + public function getPackage() { + if ($this->package instanceof ElggPluginPackage) { + return $this->package; + } + + try { + $this->package = new ElggPluginPackage($this->path, false); + } catch (Exception $e) { + elgg_log("Failed to load package for $this->guid. " . $e->getMessage(), 'WARNING'); + $this->errorMsg = $e->getmessage(); + } + + return $this->package; + } +} diff --git a/engine/classes/ElggPluginManifest.php b/engine/classes/ElggPluginManifest.php new file mode 100644 index 000000000..6912c2b08 --- /dev/null +++ b/engine/classes/ElggPluginManifest.php @@ -0,0 +1,656 @@ +<?php +/** + * Parses Elgg manifest.xml files. + * + * Normalizes the values from the ElggManifestParser object. + * + * This requires an ElggPluginManifestParser class implementation + * as $this->parser. + * + * To add new parser versions, name them ElggPluginManifestParserXX + * where XX is the version specified in the top-level <plugin_manifest> + * tag's XML namespace. + * + * @package Elgg.Core + * @subpackage Plugins + * @since 1.8 + */ +class ElggPluginManifest { + + /** + * The parser object + */ + protected $parser; + + /** + * The root for plugin manifest namespaces. + * This is in the format http://www.elgg.org/plugin_manifest/<version> + */ + protected $namespace_root = 'http://www.elgg.org/plugin_manifest/'; + + /** + * The expected structure of a plugins requires element + */ + private $depsStructPlugin = array( + 'type' => '', + 'name' => '', + 'version' => '', + 'comparison' => 'ge' + ); + + /** + * The expected structure of a priority element + */ + private $depsStructPriority = array( + 'type' => '', + 'priority' => '', + 'plugin' => '' + ); + + /* + * The expected structure of elgg_version and elgg_release requires element + */ + private $depsStructElgg = array( + 'type' => '', + 'version' => '', + 'comparison' => 'ge' + ); + + /** + * The expected structure of a requires php_ini dependency element + */ + private $depsStructPhpIni = array( + 'type' => '', + 'name' => '', + 'value' => '', + 'comparison' => '=' + ); + + /** + * The expected structure of a requires php_extension dependency element + */ + private $depsStructPhpExtension = array( + 'type' => '', + 'name' => '', + 'version' => '', + 'comparison' => '=' + ); + + /** + * The expected structure of a conflicts depedency element + */ + private $depsConflictsStruct = array( + 'type' => '', + 'name' => '', + 'version' => '', + 'comparison' => '=' + ); + + /** + * The expected structure of a provides dependency element. + */ + private $depsProvidesStruct = array( + 'type' => '', + 'name' => '', + 'version' => '' + ); + + /** + * The expected structure of a screenshot element + */ + private $screenshotStruct = array( + 'description' => '', + 'path' => '' + ); + + /** + * The API version of the manifest. + * + * @var int + */ + protected $apiVersion; + + /** + * The optional plugin id this manifest belongs to. + * + * @var string + */ + protected $pluginID; + + /** + * Load a manifest file, XmlElement or path to manifest.xml file + * + * @param mixed $manifest A string, XmlElement, or path of a manifest file. + * @param string $plugin_id Optional ID of the owning plugin. Used to + * fill in some values automatically. + */ + public function __construct($manifest, $plugin_id = null) { + if ($plugin_id) { + $this->pluginID = $plugin_id; + } + + // see if we need to construct the xml object. + if ($manifest instanceof ElggXMLElement) { + $manifest_obj = $manifest; + } else { + if (substr(trim($manifest), 0, 1) == '<') { + // this is a string + $raw_xml = $manifest; + } elseif (is_file($manifest)) { + // this is a file + $raw_xml = file_get_contents($manifest); + } + + $manifest_obj = xml_to_object($raw_xml); + } + + if (!$manifest_obj) { + throw new PluginException(elgg_echo('PluginException:InvalidManifest', + array($this->getPluginID()))); + } + + // set manifest api version + if (isset($manifest_obj->attributes['xmlns'])) { + $namespace = $manifest_obj->attributes['xmlns']; + $version = str_replace($this->namespace_root, '', $namespace); + } else { + $version = 1.7; + } + + $this->apiVersion = $version; + + $parser_class_name = 'ElggPluginManifestParser' . str_replace('.', '', $this->apiVersion); + + // @todo currently the autoloader freaks out if a class doesn't exist. + try { + $class_exists = class_exists($parser_class_name); + } catch (Exception $e) { + $class_exists = false; + } + + if ($class_exists) { + $this->parser = new $parser_class_name($manifest_obj, $this); + } else { + throw new PluginException(elgg_echo('PluginException:NoAvailableParser', + array($this->apiVersion, $this->getPluginID()))); + } + + if (!$this->parser->parse()) { + throw new PluginException(elgg_echo('PluginException:ParserError', + array($this->apiVersion, $this->getPluginID()))); + } + + return true; + } + + /** + * Returns the API version in use. + * + * @return int + */ + public function getApiVersion() { + return $this->apiVersion; + } + + /** + * Returns the plugin ID. + * + * @return string + */ + public function getPluginID() { + if ($this->pluginID) { + return $this->pluginID; + } else { + return elgg_echo('unknown'); + } + } + + /** + * Returns the manifest array. + * + * Used for backward compatibility. Specific + * methods should be called instead. + * + * @return array + */ + public function getManifest() { + return $this->parser->getManifest(); + } + + /*************************************** + * Parsed and Normalized Manifest Data * + ***************************************/ + + /** + * Returns the plugin name + * + * @return string + */ + public function getName() { + $name = $this->parser->getAttribute('name'); + + if (!$name && $this->pluginID) { + $name = ucwords(str_replace('_', ' ', $this->pluginID)); + } + + return $name; + } + + + /** + * Return the description + * + * @return string + */ + public function getDescription() { + return $this->parser->getAttribute('description'); + } + + /** + * Return the short description + * + * @return string + */ + public function getBlurb() { + $blurb = $this->parser->getAttribute('blurb'); + + if (!$blurb) { + $blurb = elgg_get_excerpt($this->getDescription()); + } + + return $blurb; + } + + /** + * Returns the license + * + * @return string + */ + public function getLicense() { + // license vs licence. Use license. + $en_us = $this->parser->getAttribute('license'); + if ($en_us) { + return $en_us; + } else { + return $this->parser->getAttribute('licence'); + } + } + + /** + * Returns the repository url + * + * @return string + */ + public function getRepositoryURL() { + return $this->parser->getAttribute('repository'); + } + + /** + * Returns the bug tracker page + * + * @return string + */ + public function getBugTrackerURL() { + return $this->parser->getAttribute('bugtracker'); + } + + /** + * Returns the donations page + * + * @return string + */ + public function getDonationsPageURL() { + return $this->parser->getAttribute('donations'); + } + + /** + * Returns the version of the plugin. + * + * @return float + */ + public function getVersion() { + return $this->parser->getAttribute('version'); + } + + /** + * Returns the plugin author. + * + * @return string + */ + public function getAuthor() { + return $this->parser->getAttribute('author'); + } + + /** + * Return the copyright + * + * @return string + */ + public function getCopyright() { + return $this->parser->getAttribute('copyright'); + } + + /** + * Return the website + * + * @return string + */ + public function getWebsite() { + return $this->parser->getAttribute('website'); + } + + /** + * Return the categories listed for this plugin + * + * @return array + */ + public function getCategories() { + $bundled_plugins = array('blog', 'bookmarks', 'categories', + 'custom_index', 'dashboard', 'developers', 'diagnostics', + 'embed', 'externalpages', 'file', 'garbagecollector', + 'groups', 'htmlawed', 'invitefriends', 'likes', + 'logbrowser', 'logrotate', 'members', 'messageboard', + 'messages', 'notifications', 'oauth_api', 'pages', 'profile', + 'reportedcontent', 'search', 'tagcloud', 'thewire', 'tinymce', + 'twitter', 'twitter_api', 'uservalidationbyemail', 'zaudio', + ); + + $cats = $this->parser->getAttribute('category'); + + if (!$cats) { + $cats = array(); + } + + if (in_array('bundled', $cats) && !in_array($this->getPluginID(), $bundled_plugins)) { + unset($cats[array_search('bundled', $cats)]); + } + + return $cats; + } + + /** + * Return the screenshots listed. + * + * @return array + */ + public function getScreenshots() { + $ss = $this->parser->getAttribute('screenshot'); + + if (!$ss) { + $ss = array(); + } + + $normalized = array(); + foreach ($ss as $s) { + $normalized[] = $this->buildStruct($this->screenshotStruct, $s); + } + + return $normalized; + } + + /** + * Return the list of provides by this plugin. + * + * @return array + */ + public function getProvides() { + // normalize for 1.7 + if ($this->getApiVersion() < 1.8) { + $provides = array(); + } else { + $provides = $this->parser->getAttribute('provides'); + } + + if (!$provides) { + $provides = array(); + } + + // always provide ourself if we can + if ($this->pluginID) { + $provides[] = array( + 'type' => 'plugin', + 'name' => $this->getPluginID(), + 'version' => $this->getVersion() + ); + } + + $normalized = array(); + foreach ($provides as $provide) { + $normalized[] = $this->buildStruct($this->depsProvidesStruct, $provide); + } + + return $normalized; + } + + /** + * Returns the dependencies listed. + * + * @return array + */ + public function getRequires() { + // rewrite the 1.7 style elgg_version as a real requires. + if ($this->apiVersion < 1.8) { + $elgg_version = $this->parser->getAttribute('elgg_version'); + if ($elgg_version) { + $reqs = array( + array( + 'type' => 'elgg_version', + 'version' => $elgg_version, + 'comparison' => 'ge' + ) + ); + } else { + $reqs = array(); + } + } else { + $reqs = $this->parser->getAttribute('requires'); + } + + if (!$reqs) { + $reqs = array(); + } + + $normalized = array(); + foreach ($reqs as $req) { + $normalized[] = $this->normalizeDep($req); + } + + return $normalized; + } + + /** + * Returns the suggests elements. + * + * @return array + */ + public function getSuggests() { + $suggests = $this->parser->getAttribute('suggests'); + + if (!$suggests) { + $suggests = array(); + } + + $normalized = array(); + foreach ($suggests as $suggest) { + $normalized[] = $this->normalizeDep($suggest); + } + + return $normalized; + } + + /** + * Normalizes a dependency array using the defined structs. + * Can be used with either requires or suggests. + * + * @param array $dep A dependency array. + * @return array The normalized deps array. + */ + private function normalizeDep($dep) { + switch ($dep['type']) { + case 'elgg_version': + case 'elgg_release': + $struct = $this->depsStructElgg; + break; + + case 'plugin': + $struct = $this->depsStructPlugin; + break; + + case 'priority': + $struct = $this->depsStructPriority; + break; + + case 'php_extension': + $struct = $this->depsStructPhpExtension; + break; + + case 'php_ini': + $struct = $this->depsStructPhpIni; + + // also normalize boolean values + if (isset($dep['value'])) { + switch (strtolower($dep['value'])) { + case 'yes': + case 'true': + case 'on': + case 1: + $dep['value'] = 1; + break; + + case 'no': + case 'false': + case 'off': + case 0: + case '': + $dep['value'] = 0; + break; + } + } + break; + default: + // unrecognized so we just return the raw dependency + return $dep; + } + + $normalized_dep = $this->buildStruct($struct, $dep); + + // normalize comparison operators + if (isset($normalized_dep['comparison'])) { + switch ($normalized_dep['comparison']) { + case '<': + $normalized_dep['comparison'] = 'lt'; + break; + + case '<=': + $normalized_dep['comparison'] = 'le'; + break; + + case '>': + $normalized_dep['comparison'] = 'gt'; + break; + + case '>=': + $normalized_dep['comparison'] = 'ge'; + break; + + case '==': + case 'eq': + $normalized_dep['comparison'] = '='; + break; + + case '<>': + case 'ne': + $normalized_dep['comparison'] = '!='; + break; + } + } + + return $normalized_dep; + } + + /** + * Returns the conflicts listed + * + * @return array + */ + public function getConflicts() { + // normalize for 1.7 + if ($this->getApiVersion() < 1.8) { + $conflicts = array(); + } else { + $conflicts = $this->parser->getAttribute('conflicts'); + } + + if (!$conflicts) { + $conflicts = array(); + } + + $normalized = array(); + + foreach ($conflicts as $conflict) { + $normalized[] = $this->buildStruct($this->depsConflictsStruct, $conflict); + } + + return $normalized; + } + + /** + * Should this plugin be activated when Elgg is installed + * + * @return bool + */ + public function getActivateOnInstall() { + $activate = $this->parser->getAttribute('activate_on_install'); + switch (strtolower($activate)) { + case 'yes': + case 'true': + case 'on': + case 1: + return true; + + case 'no': + case 'false': + case 'off': + case 0: + case '': + return false; + } + } + + /** + * Normalizes an array into the structure specified + * + * @param array $struct The struct to normalize $element to. + * @param array $array The array + * + * @return array + */ + protected function buildStruct(array $struct, array $array) { + $return = array(); + + foreach ($struct as $index => $default) { + $return[$index] = elgg_extract($index, $array, $default); + } + + return $return; + } + + /** + * Returns a category's friendly name. This can be localized by + * defining the string 'admin:plugins:category:<category>'. If no + * localization is found, returns the category with _ and - converted to ' ' + * and then ucwords()'d. + * + * @param str $category The category as defined in the manifest. + * @return str A human-readable category + */ + static public function getFriendlyCategory($category) { + $cat_raw_string = "admin:plugins:category:$category"; + $cat_display_string = elgg_echo($cat_raw_string); + if ($cat_display_string == $cat_raw_string) { + $category = str_replace(array('-', '_'), ' ', $category); + $cat_display_string = ucwords($category); + } + return $cat_display_string; + } +} diff --git a/engine/classes/ElggPluginManifestParser.php b/engine/classes/ElggPluginManifestParser.php new file mode 100644 index 000000000..af152b561 --- /dev/null +++ b/engine/classes/ElggPluginManifestParser.php @@ -0,0 +1,102 @@ +<?php +/** + * Parent class for manifest parsers. + * + * Converts manifest.xml files or strings to an array. + * + * This should be extended by a class that does the actual work + * to convert based on the manifest.xml version. + * + * This class only parses XML to an XmlEntity object and + * an array. The array should be used primarily to extract + * information since it is quicker to parse once and store + * values from the XmlElement object than to parse the object + * each time. + * + * The array should be an exact representation of the manifest.xml + * file or string. Any normalization needs to be done in the + * calling class / function. + * + * @package Elgg.Core + * @subpackage Plugins + * @since 1.8 + */ +abstract class ElggPluginManifestParser { + /** + * The XmlElement object + * + * @var XmlElement + */ + protected $manifestObject; + + /** + * The manifest array + * + * @var array + */ + protected $manifest; + + /** + * All valid manifest attributes with default values. + * + * @var array + */ + protected $validAttributes; + + /** + * The object we're doing parsing for. + * + * @var object + */ + protected $caller; + + /** + * Loads the manifest XML to be parsed. + * + * @param ElggXmlElement $xml The Manifest XML object to be parsed + * @param object $caller The object calling this parser. + */ + public function __construct(ElggXMLElement $xml, $caller) { + $this->manifestObject = $xml; + $this->caller = $caller; + } + + /** + * Returns the manifest XML object + * + * @return XmlElement + */ + public function getManifestObject() { + return $this->manifestObject; + } + + /** + * Return the parsed manifest array + * + * @return array + */ + public function getManifest() { + return $this->manifest; + } + + /** + * Return an attribute in the manifest. + * + * @param string $name Attribute name + * @return mixed + */ + public function getAttribute($name) { + if (in_array($name, $this->validAttributes) && isset($this->manifest[$name])) { + return $this->manifest[$name]; + } + + return false; + } + + /** + * Parse the XML object into an array + * + * @return bool + */ + abstract public function parse(); +} diff --git a/engine/classes/ElggPluginManifestParser17.php b/engine/classes/ElggPluginManifestParser17.php new file mode 100644 index 000000000..5658ee804 --- /dev/null +++ b/engine/classes/ElggPluginManifestParser17.php @@ -0,0 +1,82 @@ +<?php +/** + * Plugin manifest.xml parser for Elgg 1.7 and lower. + * + * @package Elgg.Core + * @subpackage Plugins + * @since 1.8 + */ +class ElggPluginManifestParser17 extends ElggPluginManifestParser { + /** + * The valid top level attributes and defaults for a 1.7 manifest + */ + protected $validAttributes = array( + 'author', 'version', 'description', 'website', + 'copyright', 'license', 'licence', 'elgg_version', + + // were never really used and not enforced in code. + 'requires', 'recommends', 'conflicts', + + // not a 1.7 field, but we need it + 'name', + ); + + /** + * Parse a manifest object from 1.7 or earlier. + * + * @return void + */ + public function parse() { + if (!isset($this->manifestObject->children)) { + return false; + } + + $elements = array(); + + foreach ($this->manifestObject->children as $element) { + $key = $element->attributes['key']; + $value = $element->attributes['value']; + + // create arrays if multiple fields are set + if (array_key_exists($key, $elements)) { + if (!is_array($elements[$key])) { + $orig = $elements[$key]; + $elements[$key] = array($orig); + } + + $elements[$key][] = $value; + } else { + $elements[$key] = $value; + } + } + + if ($elements && !array_key_exists('name', $elements)) { + $elements['name'] = $this->caller->getName(); + } + + $this->manifest = $elements; + + if (!$this->manifest) { + return false; + } + + return true; + } + + /** + * Return an attribute in the manifest. + * + * Overrides ElggPluginManifestParser::getAttribute() because before 1.8 + * there were no rules...weeeeeeeee! + * + * @param string $name Attribute name + * @return mixed + */ + public function getAttribute($name) { + if (isset($this->manifest[$name])) { + return $this->manifest[$name]; + } + + return false; + } +} diff --git a/engine/classes/ElggPluginManifestParser18.php b/engine/classes/ElggPluginManifestParser18.php new file mode 100644 index 000000000..3b753f17b --- /dev/null +++ b/engine/classes/ElggPluginManifestParser18.php @@ -0,0 +1,97 @@ +<?php +/** + * Plugin manifest.xml parser for Elgg 1.8 and above. + * + * @package Elgg.Core + * @subpackage Plugins + * @since 1.8 + */ +class ElggPluginManifestParser18 extends ElggPluginManifestParser { + /** + * The valid top level attributes and defaults for a 1.8 manifest array. + * + * @var array + */ + protected $validAttributes = array( + 'name', 'author', 'version', 'blurb', 'description','website', + 'repository', 'bugtracker', 'donations', 'copyright', 'license', + 'requires', 'suggests', 'conflicts', 'provides', + 'screenshot', 'category', 'activate_on_install' + ); + + /** + * Required attributes for a valid 1.8 manifest + * + * @var array + */ + protected $requiredAttributes = array( + 'name', 'author', 'version', 'description', 'requires' + ); + + /** + * Parse a manifest object from 1.8 and later + * + * @return void + */ + public function parse() { + $parsed = array(); + foreach ($this->manifestObject->children as $element) { + switch ($element->name) { + // single elements + case 'blurb': + case 'description': + case 'name': + case 'author': + case 'version': + case 'website': + case 'copyright': + case 'license': + case 'repository': + case 'bugtracker': + case 'donations': + case 'activate_on_install': + $parsed[$element->name] = $element->content; + break; + + // arrays + case 'category': + $parsed[$element->name][] = $element->content; + break; + + // 3d arrays + case 'screenshot': + case 'provides': + case 'conflicts': + case 'requires': + case 'suggests': + if (!isset($element->children)) { + return false; + } + + $info = array(); + foreach ($element->children as $child_element) { + $info[$child_element->name] = $child_element->content; + } + + $parsed[$element->name][] = $info; + break; + } + } + + // check we have all the required fields + foreach ($this->requiredAttributes as $attr) { + if (!array_key_exists($attr, $parsed)) { + throw new PluginException(elgg_echo('PluginException:ParserErrorMissingRequiredAttribute', + array($attr, $this->caller->getPluginID()))); + } + } + + $this->manifest = $parsed; + + if (!$this->manifest) { + return false; + } + + return true; + } +} diff --git a/engine/classes/ElggPluginPackage.php b/engine/classes/ElggPluginPackage.php new file mode 100644 index 000000000..37eb4bf4d --- /dev/null +++ b/engine/classes/ElggPluginPackage.php @@ -0,0 +1,640 @@ +<?php +/** + * Manages plugin packages under mod. + * + * @todo This should eventually be merged into ElggPlugin. + * Currently ElggPlugin objects are only used to get and save + * plugin settings and user settings, so not every plugin + * has an ElggPlugin object. It's not implemented in ElggPlugin + * right now because of conflicts with at least the constructor, + * enable(), disable(), and private settings. + * + * Around 1.9 or so we should each plugin over to using + * ElggPlugin and merge ElggPluginPackage and ElggPlugin. + * + * @package Elgg.Core + * @subpackage Plugins + * @since 1.8 + */ +class ElggPluginPackage { + + /** + * The required files in the package + * + * @var array + */ + private $requiredFiles = array( + 'start.php', 'manifest.xml' + ); + + /** + * The optional files that can be read and served through the markdown page handler + * @var array + */ + private $textFiles = array( + 'README.txt', 'CHANGES.txt', + 'INSTALL.txt', 'COPYRIGHT.txt', 'LICENSE.txt', + + 'README', 'README.md', 'README.markdown' + ); + + /** + * Valid types for provides. + * + * @var array + */ + private $providesSupportedTypes = array( + 'plugin', 'php_extension' + ); + + /** + * The type of requires/conflicts supported + * + * @var array + */ + private $depsSupportedTypes = array( + 'elgg_version', 'elgg_release', 'php_extension', 'php_ini', 'plugin', 'priority', + ); + + /** + * An invalid plugin error. + */ + private $errorMsg = ''; + + /** + * Any dependencies messages + */ + private $depsMsgs = array(); + + /** + * The plugin's manifest object + * + * @var ElggPluginManifest + */ + protected $manifest; + + /** + * The plugin's full path + * + * @var string + */ + protected $path; + + /** + * Is the plugin valid? + * + * @var mixed Bool after validation check, null before. + */ + protected $valid = null; + + /** + * The plugin ID (dir name) + * + * @var string + */ + protected $id; + + /** + * Load a plugin package from mod/$id or by full path. + * + * @param string $plugin The ID (directory name) or full path of the plugin. + * @param bool $validate Automatically run isValid()? + * + * @throws PluginException + */ + public function __construct($plugin, $validate = true) { + $plugin_path = elgg_get_plugins_path(); + // @todo wanted to avoid another is_dir() call here. + // should do some profiling to see how much it affects + if (strpos($plugin, $plugin_path) === 0 || is_dir($plugin)) { + // this is a path + $path = sanitise_filepath($plugin); + + // the id is the last element of the array + $path_array = explode('/', trim($path, '/')); + $id = array_pop($path_array); + } else { + // this is a plugin id + // strict plugin names + if (preg_match('/[^a-z0-9\.\-_]/i', $plugin)) { + throw new PluginException(elgg_echo('PluginException:InvalidID', array($plugin))); + } + + $path = "{$plugin_path}$plugin/"; + $id = $plugin; + } + + if (!is_dir($path)) { + throw new PluginException(elgg_echo('PluginException:InvalidPath', array($path))); + } + + $this->path = $path; + $this->id = $id; + + if ($validate && !$this->isValid()) { + if ($this->errorMsg) { + throw new PluginException(elgg_echo('PluginException:InvalidPlugin:Details', + array($plugin, $this->errorMsg))); + } else { + throw new PluginException(elgg_echo('PluginException:InvalidPlugin', array($plugin))); + } + } + + return true; + } + + /******************************** + * Validation and sanity checks * + ********************************/ + + /** + * Checks if this is a valid Elgg plugin. + * + * Checks for requires files as defined at the start of this + * class. Will check require manifest fields via ElggPluginManifest + * for Elgg 1.8 plugins. + * + * @note This doesn't check dependencies or conflicts. + * Use {@link ElggPluginPackage::canActivate()} or + * {@link ElggPluginPackage::checkDependencies()} for that. + * + * @return bool + */ + public function isValid() { + if (isset($this->valid)) { + return $this->valid; + } + + // check required files. + $have_req_files = true; + foreach ($this->requiredFiles as $file) { + if (!is_readable($this->path . $file)) { + $have_req_files = false; + $this->errorMsg = + elgg_echo('ElggPluginPackage:InvalidPlugin:MissingFile', array($file)); + break; + } + } + + // check required files + if (!$have_req_files) { + return $this->valid = false; + } + + // check for valid manifest. + if (!$this->loadManifest()) { + return $this->valid = false; + } + + // can't require or conflict with yourself or something you provide. + // make sure provides are all valid. + if (!$this->isSaneDeps()) { + return $this->valid = false; + } + + return $this->valid = true; + } + + /** + * Check the plugin doesn't require or conflict with itself + * or something provides. Also check that it only list + * valid provides. Deps are checked in checkDependencies() + * + * @note Plugins always provide themselves. + * + * @todo Don't let them require and conflict the same thing + * + * @return bool + */ + private function isSaneDeps() { + // protection against plugins with no manifest file + if (!$this->getManifest()) { + return false; + } + + // Note: $conflicts and $requires are not unused. They're called dynamically + $conflicts = $this->getManifest()->getConflicts(); + $requires = $this->getManifest()->getRequires(); + $provides = $this->getManifest()->getProvides(); + + foreach ($provides as $provide) { + // only valid provide types + if (!in_array($provide['type'], $this->providesSupportedTypes)) { + $this->errorMsg = + elgg_echo('ElggPluginPackage:InvalidPlugin:InvalidProvides', array($provide['type'])); + return false; + } + + // doesn't conflict or require any of its provides + $name = $provide['name']; + foreach (array('conflicts', 'requires') as $dep_type) { + foreach (${$dep_type} as $dep) { + if (!in_array($dep['type'], $this->depsSupportedTypes)) { + $this->errorMsg = + elgg_echo('ElggPluginPackage:InvalidPlugin:InvalidDependency', array($dep['type'])); + return false; + } + + // make sure nothing is providing something it conflicts or requires. + if (isset($dep['name']) && $dep['name'] == $name) { + $version_compare = version_compare($provide['version'], $dep['version'], $dep['comparison']); + + if ($version_compare) { + $this->errorMsg = + elgg_echo('ElggPluginPackage:InvalidPlugin:CircularDep', + array($dep['type'], $dep['name'], $this->id)); + + return false; + } + } + } + } + } + + return true; + } + + + /************ + * Manifest * + ************/ + + /** + * Returns a parsed manifest file. + * + * @return ElggPluginManifest + */ + public function getManifest() { + if (!$this->manifest) { + if (!$this->loadManifest()) { + return false; + } + } + + return $this->manifest; + } + + /** + * Loads the manifest into this->manifest as an + * ElggPluginManifest object. + * + * @return bool + */ + private function loadManifest() { + $file = $this->path . 'manifest.xml'; + + try { + $this->manifest = new ElggPluginManifest($file, $this->id); + } catch (Exception $e) { + $this->errorMsg = $e->getMessage(); + return false; + } + + if ($this->manifest instanceof ElggPluginManifest) { + return true; + } + + $this->errorMsg = elgg_echo('unknown_error'); + return false; + } + + /**************** + * Readme Files * + ***************/ + + /** + * Returns an array of present and readable text files + * + * @return array + */ + public function getTextFilenames() { + return $this->textFiles; + } + + /*********************** + * Dependencies system * + ***********************/ + + /** + * Returns if the Elgg system meets the plugin's dependency + * requirements. This includes both requires and conflicts. + * + * Full reports can be requested. The results are returned + * as an array of arrays in the form array( + * 'type' => requires|conflicts, + * 'dep' => array( dependency array ), + * 'status' => bool if depedency is met, + * 'comment' => optional comment to display to the user. + * ) + * + * @param bool $full_report Return a full report. + * @return bool|array + */ + public function checkDependencies($full_report = false) { + // Note: $conflicts and $requires are not unused. They're called dynamically + $requires = $this->getManifest()->getRequires(); + $conflicts = $this->getManifest()->getConflicts(); + + $enabled_plugins = elgg_get_plugins('active'); + $this_id = $this->getID(); + $report = array(); + + // first, check if any active plugin conflicts with us. + foreach ($enabled_plugins as $plugin) { + $temp_conflicts = array(); + $temp_manifest = $plugin->getManifest(); + if ($temp_manifest instanceof ElggPluginManifest) { + $temp_conflicts = $plugin->getManifest()->getConflicts(); + } + foreach ($temp_conflicts as $conflict) { + if ($conflict['type'] == 'plugin' && $conflict['name'] == $this_id) { + $result = $this->checkDepPlugin($conflict, $enabled_plugins, false); + + // rewrite the conflict to show the originating plugin + $conflict['name'] = $plugin->getManifest()->getName(); + + if (!$full_report && !$result['status']) { + $this->errorMsg = "Conflicts with plugin \"{$plugin->getManifest()->getName()}\"."; + return $result['status']; + } else { + $report[] = array( + 'type' => 'conflicted', + 'dep' => $conflict, + 'status' => $result['status'], + 'value' => $this->getManifest()->getVersion() + ); + } + } + } + } + + $check_types = array('requires', 'conflicts'); + + if ($full_report) { + // Note: $suggests is not unused. It's called dynamically + $suggests = $this->getManifest()->getSuggests(); + $check_types[] = 'suggests'; + } + + foreach ($check_types as $dep_type) { + $inverse = ($dep_type == 'conflicts') ? true : false; + + foreach (${$dep_type} as $dep) { + switch ($dep['type']) { + case 'elgg_version': + $result = $this->checkDepElgg($dep, get_version(), $inverse); + break; + + case 'elgg_release': + $result = $this->checkDepElgg($dep, get_version(true), $inverse); + break; + + case 'plugin': + $result = $this->checkDepPlugin($dep, $enabled_plugins, $inverse); + break; + + case 'priority': + $result = $this->checkDepPriority($dep, $enabled_plugins, $inverse); + break; + + case 'php_extension': + $result = $this->checkDepPhpExtension($dep, $inverse); + break; + + case 'php_ini': + $result = $this->checkDepPhpIni($dep, $inverse); + break; + } + + // unless we're doing a full report, break as soon as we fail. + if (!$full_report && !$result['status']) { + $this->errorMsg = "Missing dependencies."; + return $result['status']; + } else { + // build report element and comment + $report[] = array( + 'type' => $dep_type, + 'dep' => $dep, + 'status' => $result['status'], + 'value' => $result['value'] + ); + } + } + } + + if ($full_report) { + // add provides to full report + $provides = $this->getManifest()->getProvides(); + + foreach ($provides as $provide) { + $report[] = array( + 'type' => 'provides', + 'dep' => $provide, + 'status' => true, + 'value' => '' + ); + } + + return $report; + } + + return true; + } + + /** + * Checks if $plugins meets the requirement by $dep. + * + * @param array $dep An Elgg manifest.xml deps array + * @param array $plugins A list of plugins as returned by elgg_get_plugins(); + * @param bool $inverse Inverse the results to use as a conflicts. + * @return bool + */ + private function checkDepPlugin(array $dep, array $plugins, $inverse = false) { + $r = elgg_check_plugins_provides('plugin', $dep['name'], $dep['version'], $dep['comparison']); + + if ($inverse) { + $r['status'] = !$r['status']; + } + + return $r; + } + + /** + * Checks if $plugins meets the requirement by $dep. + * + * @param array $dep An Elgg manifest.xml deps array + * @param array $plugins A list of plugins as returned by elgg_get_plugins(); + * @param bool $inverse Inverse the results to use as a conflicts. + * @return bool + */ + private function checkDepPriority(array $dep, array $plugins, $inverse = false) { + // grab the ElggPlugin using this package. + $plugin_package = elgg_get_plugin_from_id($this->getID()); + $plugin_priority = $plugin_package->getPriority(); + $test_plugin = elgg_get_plugin_from_id($dep['plugin']); + + // If this isn't a plugin or the plugin isn't installed or active + // priority doesn't matter. Use requires to check if a plugin is active. + if (!$plugin_package || !$test_plugin || !$test_plugin->isActive()) { + return array( + 'status' => true, + 'value' => 'uninstalled' + ); + } + + $test_plugin_priority = $test_plugin->getPriority(); + + switch ($dep['priority']) { + case 'before': + $status = $plugin_priority < $test_plugin_priority; + break; + + case 'after': + $status = $plugin_priority > $test_plugin_priority; + break; + + default; + $status = false; + } + + // get the current value + if ($plugin_priority < $test_plugin_priority) { + $value = 'before'; + } else { + $value = 'after'; + } + + if ($inverse) { + $status = !$status; + } + + return array( + 'status' => $status, + 'value' => $value + ); + } + + /** + * Checks if $elgg_version meets the requirement by $dep. + * + * @param array $dep An Elgg manifest.xml deps array + * @param array $elgg_version An Elgg version (either YYYYMMDDXX or X.Y.Z) + * @param bool $inverse Inverse the result to use as a conflicts. + * @return bool + */ + private function checkDepElgg(array $dep, $elgg_version, $inverse = false) { + $status = version_compare($elgg_version, $dep['version'], $dep['comparison']); + + if ($inverse) { + $status = !$status; + } + + return array( + 'status' => $status, + 'value' => $elgg_version + ); + } + + /** + * Checks if the PHP extension in $dep is loaded. + * + * @todo Can this be merged with the plugin checker? + * + * @param array $dep An Elgg manifest.xml deps array + * @param bool $inverse Inverse the result to use as a conflicts. + * @return array An array in the form array( + * 'status' => bool + * 'value' => string The version provided + * ) + */ + private function checkDepPhpExtension(array $dep, $inverse = false) { + $name = $dep['name']; + $version = $dep['version']; + $comparison = $dep['comparison']; + + // not enabled. + $status = extension_loaded($name); + + // enabled. check version. + $ext_version = phpversion($name); + + if ($status) { + // some extensions (like gd) don't provide versions. neat. + // don't check version info and return a lie. + if ($ext_version && $version) { + $status = version_compare($ext_version, $version, $comparison); + } + + if (!$ext_version) { + $ext_version = '???'; + } + } + + // some php extensions can be emulated, so check provides. + if ($status == false) { + $provides = elgg_check_plugins_provides('php_extension', $name, $version, $comparison); + $status = $provides['status']; + $ext_version = $provides['value']; + } + + if ($inverse) { + $status = !$status; + } + + return array( + 'status' => $status, + 'value' => $ext_version + ); + } + + /** + * Check if the PHP ini setting satisfies $dep. + * + * @param array $dep An Elgg manifest.xml deps array + * @param bool $inverse Inverse the result to use as a conflicts. + * @return bool + */ + private function checkDepPhpIni($dep, $inverse = false) { + $name = $dep['name']; + $value = $dep['value']; + $comparison = $dep['comparison']; + + // ini_get() normalizes truthy values to 1 but falsey values to 0 or ''. + // version_compare() considers '' < 0, so normalize '' to 0. + // ElggPluginManifest normalizes all bool values and '' to 1 or 0. + $setting = ini_get($name); + + if ($setting === '') { + $setting = 0; + } + + $status = version_compare($setting, $value, $comparison); + + if ($inverse) { + $status = !$status; + } + + return array( + 'status' => $status, + 'value' => $setting + ); + } + + /** + * Returns the Plugin ID + * + * @return string + */ + public function getID() { + return $this->id; + } + + /** + * Returns the last error message. + * + * @return string + */ + public function getError() { + return $this->errorMsg; + } +} diff --git a/engine/classes/ElggPriorityList.php b/engine/classes/ElggPriorityList.php new file mode 100644 index 000000000..416df885c --- /dev/null +++ b/engine/classes/ElggPriorityList.php @@ -0,0 +1,366 @@ +<?php +/** + * Iterate over elements in a specific priority. + * + * $pl = new ElggPriorityList(); + * $pl->add('Element 0'); + * $pl->add('Element 10', 10); + * $pl->add('Element -10', -10); + * + * foreach ($pl as $priority => $element) { + * var_dump("$priority => $element"); + * } + * + * Yields: + * -10 => Element -10 + * 0 => Element 0 + * 10 => Element 10 + * + * Collisions on priority are handled by inserting the element at or as close to the + * requested priority as possible: + * + * $pl = new ElggPriorityList(); + * $pl->add('Element 5', 5); + * $pl->add('Colliding element 5', 5); + * $pl->add('Another colliding element 5', 5); + * + * foreach ($pl as $priority => $element) { + * var_dump("$priority => $element"); + * } + * + * Yields: + * 5 => 'Element 5', + * 6 => 'Colliding element 5', + * 7 => 'Another colliding element 5' + * + * You can do priority lookups by element: + * + * $pl = new ElggPriorityList(); + * $pl->add('Element 0'); + * $pl->add('Element -5', -5); + * $pl->add('Element 10', 10); + * $pl->add('Element -10', -10); + * + * $priority = $pl->getPriority('Element -5'); + * + * Or element lookups by priority. + * $element = $pl->getElement(-5); + * + * To remove elements, pass the element. + * $pl->remove('Element -10'); + * + * To check if an element exists: + * $pl->contains('Element -5'); + * + * To move an element: + * $pl->move('Element -5', -3); + * + * ElggPriorityList only tracks priority. No checking is done in ElggPriorityList for duplicates or + * updating. If you need to track this use objects and an external map: + * + * function elgg_register_something($id, $display_name, $location, $priority = 500) { + * // $id => $element. + * static $map = array(); + * static $list; + * + * if (!$list) { + * $list = new ElggPriorityList(); + * } + * + * // update if already registered. + * if (isset($map[$id])) { + * $element = $map[$id]; + * // move it first because we have to pass the original element. + * if (!$list->move($element, $priority)) { + * return false; + * } + * $element->display_name = $display_name; + * $element->location = $location; + * } else { + * $element = new stdClass(); + * $element->display_name = $display_name; + * $element->location = $location; + * if (!$list->add($element, $priority)) { + * return false; + * } + * $map[$id] = $element; + * } + * + * return true; + * } + * + * @package Elgg.Core + * @subpackage Helpers + */ +class ElggPriorityList + implements Iterator, Countable { + + /** + * The list of elements + * + * @var array + */ + private $elements = array(); + + /** + * Create a new priority list. + * + * @param array $elements An optional array of priorities => element + */ + public function __construct(array $elements = array()) { + if ($elements) { + foreach ($elements as $priority => $element) { + $this->add($element, $priority); + } + } + } + + /** + * Adds an element to the list. + * + * @warning This returns the priority at which the element was added, which can be 0. Use + * !== false to check for success. + * + * @param mixed $element The element to add to the list. + * @param mixed $priority Priority to add the element. In priority collisions, the original element + * maintains its priority and the new element is to the next available + * slot, taking into consideration all previously registered elements. + * Negative elements are accepted. + * @param bool $exact unused + * @return int The priority of the added element. + * @todo remove $exact or implement it. Note we use variable name strict below. + */ + public function add($element, $priority = null, $exact = false) { + if ($priority !== null && !is_numeric($priority)) { + return false; + } else { + $priority = $this->getNextPriority($priority); + } + + $this->elements[$priority] = $element; + $this->sorted = false; + return $priority; + } + + /** + * Removes an element from the list. + * + * @warning The element must have the same attributes / values. If using $strict, it must have + * the same types. array(10) will fail in strict against array('10') (str vs int). + * + * @param mixed $element The element to remove from the list + * @param bool $strict Whether to check the type of the element match + * @return bool + */ + public function remove($element, $strict = false) { + $index = array_search($element, $this->elements, $strict); + if ($index !== false) { + unset($this->elements[$index]); + return true; + } else { + return false; + } + } + + /** + * Move an existing element to a new priority. + * + * @param mixed $element The element to move + * @param int $new_priority The new priority for the element + * @param bool $strict Whether to check the type of the element match + * @return bool + */ + public function move($element, $new_priority, $strict = false) { + $new_priority = (int) $new_priority; + + $current_priority = $this->getPriority($element, $strict); + if ($current_priority === false) { + return false; + } + + if ($current_priority == $new_priority) { + return true; + } + + // move the actual element so strict operations still work + $element = $this->getElement($current_priority); + unset($this->elements[$current_priority]); + return $this->add($element, $new_priority); + } + + /** + * Returns the elements + * + * @return array + */ + public function getElements() { + $this->sortIfUnsorted(); + return $this->elements; + } + + /** + * Sort the elements optionally by a callback function. + * + * If no user function is provided the elements are sorted by priority registered. + * + * The callback function should accept the array of elements as the first + * argument and should return a sorted array. + * + * This function can be called multiple times. + * + * @param callback $callback The callback for sorting. Numeric sorting is the default. + * @return bool + */ + public function sort($callback = null) { + if (!$callback) { + ksort($this->elements, SORT_NUMERIC); + } else { + $sorted = call_user_func($callback, $this->elements); + + if (!$sorted) { + return false; + } + + $this->elements = $sorted; + } + + $this->sorted = true; + return true; + } + + /** + * Sort the elements if they haven't been sorted yet. + * + * @return bool + */ + private function sortIfUnsorted() { + if (!$this->sorted) { + return $this->sort(); + } + } + + /** + * Returns the next priority available. + * + * @param int $near Make the priority as close to $near as possible. + * @return int + */ + public function getNextPriority($near = 0) { + $near = (int) $near; + + while (array_key_exists($near, $this->elements)) { + $near++; + } + + return $near; + } + + /** + * Returns the priority of an element if it exists in the list. + * + * @warning This can return 0 if the element's priority is 0. + * + * @param mixed $element The element to check for. + * @param bool $strict Use strict checking? + * @return mixed False if the element doesn't exists, the priority if it does. + */ + public function getPriority($element, $strict = false) { + return array_search($element, $this->elements, $strict); + } + + /** + * Returns the element at $priority. + * + * @param int $priority The priority + * @return mixed The element or false on fail. + */ + public function getElement($priority) { + return (isset($this->elements[$priority])) ? $this->elements[$priority] : false; + } + + /** + * Returns if the list contains $element. + * + * @param mixed $element The element to check. + * @param bool $strict Use strict checking? + * @return bool + */ + public function contains($element, $strict = false) { + return $this->getPriority($element, $strict) !== false; + } + + + /********************** + * Interface methods * + **********************/ + + /** + * Iterator + */ + + /** + * PHP Iterator Interface + * + * @see Iterator::rewind() + * @return void + */ + public function rewind() { + $this->sortIfUnsorted(); + return reset($this->elements); + } + + /** + * PHP Iterator Interface + * + * @see Iterator::current() + * @return mixed + */ + public function current() { + $this->sortIfUnsorted(); + return current($this->elements); + } + + /** + * PHP Iterator Interface + * + * @see Iterator::key() + * @return int + */ + public function key() { + $this->sortIfUnsorted(); + return key($this->elements); + } + + /** + * PHP Iterator Interface + * + * @see Iterator::next() + * @return mixed + */ + public function next() { + $this->sortIfUnsorted(); + return next($this->elements); + } + + /** + * PHP Iterator Interface + * + * @see Iterator::valid() + * @return bool + */ + public function valid() { + $this->sortIfUnsorted(); + $key = key($this->elements); + return ($key !== NULL && $key !== FALSE); + } + + /** + * Countable interface + * + * @see Countable::count() + * @return int + */ + public function count() { + return count($this->elements); + } +}
\ No newline at end of file diff --git a/engine/classes/ElggRelationship.php b/engine/classes/ElggRelationship.php new file mode 100644 index 000000000..d2e88882a --- /dev/null +++ b/engine/classes/ElggRelationship.php @@ -0,0 +1,231 @@ +<?php +/** + * Relationship class. + * + * @package Elgg.Core + * @subpackage Core + * + * @property int $id The unique identifier (read-only) + * @property int $guid_one The GUID of the subject of the relationship + * @property string $relationship The name of the relationship + * @property int $guid_two The GUID of the object of the relationship + * @property int $time_created A UNIX timestamp of when the relationship was created (read-only, set on first save) + */ +class ElggRelationship extends ElggData implements + Importable +{ + + /** + * Create a relationship object, optionally from a given id value or row. + * + * @param mixed $id ElggRelationship id, database row, or null for new relationship + */ + function __construct($id = null) { + $this->initializeAttributes(); + + if (!empty($id)) { + if ($id instanceof stdClass) { + $relationship = $id; // Create from db row + } else { + $relationship = get_relationship($id); + } + + if ($relationship) { + $objarray = (array) $relationship; + foreach ($objarray as $key => $value) { + $this->attributes[$key] = $value; + } + } + } + } + + /** + * Class member get overloading + * + * @param string $name Name + * + * @return mixed + */ + function get($name) { + if (array_key_exists($name, $this->attributes)) { + return $this->attributes[$name]; + } + + return null; + } + + /** + * Class member set overloading + * + * @param string $name Name + * @param mixed $value Value + * + * @return mixed + */ + function set($name, $value) { + $this->attributes[$name] = $value; + return true; + } + + /** + * Save the relationship + * + * @return int the relationship id + * @throws IOException + */ + public function save() { + if ($this->id > 0) { + delete_relationship($this->id); + } + + $this->id = add_entity_relationship($this->guid_one, $this->relationship, $this->guid_two); + if (!$this->id) { + throw new IOException(elgg_echo('IOException:UnableToSaveNew', array(get_class()))); + } + + return $this->id; + } + + /** + * Delete a given relationship. + * + * @return bool + */ + public function delete() { + return delete_relationship($this->id); + } + + /** + * Get a URL for this relationship. + * + * @return string + */ + public function getURL() { + return get_relationship_url($this->id); + } + + // EXPORTABLE INTERFACE //////////////////////////////////////////////////////////// + + /** + * Return an array of fields which can be exported. + * + * @return array + */ + public function getExportableValues() { + return array( + 'id', + 'guid_one', + 'relationship', + 'guid_two' + ); + } + + /** + * Export this relationship + * + * @return array + */ + public function export() { + $uuid = get_uuid_from_object($this); + $relationship = new ODDRelationship( + guid_to_uuid($this->guid_one), + $this->relationship, + guid_to_uuid($this->guid_two) + ); + + $relationship->setAttribute('uuid', $uuid); + + return $relationship; + } + + // IMPORTABLE INTERFACE //////////////////////////////////////////////////////////// + + /** + * Import a relationship + * + * @param ODD $data ODD data + + * @return bool + * @throws ImportException|InvalidParameterException + */ + public function import(ODD $data) { + if (!($data instanceof ODDRelationship)) { + throw new InvalidParameterException(elgg_echo('InvalidParameterException:UnexpectedODDClass')); + } + + $uuid_one = $data->getAttribute('uuid1'); + $uuid_two = $data->getAttribute('uuid2'); + + // See if this entity has already been imported, if so then we need to link to it + $entity1 = get_entity_from_uuid($uuid_one); + $entity2 = get_entity_from_uuid($uuid_two); + if (($entity1) && ($entity2)) { + // Set the item ID + $this->attributes['guid_one'] = $entity1->getGUID(); + $this->attributes['guid_two'] = $entity2->getGUID(); + + // Map verb to relationship + //$verb = $data->getAttribute('verb'); + //$relationship = get_relationship_from_verb($verb); + $relationship = $data->getAttribute('type'); + + if ($relationship) { + $this->attributes['relationship'] = $relationship; + // save + $result = $this->save(); + if (!$result) { + throw new ImportException(elgg_echo('ImportException:ProblemSaving', array(get_class()))); + } + + return true; + } + } + + return false; + } + + // SYSTEM LOG INTERFACE //////////////////////////////////////////////////////////// + + /** + * Return an identification for the object for storage in the system log. + * This id must be an integer. + * + * @return int + */ + public function getSystemLogID() { + return $this->id; + } + + /** + * For a given ID, return the object associated with it. + * This is used by the river functionality primarily. + * This is useful for checking access permissions etc on objects. + * + * @param int $id ID + * + * @return ElggRelationship + */ + public function getObjectFromID($id) { + return get_relationship($id); + } + + /** + * Return a type of the object - eg. object, group, user, relationship, metadata, annotation etc + * + * @return string 'relationship' + */ + public function getType() { + return 'relationship'; + } + + /** + * Return a subtype. For metadata & annotations this is the 'name' and for relationship this + * is the relationship type. + * + * @return string + */ + public function getSubtype() { + return $this->relationship; + } + +} diff --git a/engine/classes/ElggRiverItem.php b/engine/classes/ElggRiverItem.php new file mode 100644 index 000000000..d3d09cd91 --- /dev/null +++ b/engine/classes/ElggRiverItem.php @@ -0,0 +1,115 @@ +<?php +/** + * River item class. + * + * @package Elgg.Core + * @subpackage Core + * + * @property int $id The unique identifier (read-only) + * @property int $subject_guid The GUID of the actor + * @property int $object_guid The GUID of the object + * @property int $annotation_id The ID of the annotation involved in the action + * @property string $type The type of one of the entities involved in the action + * @property string $subtype The subtype of one of the entities involved in the action + * @property string $action_type The name of the action + * @property string $view The view for displaying this river item + * @property int $access_id The visibility of the river item + * @property int $posted UNIX timestamp when the action occurred + */ +class ElggRiverItem { + public $id; + public $subject_guid; + public $object_guid; + public $annotation_id; + public $type; + public $subtype; + public $action_type; + public $access_id; + public $view; + public $posted; + + /** + * Construct a river item object given a database row. + * + * @param stdClass $object Object obtained from database + */ + function __construct($object) { + if (!($object instanceof stdClass)) { + // throw exception + } + + // the casting is to support typed serialization like json + $int_types = array('id', 'subject_guid', 'object_guid', 'annotation_id', 'access_id', 'posted'); + foreach ($object as $key => $value) { + if (in_array($key, $int_types)) { + $this->$key = (int)$value; + } else { + $this->$key = $value; + } + } + } + + /** + * Get the subject of this river item + * + * @return ElggEntity + */ + public function getSubjectEntity() { + return get_entity($this->subject_guid); + } + + /** + * Get the object of this river item + * + * @return ElggEntity + */ + public function getObjectEntity() { + return get_entity($this->object_guid); + } + + /** + * Get the Annotation for this river item + * + * @return ElggAnnotation + */ + public function getAnnotation() { + return elgg_get_annotation_from_id($this->annotation_id); + } + + /** + * Get the view used to display this river item + * + * @return string + */ + public function getView() { + return $this->view; + } + + /** + * Get the time this activity was posted + * + * @return int + */ + public function getPostedTime() { + return (int)$this->posted; + } + + /** + * Get the type of the object + * + * @return string 'river' + */ + public function getType() { + return 'river'; + } + + /** + * Get the subtype of the object + * + * @return string 'item' + */ + public function getSubtype() { + return 'item'; + } + +} diff --git a/engine/classes/ElggSession.php b/engine/classes/ElggSession.php new file mode 100644 index 000000000..9750f063e --- /dev/null +++ b/engine/classes/ElggSession.php @@ -0,0 +1,153 @@ +<?php +/** + * Magic session class. + * This class is intended to extend the $_SESSION magic variable by providing an API hook + * to plug in other values. + * + * Primarily this is intended to provide a way of supplying "logged in user" + * details without touching the session (which can cause problems when + * accessed server side). + * + * If a value is present in the session then that value is returned, otherwise + * a plugin hook 'session:get', '$var' is called, where $var is the variable + * being requested. + * + * Setting values will store variables in the session in the normal way. + * + * LIMITATIONS: You can not access multidimensional arrays + * + * @package Elgg.Core + * @subpackage Sessions + */ +class ElggSession implements ArrayAccess { + /** Local cache of trigger retrieved variables */ + private static $__localcache; + + /** + * Test if property is set either as an attribute or metadata. + * + * @param string $key The name of the attribute or metadata. + * + * @return bool + */ + function __isset($key) { + return $this->offsetExists($key); + } + + /** + * Set a value, go straight to session. + * + * @param string $key Name + * @param mixed $value Value + * + * @return void + */ + function offsetSet($key, $value) { + $_SESSION[$key] = $value; + } + + /** + * Get a variable from either the session, or if its not in the session + * attempt to get it from an api call. + * + * @see ArrayAccess::offsetGet() + * + * @param mixed $key Name + * + * @return mixed + */ + function offsetGet($key) { + if (!ElggSession::$__localcache) { + ElggSession::$__localcache = array(); + } + + if (isset($_SESSION[$key])) { + return $_SESSION[$key]; + } + + if (isset(ElggSession::$__localcache[$key])) { + return ElggSession::$__localcache[$key]; + } + + $value = NULL; + $value = elgg_trigger_plugin_hook('session:get', $key, NULL, $value); + + ElggSession::$__localcache[$key] = $value; + + return ElggSession::$__localcache[$key]; + } + + /** + * Unset a value from the cache and the session. + * + * @see ArrayAccess::offsetUnset() + * + * @param mixed $key Name + * + * @return void + */ + function offsetUnset($key) { + unset(ElggSession::$__localcache[$key]); + unset($_SESSION[$key]); + } + + /** + * Return whether the value is set in either the session or the cache. + * + * @see ArrayAccess::offsetExists() + * + * @param int $offset Offset + * + * @return bool + */ + function offsetExists($offset) { + if (isset(ElggSession::$__localcache[$offset])) { + return true; + } + + if (isset($_SESSION[$offset])) { + return true; + } + + if ($this->offsetGet($offset)) { + return true; + } + + return false; + } + + + /** + * Alias to ::offsetGet() + * + * @param string $key Name + * + * @return mixed + */ + function get($key) { + return $this->offsetGet($key); + } + + /** + * Alias to ::offsetSet() + * + * @param string $key Name + * @param mixed $value Value + * + * @return void + */ + function set($key, $value) { + $this->offsetSet($key, $value); + } + + /** + * Alias to offsetUnset() + * + * @param string $key Name + * + * @return void + */ + function del($key) { + $this->offsetUnset($key); + } +} diff --git a/engine/classes/ElggSharedMemoryCache.php b/engine/classes/ElggSharedMemoryCache.php new file mode 100644 index 000000000..f5f11d2c7 --- /dev/null +++ b/engine/classes/ElggSharedMemoryCache.php @@ -0,0 +1,40 @@ +<?php +/** + * Shared memory cache description. + * Extends ElggCache with functions useful to shared memory + * style caches (static variables, memcache etc) + * + * @package Elgg.Core + * @subpackage Cache + */ +abstract class ElggSharedMemoryCache extends ElggCache { + /** + * Namespace variable used to keep various bits of the cache + * separate. + * + * @var string + */ + private $namespace; + + /** + * Set the namespace of this cache. + * This is useful for cache types (like memcache or static variables) where there is one large + * flat area of memory shared across all instances of the cache. + * + * @param string $namespace Namespace for cache + * + * @return void + */ + public function setNamespace($namespace = "default") { + $this->namespace = $namespace; + } + + /** + * Get the namespace currently defined. + * + * @return string + */ + public function getNamespace() { + return $this->namespace; + } +} diff --git a/engine/classes/ElggSite.php b/engine/classes/ElggSite.php new file mode 100644 index 000000000..dd996fe98 --- /dev/null +++ b/engine/classes/ElggSite.php @@ -0,0 +1,455 @@ +<?php +/** + * A Site entity. + * + * ElggSite represents a single site entity. + * + * An ElggSite object is an ElggEntity child class with the subtype + * of "site." It is created upon installation and hold all the + * information about a site: + * - name + * - description + * - url + * + * Every ElggEntity (except ElggSite) belongs to a site. + * + * @internal ElggSite represents a single row from the sites_entity + * table, as well as the corresponding ElggEntity row from the entities table. + * + * @warning Multiple site support isn't fully developed. + * + * @package Elgg.Core + * @subpackage DataMode.Site + * @link http://docs.elgg.org/DataModel/Sites + * + * @property string $name The name or title of the website + * @property string $description A motto, mission statement, or description of the website + * @property string $url The root web address for the site, including trailing slash + */ +class ElggSite extends ElggEntity { + + /** + * Initialise the attributes array. + * This is vital to distinguish between metadata and base parameters. + * + * Place your base parameters here. + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['type'] = "site"; + $this->attributes['name'] = NULL; + $this->attributes['description'] = NULL; + $this->attributes['url'] = NULL; + $this->attributes['tables_split'] = 2; + } + + /** + * Load or create a new ElggSite. + * + * If no arguments are passed, create a new entity. + * + * If an argument is passed attempt to load a full Site entity. Arguments + * can be: + * - The GUID of a site entity. + * - A URL as stored in ElggSite->url + * - A DB result object with a guid property + * + * @param mixed $guid If an int, load that GUID. If a db row then will + * load the rest of the data. + * + * @throws IOException If passed an incorrect guid + * @throws InvalidParameterException If passed an Elgg* Entity that isn't an ElggSite + */ + function __construct($guid = null) { + $this->initializeAttributes(); + + // compatibility for 1.7 api. + $this->initialise_attributes(false); + + if (!empty($guid)) { + // Is $guid is a DB entity table row + if ($guid instanceof stdClass) { + // Load the rest + if (!$this->load($guid)) { + $msg = elgg_echo('IOException:FailedToLoadGUID', array(get_class(), $guid->guid)); + throw new IOException($msg); + } + } else if ($guid instanceof ElggSite) { + // $guid is an ElggSite so this is a copy constructor + elgg_deprecated_notice('This type of usage of the ElggSite constructor was deprecated. Please use the clone method.', 1.7); + + foreach ($guid->attributes as $key => $value) { + $this->attributes[$key] = $value; + } + } else if ($guid instanceof ElggEntity) { + // @todo remove and just use else clause + throw new InvalidParameterException(elgg_echo('InvalidParameterException:NonElggSite')); + } else if (strpos($guid, "http") !== false) { + // url so retrieve by url + $guid = get_site_by_url($guid); + foreach ($guid->attributes as $key => $value) { + $this->attributes[$key] = $value; + } + } else if (is_numeric($guid)) { + // $guid is a GUID so load + if (!$this->load($guid)) { + throw new IOException(elgg_echo('IOException:FailedToLoadGUID', array(get_class(), $guid))); + } + } else { + throw new InvalidParameterException(elgg_echo('InvalidParameterException:UnrecognisedValue')); + } + } + } + + /** + * Loads the full ElggSite when given a guid. + * + * @param mixed $guid GUID of ElggSite entity or database row object + * + * @return bool + * @throws InvalidClassException + */ + protected function load($guid) { + $attr_loader = new ElggAttributeLoader(get_class(), 'site', $this->attributes); + $attr_loader->requires_access_control = !($this instanceof ElggPlugin); + $attr_loader->secondary_loader = 'get_site_entity_as_row'; + + $attrs = $attr_loader->getRequiredAttributes($guid); + if (!$attrs) { + return false; + } + + $this->attributes = $attrs; + $this->attributes['tables_loaded'] = 2; + _elgg_cache_entity($this); + + return true; + } + + /** + * Saves site-specific attributes. + * + * @internal Site attributes are saved in the sites_entity table. + * + * @return bool + */ + public function save() { + global $CONFIG; + + // Save generic stuff + if (!parent::save()) { + return false; + } + + // make sure the site guid is set (if not, set to self) + if (!$this->get('site_guid')) { + $guid = $this->get('guid'); + update_data("UPDATE {$CONFIG->dbprefix}entities SET site_guid=$guid + WHERE guid=$guid"); + } + + // Now save specific stuff + return create_site_entity($this->get('guid'), $this->get('name'), + $this->get('description'), $this->get('url')); + } + + /** + * Delete the site. + * + * @note You cannot delete the current site. + * + * @return bool + * @throws SecurityException + */ + public function delete() { + global $CONFIG; + if ($CONFIG->site->getGUID() == $this->guid) { + throw new SecurityException('SecurityException:deletedisablecurrentsite'); + } + + return parent::delete(); + } + + /** + * Disable the site + * + * @note You cannot disable the current site. + * + * @param string $reason Optional reason for disabling + * @param bool $recursive Recursively disable all contained entities? + * + * @return bool + * @throws SecurityException + */ + public function disable($reason = "", $recursive = true) { + global $CONFIG; + + if ($CONFIG->site->getGUID() == $this->guid) { + throw new SecurityException('SecurityException:deletedisablecurrentsite'); + } + + return parent::disable($reason, $recursive); + } + + /** + * Gets an array of ElggUser entities who are members of the site. + * + * @param array $options An associative array for key => value parameters + * accepted by elgg_get_entities(). Common parameters + * include 'limit', and 'offset'. + * Note: this was $limit before version 1.8 + * @param int $offset Offset @deprecated parameter + * + * @todo remove $offset in 2.0 + * + * @return array of ElggUsers + */ + public function getMembers($options = array(), $offset = 0) { + if (!is_array($options)) { + elgg_deprecated_notice("ElggSite::getMembers uses different arguments!", 1.8); + $options = array( + 'limit' => $options, + 'offset' => $offset, + ); + } + + $defaults = array( + 'site_guids' => ELGG_ENTITIES_ANY_VALUE, + 'relationship' => 'member_of_site', + 'relationship_guid' => $this->getGUID(), + 'inverse_relationship' => TRUE, + 'type' => 'user', + ); + + $options = array_merge($defaults, $options); + + return elgg_get_entities_from_relationship($options); + } + + /** + * List the members of this site + * + * @param array $options An associative array for key => value parameters + * accepted by elgg_list_entities(). Common parameters + * include 'full_view', 'limit', and 'offset'. + * + * @return string + * @since 1.8.0 + */ + public function listMembers($options = array()) { + $defaults = array( + 'site_guids' => ELGG_ENTITIES_ANY_VALUE, + 'relationship' => 'member_of_site', + 'relationship_guid' => $this->getGUID(), + 'inverse_relationship' => TRUE, + 'type' => 'user', + ); + + $options = array_merge($defaults, $options); + + return elgg_list_entities_from_relationship($options); + } + + /** + * Adds a user to the site. + * + * @param int $user_guid GUID + * + * @return bool + */ + public function addUser($user_guid) { + return add_site_user($this->getGUID(), $user_guid); + } + + /** + * Removes a user from the site. + * + * @param int $user_guid GUID + * + * @return bool + */ + public function removeUser($user_guid) { + return remove_site_user($this->getGUID(), $user_guid); + } + + /** + * Returns an array of ElggObject entities that belong to the site. + * + * @warning This only returns objects that have been explicitly added to the + * site through addObject() + * + * @param string $subtype Entity subtype + * @param int $limit Limit + * @param int $offset Offset + * + * @return array + */ + public function getObjects($subtype = "", $limit = 10, $offset = 0) { + return get_site_objects($this->getGUID(), $subtype, $limit, $offset); + } + + /** + * Adds an object to the site. + * + * @param int $object_guid GUID + * + * @return bool + */ + public function addObject($object_guid) { + return add_site_object($this->getGUID(), $object_guid); + } + + /** + * Remvoes an object from the site. + * + * @param int $object_guid GUID + * + * @return bool + */ + public function removeObject($object_guid) { + return remove_site_object($this->getGUID(), $object_guid); + } + + /** + * Get the collections associated with a site. + * + * @param string $subtype Subtype + * @param int $limit Limit + * @param int $offset Offset + * + * @return unknown + * @deprecated 1.8 Was never implemented + */ + public function getCollections($subtype = "", $limit = 10, $offset = 0) { + elgg_deprecated_notice("ElggSite::getCollections() is deprecated", 1.8); + get_site_collections($this->getGUID(), $subtype, $limit, $offset); + } + + /* + * EXPORTABLE INTERFACE + */ + + /** + * Return an array of fields which can be exported. + * + * @return array + */ + public function getExportableValues() { + return array_merge(parent::getExportableValues(), array( + 'name', + 'description', + 'url', + )); + } + + /** + * Halts bootup and redirects to the site front page + * if site is in walled garden mode, no user is logged in, + * and the URL is not a public page. + * + * @link http://docs.elgg.org/Tutorials/WalledGarden + * + * @return void + * @since 1.8.0 + */ + public function checkWalledGarden() { + global $CONFIG; + + // command line calls should not invoke the walled garden check + if (PHP_SAPI === 'cli') { + return; + } + + if ($CONFIG->walled_garden) { + if ($CONFIG->default_access == ACCESS_PUBLIC) { + $CONFIG->default_access = ACCESS_LOGGED_IN; + } + elgg_register_plugin_hook_handler( + 'access:collections:write', + 'user', + '_elgg_walled_garden_remove_public_access'); + + if (!elgg_is_logged_in()) { + // hook into the index system call at the highest priority + elgg_register_plugin_hook_handler('index', 'system', 'elgg_walled_garden_index', 1); + + if (!$this->isPublicPage()) { + if (!elgg_is_xhr()) { + $_SESSION['last_forward_from'] = current_page_url(); + } + register_error(elgg_echo('loggedinrequired')); + forward(); + } + } + } + } + + /** + * Returns if a URL is public for this site when in Walled Garden mode. + * + * Pages are registered to be public by {@elgg_plugin_hook public_pages walled_garden}. + * + * @param string $url Defaults to the current URL. + * + * @return bool + * @since 1.8.0 + */ + public function isPublicPage($url = '') { + global $CONFIG; + + if (empty($url)) { + $url = current_page_url(); + + // do not check against URL queries + if ($pos = strpos($url, '?')) { + $url = substr($url, 0, $pos); + } + } + + // always allow index page + if ($url == elgg_get_site_url($this->guid)) { + return TRUE; + } + + // default public pages + $defaults = array( + 'walled_garden/.*', + 'login', + 'action/login', + 'register', + 'action/register', + 'forgotpassword', + 'resetpassword', + 'action/user/requestnewpassword', + 'action/user/passwordreset', + 'action/security/refreshtoken', + 'ajax/view/js/languages', + 'upgrade\.php', + 'xml-rpc\.php', + 'mt/mt-xmlrpc\.cgi', + 'css/.*', + 'js/.*', + 'cache/css/.*', + 'cache/js/.*', + 'cron/.*', + 'services/.*', + ); + + // include a hook for plugin authors to include public pages + $plugins = elgg_trigger_plugin_hook('public_pages', 'walled_garden', NULL, array()); + + // allow public pages + foreach (array_merge($defaults, $plugins) as $public) { + $pattern = "`^{$CONFIG->url}$public/*$`i"; + if (preg_match($pattern, $url)) { + return TRUE; + } + } + + // non-public page + return FALSE; + } +} diff --git a/engine/classes/ElggStaticVariableCache.php b/engine/classes/ElggStaticVariableCache.php new file mode 100644 index 000000000..9c14fdfba --- /dev/null +++ b/engine/classes/ElggStaticVariableCache.php @@ -0,0 +1,96 @@ +<?php +/** + * ElggStaticVariableCache + * Dummy cache which stores values in a static array. Using this makes future + * replacements to other caching back ends (eg memcache) much easier. + * + * @package Elgg.Core + * @subpackage Cache + */ +class ElggStaticVariableCache extends ElggSharedMemoryCache { + /** + * The cache. + * + * @var array + */ + private static $__cache; + + /** + * Create the variable cache. + * + * This function creates a variable cache in a static variable in + * memory, optionally with a given namespace (to avoid overlap). + * + * @param string $namespace The namespace for this cache to write to. + * @warning namespaces of the same name are shared! + */ + function __construct($namespace = 'default') { + $this->setNamespace($namespace); + $this->clear(); + } + + /** + * Save a key + * + * @param string $key Name + * @param string $data Value + * + * @return boolean + */ + public function save($key, $data) { + $namespace = $this->getNamespace(); + + ElggStaticVariableCache::$__cache[$namespace][$key] = $data; + + return true; + } + + /** + * Load a key + * + * @param string $key Name + * @param int $offset Offset + * @param int $limit Limit + * + * @return string + */ + public function load($key, $offset = 0, $limit = null) { + $namespace = $this->getNamespace(); + + if (isset(ElggStaticVariableCache::$__cache[$namespace][$key])) { + return ElggStaticVariableCache::$__cache[$namespace][$key]; + } + + return false; + } + + /** + * Invalidate a given key. + * + * @param string $key Name + * + * @return bool + */ + public function delete($key) { + $namespace = $this->getNamespace(); + + unset(ElggStaticVariableCache::$__cache[$namespace][$key]); + + return true; + } + + /** + * Clears the cache for a particular namespace + * + * @return void + */ + public function clear() { + $namespace = $this->getNamespace(); + + if (!isset(ElggStaticVariableCache::$__cache)) { + ElggStaticVariableCache::$__cache = array(); + } + + ElggStaticVariableCache::$__cache[$namespace] = array(); + } +} diff --git a/engine/classes/ElggTranslit.php b/engine/classes/ElggTranslit.php new file mode 100644 index 000000000..b4bf87797 --- /dev/null +++ b/engine/classes/ElggTranslit.php @@ -0,0 +1,269 @@ +<?php +/** + * Elgg Transliterate + * + * For creating "friendly titles" for URLs + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * This software consists of voluntary contributions made by many individuals + * and is licensed under the LGPL. For more information, see + * <http://www.doctrine-project.org>. + * + * @package Elgg.Core + * @author Konsta Vesterinen <kvesteri@cc.hut.fi> + * @author Jonathan H. Wage <jonwage@gmail.com> + * @author Steve Clay <steve@mrclay.org> + * + * @access private Plugin authors should not use this directly + */ +class ElggTranslit { + + /** + * Create a version of a string for embedding in a URL + * + * @param string $string A UTF-8 string + * @param string $separator The character to separate words with + * @return string + */ + static public function urlize($string, $separator = '-') { + // Iñtërnâtiônàlizætiøn, AND 日本語! + + // try to force combined chars because the translit map and others expect it + if (self::hasNormalizerSupport()) { + $nfc = normalizer_normalize($string); + if (is_string($nfc)) { + $string = $nfc; + } + } + // Internationalization, AND 日本語! + $string = self::transliterateAscii($string); + + // allow HTML tags in titles + $string = preg_replace('~<([a-zA-Z][^>]*)>~', ' $1 ', $string); + + // more substitutions + // @todo put these somewhere else + $string = strtr($string, array( + // currency + "\xE2\x82\xAC" /* € */ => ' E ', + "\xC2\xA3" /* £ */ => ' GBP ', + )); + + // remove all ASCII except 0-9a-zA-Z, hyphen, underscore, and whitespace + // note: "x" modifier did not work with this pattern. + $string = preg_replace('~[' + . '\x00-\x08' // control chars + . '\x0b\x0c' // vert tab, form feed + . '\x0e-\x1f' // control chars + . '\x21-\x2c' // ! ... , + . '\x2e\x2f' // . slash + . '\x3a-\x40' // : ... @ + . '\x5b-\x5e' // [ ... ^ + . '\x60' // ` + . '\x7b-\x7f' // { ... DEL + . ']~', '', $string); + $string = strtr($string, '', ''); + + // internationalization, and 日本語! + // note: not using elgg_strtolower to keep this class portable + $string = is_callable('mb_strtolower') + ? mb_strtolower($string, 'UTF-8') + : strtolower($string); + + // split by ASCII chars not in 0-9a-zA-Z + // note: we cannot use [^0-9a-zA-Z] because that matches multibyte chars. + // note: "x" modifier did not work with this pattern. + $pattern = '~[' + . '\x00-\x2f' // controls ... slash + . '\x3a-\x40' // : ... @ + . '\x5b-\x60' // [ ... ` + . '\x7b-\x7f' // { ... DEL + . ']+~x'; + + // ['internationalization', 'and', '日本語'] + $words = preg_split($pattern, $string, -1, PREG_SPLIT_NO_EMPTY); + + // ['internationalization', 'and', '%E6%97%A5%E6%9C%AC%E8%AA%9E'] + $words = array_map('urlencode', $words); + + // internationalization-and-%E6%97%A5%E6%9C%AC%E8%AA%9E + return implode($separator, $words); + } + + /** + * Transliterate Western multibyte chars to ASCII + * + * @param string $utf8 a UTF-8 string + * @return string + */ + static public function transliterateAscii($utf8) { + static $map = null; + if (!preg_match('/[\x80-\xff]/', $utf8)) { + return $utf8; + } + if (null === $map) { + $map = self::getAsciiTranslitMap(); + } + return strtr($utf8, $map); + } + + /** + * Get array of UTF-8 (NFC) character replacements. + * + * @return array + */ + static public function getAsciiTranslitMap() { + return array( + // Decompositions for Latin-1 Supplement + "\xC2\xAA" /* ª */ => 'a', "\xC2\xBA" /* º */ => 'o', "\xC3\x80" /* À */ => 'A', + "\xC3\x81" /* Á */ => 'A', "\xC3\x82" /*  */ => 'A', "\xC3\x83" /* à */ => 'A', + "\xC3\x84" /* Ä */ => 'A', "\xC3\x85" /* Å */ => 'A', "\xC3\x86" /* Æ */ => 'AE', + "\xC3\x87" /* Ç */ => 'C', "\xC3\x88" /* È */ => 'E', "\xC3\x89" /* É */ => 'E', + "\xC3\x8A" /* Ê */ => 'E', "\xC3\x8B" /* Ë */ => 'E', "\xC3\x8C" /* Ì */ => 'I', + "\xC3\x8D" /* Í */ => 'I', "\xC3\x8E" /* Î */ => 'I', "\xC3\x8F" /* Ï */ => 'I', + "\xC3\x90" /* Ð */ => 'D', "\xC3\x91" /* Ñ */ => 'N', "\xC3\x92" /* Ò */ => 'O', + "\xC3\x93" /* Ó */ => 'O', "\xC3\x94" /* Ô */ => 'O', "\xC3\x95" /* Õ */ => 'O', + "\xC3\x96" /* Ö */ => 'O', "\xC3\x99" /* Ù */ => 'U', "\xC3\x9A" /* Ú */ => 'U', + "\xC3\x9B" /* Û */ => 'U', "\xC3\x9C" /* Ü */ => 'U', "\xC3\x9D" /* Ý */ => 'Y', + "\xC3\x9E" /* Þ */ => 'TH', "\xC3\x9F" /* ß */ => 'ss', "\xC3\xA0" /* à */ => 'a', + "\xC3\xA1" /* á */ => 'a', "\xC3\xA2" /* â */ => 'a', "\xC3\xA3" /* ã */ => 'a', + "\xC3\xA4" /* ä */ => 'a', "\xC3\xA5" /* å */ => 'a', "\xC3\xA6" /* æ */ => 'ae', + "\xC3\xA7" /* ç */ => 'c', "\xC3\xA8" /* è */ => 'e', "\xC3\xA9" /* é */ => 'e', + "\xC3\xAA" /* ê */ => 'e', "\xC3\xAB" /* ë */ => 'e', "\xC3\xAC" /* ì */ => 'i', + "\xC3\xAD" /* í */ => 'i', "\xC3\xAE" /* î */ => 'i', "\xC3\xAF" /* ï */ => 'i', + "\xC3\xB0" /* ð */ => 'd', "\xC3\xB1" /* ñ */ => 'n', "\xC3\xB2" /* ò */ => 'o', + "\xC3\xB3" /* ó */ => 'o', "\xC3\xB4" /* ô */ => 'o', "\xC3\xB5" /* õ */ => 'o', + "\xC3\xB6" /* ö */ => 'o', "\xC3\xB8" /* ø */ => 'o', "\xC3\xB9" /* ù */ => 'u', + "\xC3\xBA" /* ú */ => 'u', "\xC3\xBB" /* û */ => 'u', "\xC3\xBC" /* ü */ => 'u', + "\xC3\xBD" /* ý */ => 'y', "\xC3\xBE" /* þ */ => 'th', "\xC3\xBF" /* ÿ */ => 'y', + "\xC3\x98" /* Ø */ => 'O', + // Decompositions for Latin Extended-A + "\xC4\x80" /* Ā */ => 'A', "\xC4\x81" /* ā */ => 'a', "\xC4\x82" /* Ă */ => 'A', + "\xC4\x83" /* ă */ => 'a', "\xC4\x84" /* Ą */ => 'A', "\xC4\x85" /* ą */ => 'a', + "\xC4\x86" /* Ć */ => 'C', "\xC4\x87" /* ć */ => 'c', "\xC4\x88" /* Ĉ */ => 'C', + "\xC4\x89" /* ĉ */ => 'c', "\xC4\x8A" /* Ċ */ => 'C', "\xC4\x8B" /* ċ */ => 'c', + "\xC4\x8C" /* Č */ => 'C', "\xC4\x8D" /* č */ => 'c', "\xC4\x8E" /* Ď */ => 'D', + "\xC4\x8F" /* ď */ => 'd', "\xC4\x90" /* Đ */ => 'D', "\xC4\x91" /* đ */ => 'd', + "\xC4\x92" /* Ē */ => 'E', "\xC4\x93" /* ē */ => 'e', "\xC4\x94" /* Ĕ */ => 'E', + "\xC4\x95" /* ĕ */ => 'e', "\xC4\x96" /* Ė */ => 'E', "\xC4\x97" /* ė */ => 'e', + "\xC4\x98" /* Ę */ => 'E', "\xC4\x99" /* ę */ => 'e', "\xC4\x9A" /* Ě */ => 'E', + "\xC4\x9B" /* ě */ => 'e', "\xC4\x9C" /* Ĝ */ => 'G', "\xC4\x9D" /* ĝ */ => 'g', + "\xC4\x9E" /* Ğ */ => 'G', "\xC4\x9F" /* ğ */ => 'g', "\xC4\xA0" /* Ġ */ => 'G', + "\xC4\xA1" /* ġ */ => 'g', "\xC4\xA2" /* Ģ */ => 'G', "\xC4\xA3" /* ģ */ => 'g', + "\xC4\xA4" /* Ĥ */ => 'H', "\xC4\xA5" /* ĥ */ => 'h', "\xC4\xA6" /* Ħ */ => 'H', + "\xC4\xA7" /* ħ */ => 'h', "\xC4\xA8" /* Ĩ */ => 'I', "\xC4\xA9" /* ĩ */ => 'i', + "\xC4\xAA" /* Ī */ => 'I', "\xC4\xAB" /* ī */ => 'i', "\xC4\xAC" /* Ĭ */ => 'I', + "\xC4\xAD" /* ĭ */ => 'i', "\xC4\xAE" /* Į */ => 'I', "\xC4\xAF" /* į */ => 'i', + "\xC4\xB0" /* İ */ => 'I', "\xC4\xB1" /* ı */ => 'i', "\xC4\xB2" /* IJ */ => 'IJ', + "\xC4\xB3" /* ij */ => 'ij', "\xC4\xB4" /* Ĵ */ => 'J', "\xC4\xB5" /* ĵ */ => 'j', + "\xC4\xB6" /* Ķ */ => 'K', "\xC4\xB7" /* ķ */ => 'k', "\xC4\xB8" /* ĸ */ => 'k', + "\xC4\xB9" /* Ĺ */ => 'L', "\xC4\xBA" /* ĺ */ => 'l', "\xC4\xBB" /* Ļ */ => 'L', + "\xC4\xBC" /* ļ */ => 'l', "\xC4\xBD" /* Ľ */ => 'L', "\xC4\xBE" /* ľ */ => 'l', + "\xC4\xBF" /* Ŀ */ => 'L', "\xC5\x80" /* ŀ */ => 'l', "\xC5\x81" /* Ł */ => 'L', + "\xC5\x82" /* ł */ => 'l', "\xC5\x83" /* Ń */ => 'N', "\xC5\x84" /* ń */ => 'n', + "\xC5\x85" /* Ņ */ => 'N', "\xC5\x86" /* ņ */ => 'n', "\xC5\x87" /* Ň */ => 'N', + "\xC5\x88" /* ň */ => 'n', "\xC5\x89" /* ʼn */ => 'N', "\xC5\x8A" /* Ŋ */ => 'n', + "\xC5\x8B" /* ŋ */ => 'N', "\xC5\x8C" /* Ō */ => 'O', "\xC5\x8D" /* ō */ => 'o', + "\xC5\x8E" /* Ŏ */ => 'O', "\xC5\x8F" /* ŏ */ => 'o', "\xC5\x90" /* Ő */ => 'O', + "\xC5\x91" /* ő */ => 'o', "\xC5\x92" /* Œ */ => 'OE', "\xC5\x93" /* œ */ => 'oe', + "\xC5\x94" /* Ŕ */ => 'R', "\xC5\x95" /* ŕ */ => 'r', "\xC5\x96" /* Ŗ */ => 'R', + "\xC5\x97" /* ŗ */ => 'r', "\xC5\x98" /* Ř */ => 'R', "\xC5\x99" /* ř */ => 'r', + "\xC5\x9A" /* Ś */ => 'S', "\xC5\x9B" /* ś */ => 's', "\xC5\x9C" /* Ŝ */ => 'S', + "\xC5\x9D" /* ŝ */ => 's', "\xC5\x9E" /* Ş */ => 'S', "\xC5\x9F" /* ş */ => 's', + "\xC5\xA0" /* Š */ => 'S', "\xC5\xA1" /* š */ => 's', "\xC5\xA2" /* Ţ */ => 'T', + "\xC5\xA3" /* ţ */ => 't', "\xC5\xA4" /* Ť */ => 'T', "\xC5\xA5" /* ť */ => 't', + "\xC5\xA6" /* Ŧ */ => 'T', "\xC5\xA7" /* ŧ */ => 't', "\xC5\xA8" /* Ũ */ => 'U', + "\xC5\xA9" /* ũ */ => 'u', "\xC5\xAA" /* Ū */ => 'U', "\xC5\xAB" /* ū */ => 'u', + "\xC5\xAC" /* Ŭ */ => 'U', "\xC5\xAD" /* ŭ */ => 'u', "\xC5\xAE" /* Ů */ => 'U', + "\xC5\xAF" /* ů */ => 'u', "\xC5\xB0" /* Ű */ => 'U', "\xC5\xB1" /* ű */ => 'u', + "\xC5\xB2" /* Ų */ => 'U', "\xC5\xB3" /* ų */ => 'u', "\xC5\xB4" /* Ŵ */ => 'W', + "\xC5\xB5" /* ŵ */ => 'w', "\xC5\xB6" /* Ŷ */ => 'Y', "\xC5\xB7" /* ŷ */ => 'y', + "\xC5\xB8" /* Ÿ */ => 'Y', "\xC5\xB9" /* Ź */ => 'Z', "\xC5\xBA" /* ź */ => 'z', + "\xC5\xBB" /* Ż */ => 'Z', "\xC5\xBC" /* ż */ => 'z', "\xC5\xBD" /* Ž */ => 'Z', + "\xC5\xBE" /* ž */ => 'z', "\xC5\xBF" /* ſ */ => 's', + // Decompositions for Latin Extended-B + "\xC8\x98" /* Ș */ => 'S', "\xC8\x99" /* ș */ => 's', + "\xC8\x9A" /* Ț */ => 'T', "\xC8\x9B" /* ț */ => 't', + // unmarked + "\xC6\xA0" /* Ơ */ => 'O', "\xC6\xA1" /* ơ */ => 'o', + "\xC6\xAF" /* Ư */ => 'U', "\xC6\xB0" /* ư */ => 'u', + // grave accent + "\xE1\xBA\xA6" /* Ầ */ => 'A', "\xE1\xBA\xA7" /* ầ */ => 'a', + "\xE1\xBA\xB0" /* Ằ */ => 'A', "\xE1\xBA\xB1" /* ằ */ => 'a', + "\xE1\xBB\x80" /* Ề */ => 'E', "\xE1\xBB\x81" /* ề */ => 'e', + "\xE1\xBB\x92" /* Ồ */ => 'O', "\xE1\xBB\x93" /* ồ */ => 'o', + "\xE1\xBB\x9C" /* Ờ */ => 'O', "\xE1\xBB\x9D" /* ờ */ => 'o', + "\xE1\xBB\xAA" /* Ừ */ => 'U', "\xE1\xBB\xAB" /* ừ */ => 'u', + "\xE1\xBB\xB2" /* Ỳ */ => 'Y', "\xE1\xBB\xB3" /* ỳ */ => 'y', + // hook + "\xE1\xBA\xA2" /* Ả */ => 'A', "\xE1\xBA\xA3" /* ả */ => 'a', + "\xE1\xBA\xA8" /* Ẩ */ => 'A', "\xE1\xBA\xA9" /* ẩ */ => 'a', + "\xE1\xBA\xB2" /* Ẳ */ => 'A', "\xE1\xBA\xB3" /* ẳ */ => 'a', + "\xE1\xBA\xBA" /* Ẻ */ => 'E', "\xE1\xBA\xBB" /* ẻ */ => 'e', + "\xE1\xBB\x82" /* Ể */ => 'E', "\xE1\xBB\x83" /* ể */ => 'e', + "\xE1\xBB\x88" /* Ỉ */ => 'I', "\xE1\xBB\x89" /* ỉ */ => 'i', + "\xE1\xBB\x8E" /* Ỏ */ => 'O', "\xE1\xBB\x8F" /* ỏ */ => 'o', + "\xE1\xBB\x94" /* Ổ */ => 'O', "\xE1\xBB\x95" /* ổ */ => 'o', + "\xE1\xBB\x9E" /* Ở */ => 'O', "\xE1\xBB\x9F" /* ở */ => 'o', + "\xE1\xBB\xA6" /* Ủ */ => 'U', "\xE1\xBB\xA7" /* ủ */ => 'u', + "\xE1\xBB\xAC" /* Ử */ => 'U', "\xE1\xBB\xAD" /* ử */ => 'u', + "\xE1\xBB\xB6" /* Ỷ */ => 'Y', "\xE1\xBB\xB7" /* ỷ */ => 'y', + // tilde + "\xE1\xBA\xAA" /* Ẫ */ => 'A', "\xE1\xBA\xAB" /* ẫ */ => 'a', + "\xE1\xBA\xB4" /* Ẵ */ => 'A', "\xE1\xBA\xB5" /* ẵ */ => 'a', + "\xE1\xBA\xBC" /* Ẽ */ => 'E', "\xE1\xBA\xBD" /* ẽ */ => 'e', + "\xE1\xBB\x84" /* Ễ */ => 'E', "\xE1\xBB\x85" /* ễ */ => 'e', + "\xE1\xBB\x96" /* Ỗ */ => 'O', "\xE1\xBB\x97" /* ỗ */ => 'o', + "\xE1\xBB\xA0" /* Ỡ */ => 'O', "\xE1\xBB\xA1" /* ỡ */ => 'o', + "\xE1\xBB\xAE" /* Ữ */ => 'U', "\xE1\xBB\xAF" /* ữ */ => 'u', + "\xE1\xBB\xB8" /* Ỹ */ => 'Y', "\xE1\xBB\xB9" /* ỹ */ => 'y', + // acute accent + "\xE1\xBA\xA4" /* Ấ */ => 'A', "\xE1\xBA\xA5" /* ấ */ => 'a', + "\xE1\xBA\xAE" /* Ắ */ => 'A', "\xE1\xBA\xAF" /* ắ */ => 'a', + "\xE1\xBA\xBE" /* Ế */ => 'E', "\xE1\xBA\xBF" /* ế */ => 'e', + "\xE1\xBB\x90" /* Ố */ => 'O', "\xE1\xBB\x91" /* ố */ => 'o', + "\xE1\xBB\x9A" /* Ớ */ => 'O', "\xE1\xBB\x9B" /* ớ */ => 'o', + "\xE1\xBB\xA8" /* Ứ */ => 'U', "\xE1\xBB\xA9" /* ứ */ => 'u', + // dot below + "\xE1\xBA\xA0" /* Ạ */ => 'A', "\xE1\xBA\xA1" /* ạ */ => 'a', + "\xE1\xBA\xAC" /* Ậ */ => 'A', "\xE1\xBA\xAD" /* ậ */ => 'a', + "\xE1\xBA\xB6" /* Ặ */ => 'A', "\xE1\xBA\xB7" /* ặ */ => 'a', + "\xE1\xBA\xB8" /* Ẹ */ => 'E', "\xE1\xBA\xB9" /* ẹ */ => 'e', + "\xE1\xBB\x86" /* Ệ */ => 'E', "\xE1\xBB\x87" /* ệ */ => 'e', + "\xE1\xBB\x8A" /* Ị */ => 'I', "\xE1\xBB\x8B" /* ị */ => 'i', + "\xE1\xBB\x8C" /* Ọ */ => 'O', "\xE1\xBB\x8D" /* ọ */ => 'o', + "\xE1\xBB\x98" /* Ộ */ => 'O', "\xE1\xBB\x99" /* ộ */ => 'o', + "\xE1\xBB\xA2" /* Ợ */ => 'O', "\xE1\xBB\xA3" /* ợ */ => 'o', + "\xE1\xBB\xA4" /* Ụ */ => 'U', "\xE1\xBB\xA5" /* ụ */ => 'u', + "\xE1\xBB\xB0" /* Ự */ => 'U', "\xE1\xBB\xB1" /* ự */ => 'u', + "\xE1\xBB\xB4" /* Ỵ */ => 'Y', "\xE1\xBB\xB5" /* ỵ */ => 'y', + ); + } + + /** + * Tests that "normalizer_normalize" exists and works + * + * @return bool + */ + static public function hasNormalizerSupport() { + static $ret = null; + if (null === $ret) { + $form_c = "\xC3\x85"; // 'LATIN CAPITAL LETTER A WITH RING ABOVE' (U+00C5) + $form_d = "A\xCC\x8A"; // A followed by 'COMBINING RING ABOVE' (U+030A) + $ret = (function_exists('normalizer_normalize') + && $form_c === normalizer_normalize($form_d)); + } + return $ret; + } +} diff --git a/engine/classes/ElggUser.php b/engine/classes/ElggUser.php new file mode 100644 index 000000000..6163f9b62 --- /dev/null +++ b/engine/classes/ElggUser.php @@ -0,0 +1,588 @@ +<?php +/** + * ElggUser + * + * Representation of a "user" in the system. + * + * @package Elgg.Core + * @subpackage DataModel.User + * + * @property string $name The display name that the user will be known by in the network + * @property string $username The short, reference name for the user in the network + * @property string $email The email address to which Elgg will send email notifications + * @property string $language The language preference of the user (ISO 639-1 formatted) + * @property string $banned 'yes' if the user is banned from the network, 'no' otherwise + * @property string $admin 'yes' if the user is an administrator of the network, 'no' otherwise + * @property string $password The hashed password of the user + * @property string $salt The salt used to secure the password before hashing + */ +class ElggUser extends ElggEntity + implements Friendable { + + /** + * Initialise the attributes array. + * This is vital to distinguish between metadata and base parameters. + * + * Place your base parameters here. + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['type'] = "user"; + $this->attributes['name'] = NULL; + $this->attributes['username'] = NULL; + $this->attributes['password'] = NULL; + $this->attributes['salt'] = NULL; + $this->attributes['email'] = NULL; + $this->attributes['language'] = NULL; + $this->attributes['code'] = NULL; + $this->attributes['banned'] = "no"; + $this->attributes['admin'] = 'no'; + $this->attributes['prev_last_action'] = NULL; + $this->attributes['last_login'] = NULL; + $this->attributes['prev_last_login'] = NULL; + $this->attributes['tables_split'] = 2; + } + + /** + * Construct a new user entity, optionally from a given id value. + * + * @param mixed $guid If an int, load that GUID. + * If an entity table db row then will load the rest of the data. + * + * @throws Exception if there was a problem creating the user. + */ + function __construct($guid = null) { + $this->initializeAttributes(); + + // compatibility for 1.7 api. + $this->initialise_attributes(false); + + if (!empty($guid)) { + // Is $guid is a DB entity row + if ($guid instanceof stdClass) { + // Load the rest + if (!$this->load($guid)) { + $msg = elgg_echo('IOException:FailedToLoadGUID', array(get_class(), $guid->guid)); + throw new IOException($msg); + } + } else if (is_string($guid)) { + // $guid is a username + $user = get_user_by_username($guid); + if ($user) { + foreach ($user->attributes as $key => $value) { + $this->attributes[$key] = $value; + } + } + } else if ($guid instanceof ElggUser) { + // $guid is an ElggUser so this is a copy constructor + elgg_deprecated_notice('This type of usage of the ElggUser constructor was deprecated. Please use the clone method.', 1.7); + + foreach ($guid->attributes as $key => $value) { + $this->attributes[$key] = $value; + } + } else if ($guid instanceof ElggEntity) { + // @todo why have a special case here + throw new InvalidParameterException(elgg_echo('InvalidParameterException:NonElggUser')); + } else if (is_numeric($guid)) { + // $guid is a GUID so load entity + if (!$this->load($guid)) { + throw new IOException(elgg_echo('IOException:FailedToLoadGUID', array(get_class(), $guid))); + } + } else { + throw new InvalidParameterException(elgg_echo('InvalidParameterException:UnrecognisedValue')); + } + } + } + + /** + * Load the ElggUser data from the database + * + * @param mixed $guid ElggUser GUID or stdClass database row from entity table + * + * @return bool + */ + protected function load($guid) { + $attr_loader = new ElggAttributeLoader(get_class(), 'user', $this->attributes); + $attr_loader->secondary_loader = 'get_user_entity_as_row'; + + $attrs = $attr_loader->getRequiredAttributes($guid); + if (!$attrs) { + return false; + } + + $this->attributes = $attrs; + $this->attributes['tables_loaded'] = 2; + _elgg_cache_entity($this); + + return true; + } + + /** + * Saves this user to the database. + * + * @return bool + */ + public function save() { + // Save generic stuff + if (!parent::save()) { + return false; + } + + // Now save specific stuff + _elgg_disable_caching_for_entity($this->guid); + $ret = create_user_entity($this->get('guid'), $this->get('name'), $this->get('username'), + $this->get('password'), $this->get('salt'), $this->get('email'), $this->get('language'), + $this->get('code')); + _elgg_enable_caching_for_entity($this->guid); + + return $ret; + } + + /** + * User specific override of the entity delete method. + * + * @return bool + */ + public function delete() { + global $USERNAME_TO_GUID_MAP_CACHE, $CODE_TO_GUID_MAP_CACHE; + + // clear cache + if (isset($USERNAME_TO_GUID_MAP_CACHE[$this->username])) { + unset($USERNAME_TO_GUID_MAP_CACHE[$this->username]); + } + if (isset($CODE_TO_GUID_MAP_CACHE[$this->code])) { + unset($CODE_TO_GUID_MAP_CACHE[$this->code]); + } + + clear_user_files($this); + + // Delete entity + return parent::delete(); + } + + /** + * Ban this user. + * + * @param string $reason Optional reason + * + * @return bool + */ + public function ban($reason = "") { + return ban_user($this->guid, $reason); + } + + /** + * Unban this user. + * + * @return bool + */ + public function unban() { + return unban_user($this->guid); + } + + /** + * Is this user banned or not? + * + * @return bool + */ + public function isBanned() { + return $this->banned == 'yes'; + } + + /** + * Is this user admin? + * + * @return bool + */ + public function isAdmin() { + + // for backward compatibility we need to pull this directly + // from the attributes instead of using the magic methods. + // this can be removed in 1.9 + // return $this->admin == 'yes'; + return $this->attributes['admin'] == 'yes'; + } + + /** + * Make the user an admin + * + * @return bool + */ + public function makeAdmin() { + // If already saved, use the standard function. + if ($this->guid && !make_user_admin($this->guid)) { + return FALSE; + } + + // need to manually set attributes since they've already been loaded. + $this->attributes['admin'] = 'yes'; + + return TRUE; + } + + /** + * Remove the admin flag for user + * + * @return bool + */ + public function removeAdmin() { + // If already saved, use the standard function. + if ($this->guid && !remove_user_admin($this->guid)) { + return FALSE; + } + + // need to manually set attributes since they've already been loaded. + $this->attributes['admin'] = 'no'; + + return TRUE; + } + + /** + * Get sites that this user is a member of + * + * @param string $subtype Optionally, the subtype of result we want to limit to + * @param int $limit The number of results to return + * @param int $offset Any indexing offset + * + * @return array + */ + function getSites($subtype = "", $limit = 10, $offset = 0) { + return get_user_sites($this->getGUID(), $subtype, $limit, $offset); + } + + /** + * Add this user to a particular site + * + * @param int $site_guid The guid of the site to add it to + * + * @return bool + */ + function addToSite($site_guid) { + return add_site_user($site_guid, $this->getGUID()); + } + + /** + * Remove this user from a particular site + * + * @param int $site_guid The guid of the site to remove it from + * + * @return bool + */ + function removeFromSite($site_guid) { + return remove_site_user($site_guid, $this->getGUID()); + } + + /** + * Adds a user as a friend + * + * @param int $friend_guid The GUID of the user to add + * + * @return bool + */ + function addFriend($friend_guid) { + return user_add_friend($this->getGUID(), $friend_guid); + } + + /** + * Removes a user as a friend + * + * @param int $friend_guid The GUID of the user to remove + * + * @return bool + */ + function removeFriend($friend_guid) { + return user_remove_friend($this->getGUID(), $friend_guid); + } + + /** + * Determines whether or not this user is a friend of the currently logged in user + * + * @return bool + */ + function isFriend() { + return $this->isFriendOf(elgg_get_logged_in_user_guid()); + } + + /** + * Determines whether this user is friends with another user + * + * @param int $user_guid The GUID of the user to check against + * + * @return bool + */ + function isFriendsWith($user_guid) { + return user_is_friend($this->getGUID(), $user_guid); + } + + /** + * Determines whether or not this user is another user's friend + * + * @param int $user_guid The GUID of the user to check against + * + * @return bool + */ + function isFriendOf($user_guid) { + return user_is_friend($user_guid, $this->getGUID()); + } + + /** + * Gets this user's friends + * + * @param string $subtype Optionally, the user subtype (leave blank for all) + * @param int $limit The number of users to retrieve + * @param int $offset Indexing offset, if any + * + * @return array|false Array of ElggUser, or false, depending on success + */ + function getFriends($subtype = "", $limit = 10, $offset = 0) { + return get_user_friends($this->getGUID(), $subtype, $limit, $offset); + } + + /** + * Gets users who have made this user a friend + * + * @param string $subtype Optionally, the user subtype (leave blank for all) + * @param int $limit The number of users to retrieve + * @param int $offset Indexing offset, if any + * + * @return array|false Array of ElggUser, or false, depending on success + */ + function getFriendsOf($subtype = "", $limit = 10, $offset = 0) { + return get_user_friends_of($this->getGUID(), $subtype, $limit, $offset); + } + + /** + * Lists the user's friends + * + * @param string $subtype Optionally, the user subtype (leave blank for all) + * @param int $limit The number of users to retrieve + * @param array $vars Display variables for the user view + * + * @return string Rendered list of friends + * @since 1.8.0 + */ + function listFriends($subtype = "", $limit = 10, array $vars = array()) { + $defaults = array( + 'type' => 'user', + 'relationship' => 'friend', + 'relationship_guid' => $this->guid, + 'limit' => $limit, + 'full_view' => false, + ); + + $options = array_merge($defaults, $vars); + + if ($subtype) { + $options['subtype'] = $subtype; + } + + return elgg_list_entities_from_relationship($options); + } + + /** + * Gets the user's groups + * + * @param string $subtype Optionally, the subtype of user to filter to (leave blank for all) + * @param int $limit The number of groups to retrieve + * @param int $offset Indexing offset, if any + * + * @return array|false Array of ElggGroup, or false, depending on success + */ + function getGroups($subtype = "", $limit = 10, $offset = 0) { + $options = array( + 'type' => 'group', + 'relationship' => 'member', + 'relationship_guid' => $this->guid, + 'limit' => $limit, + 'offset' => $offset, + ); + + if ($subtype) { + $options['subtype'] = $subtype; + } + + return elgg_get_entities_from_relationship($options); + } + + /** + * Lists the user's groups + * + * @param string $subtype Optionally, the user subtype (leave blank for all) + * @param int $limit The number of users to retrieve + * @param int $offset Indexing offset, if any + * + * @return string + */ + function listGroups($subtype = "", $limit = 10, $offset = 0) { + $options = array( + 'type' => 'group', + 'relationship' => 'member', + 'relationship_guid' => $this->guid, + 'limit' => $limit, + 'offset' => $offset, + 'full_view' => false, + ); + + if ($subtype) { + $options['subtype'] = $subtype; + } + + return elgg_list_entities_from_relationship($options); + } + + /** + * Get an array of ElggObject owned by this user. + * + * @param string $subtype The subtype of the objects, if any + * @param int $limit Number of results to return + * @param int $offset Any indexing offset + * + * @return array|false + */ + public function getObjects($subtype = "", $limit = 10, $offset = 0) { + $params = array( + 'type' => 'object', + 'subtype' => $subtype, + 'owner_guid' => $this->getGUID(), + 'limit' => $limit, + 'offset' => $offset + ); + return elgg_get_entities($params); + } + + /** + * Get an array of ElggObjects owned by this user's friends. + * + * @param string $subtype The subtype of the objects, if any + * @param int $limit Number of results to return + * @param int $offset Any indexing offset + * + * @return array|false + */ + public function getFriendsObjects($subtype = "", $limit = 10, $offset = 0) { + return get_user_friends_objects($this->getGUID(), $subtype, $limit, $offset); + } + + /** + * Counts the number of ElggObjects owned by this user + * + * @param string $subtype The subtypes of the objects, if any + * + * @return int The number of ElggObjects + */ + public function countObjects($subtype = "") { + return count_user_objects($this->getGUID(), $subtype); + } + + /** + * Get the collections associated with a user. + * + * @param string $subtype Optionally, the subtype of result we want to limit to + * @param int $limit The number of results to return + * @param int $offset Any indexing offset + * + * @return array|false + */ + public function getCollections($subtype = "", $limit = 10, $offset = 0) { + elgg_deprecated_notice("ElggUser::getCollections() has been deprecated", 1.8); + return false; + } + + /** + * Get a user's owner GUID + * + * Returns it's own GUID if the user is not owned. + * + * @return int + */ + function getOwnerGUID() { + if ($this->owner_guid == 0) { + return $this->guid; + } + + return $this->owner_guid; + } + + /** + * If a user's owner is blank, return its own GUID as the owner + * + * @return int User GUID + * @deprecated 1.8 Use getOwnerGUID() + */ + function getOwner() { + elgg_deprecated_notice("ElggUser::getOwner deprecated for ElggUser::getOwnerGUID", 1.8); + $this->getOwnerGUID(); + } + + // EXPORTABLE INTERFACE //////////////////////////////////////////////////////////// + + /** + * Return an array of fields which can be exported. + * + * @return array + */ + public function getExportableValues() { + return array_merge(parent::getExportableValues(), array( + 'name', + 'username', + 'language', + )); + } + + /** + * Need to catch attempts to make a user an admin. Remove for 1.9 + * + * @param string $name Name + * @param mixed $value Value + * + * @return bool + */ + public function __set($name, $value) { + if ($name == 'admin' || $name == 'siteadmin') { + elgg_deprecated_notice('The admin/siteadmin metadata are not longer used. Use ElggUser->makeAdmin() and ElggUser->removeAdmin().', 1.7); + + if ($value == 'yes' || $value == '1') { + $this->makeAdmin(); + } else { + $this->removeAdmin(); + } + } + return parent::__set($name, $value); + } + + /** + * Need to catch attempts to test user for admin. Remove for 1.9 + * + * @param string $name Name + * + * @return bool + */ + public function __get($name) { + if ($name == 'admin' || $name == 'siteadmin') { + elgg_deprecated_notice('The admin/siteadmin metadata are not longer used. Use ElggUser->isAdmin().', 1.7); + return $this->isAdmin(); + } + + return parent::__get($name); + } + + /** + * Can a user comment on this user? + * + * @see ElggEntity::canComment() + * + * @param int $user_guid User guid (default is logged in user) + * @return bool + * @since 1.8.0 + */ + public function canComment($user_guid = 0) { + $result = parent::canComment($user_guid); + if ($result !== null) { + return $result; + } + return false; + } +} diff --git a/engine/classes/ElggVolatileMetadataCache.php b/engine/classes/ElggVolatileMetadataCache.php new file mode 100644 index 000000000..4acda7cee --- /dev/null +++ b/engine/classes/ElggVolatileMetadataCache.php @@ -0,0 +1,355 @@ +<?php +/** + * ElggVolatileMetadataCache + * In memory cache of known metadata values stored by entity. + * + * @package Elgg.Core + * @subpackage Cache + * + * @access private + */ +class ElggVolatileMetadataCache { + + /** + * The cached values (or null for known to be empty). If the portion of the cache + * is synchronized, missing values are assumed to indicate that values do not + * exist in storage, otherwise, we don't know what's there. + * + * @var array + */ + protected $values = array(); + + /** + * Does the cache know that it contains all names fetch-able from storage? + * The keys are entity GUIDs and either the value exists (true) or it's not set. + * + * @var array + */ + protected $isSynchronized = array(); + + /** + * @var null|bool + */ + protected $ignoreAccess = null; + + /** + * Cache metadata for an entity + * + * @param int $entity_guid The GUID of the entity + * @param array $values The metadata values to cache + * @return void + */ + public function saveAll($entity_guid, array $values) { + if (!$this->getIgnoreAccess()) { + $this->values[$entity_guid] = $values; + $this->isSynchronized[$entity_guid] = true; + } + } + + /** + * Get the metadata for an entity + * + * @param int $entity_guid The GUID of the entity + * @return array + */ + public function loadAll($entity_guid) { + if (isset($this->values[$entity_guid])) { + return $this->values[$entity_guid]; + } else { + return array(); + } + } + + /** + * Declare that there may be fetch-able metadata names in storage that this + * cache doesn't know about + * + * @param int $entity_guid The GUID of the entity + * @return void + */ + public function markOutOfSync($entity_guid) { + unset($this->isSynchronized[$entity_guid]); + } + + /** + * Have all the metadata for this entity been cached? + * + * @param int $entity_guid The GUID of the entity + * @return bool + */ + public function isSynchronized($entity_guid) { + return isset($this->isSynchronized[$entity_guid]); + } + + /** + * Cache a piece of metadata + * + * @param int $entity_guid The GUID of the entity + * @param string $name The metadata name + * @param array|int|string|null $value The metadata value. null means it is + * known that there is no fetch-able + * metadata under this name + * @param bool $allow_multiple Can the metadata be an array + * @return void + */ + public function save($entity_guid, $name, $value, $allow_multiple = false) { + if ($this->getIgnoreAccess()) { + // we don't know if what gets saves here will be available to user once + // access control returns, hence it's best to forget :/ + $this->markUnknown($entity_guid, $name); + } else { + if ($allow_multiple) { + if ($this->isKnown($entity_guid, $name)) { + $existing = $this->load($entity_guid, $name); + if ($existing !== null) { + $existing = (array) $existing; + $existing[] = $value; + $value = $existing; + } + } else { + // we don't know whether there are unknown values, so it's + // safest to leave that assumption + $this->markUnknown($entity_guid, $name); + return; + } + } + $this->values[$entity_guid][$name] = $value; + } + } + + /** + * Warning: You should always call isKnown() beforehand to verify that this + * function's return value should be trusted (otherwise a null return value + * is ambiguous). + * + * @param int $entity_guid The GUID of the entity + * @param string $name The metadata name + * @return array|string|int|null null = value does not exist + */ + public function load($entity_guid, $name) { + if (isset($this->values[$entity_guid]) && array_key_exists($name, $this->values[$entity_guid])) { + return $this->values[$entity_guid][$name]; + } else { + return null; + } + } + + /** + * Forget about this metadata entry. We don't want to try to guess what the + * next fetch from storage will return + * + * @param int $entity_guid The GUID of the entity + * @param string $name The metadata name + * @return void + */ + public function markUnknown($entity_guid, $name) { + unset($this->values[$entity_guid][$name]); + $this->markOutOfSync($entity_guid); + } + + /** + * If true, load() will return an accurate value for this name + * + * @param int $entity_guid The GUID of the entity + * @param string $name The metadata name + * @return bool + */ + public function isKnown($entity_guid, $name) { + if (isset($this->isSynchronized[$entity_guid])) { + return true; + } else { + return (isset($this->values[$entity_guid]) && array_key_exists($name, $this->values[$entity_guid])); + } + + } + + /** + * Declare that metadata under this name is known to be not fetch-able from storage + * + * @param int $entity_guid The GUID of the entity + * @param string $name The metadata name + * @return array + */ + public function markEmpty($entity_guid, $name) { + $this->values[$entity_guid][$name] = null; + } + + /** + * Forget about all metadata for an entity + * + * @param int $entity_guid The GUID of the entity + * @return void + */ + public function clear($entity_guid) { + $this->values[$entity_guid] = array(); + $this->markOutOfSync($entity_guid); + } + + /** + * Clear entire cache and mark all entities as out of sync + * + * @return void + */ + public function flush() { + $this->values = array(); + $this->isSynchronized = array(); + } + + /** + * Use this value instead of calling elgg_get_ignore_access(). By default that + * function will be called. + * + * This setting makes this component a little more loosely-coupled. + * + * @param bool $ignore Whether to ignore access or not + * @return void + */ + public function setIgnoreAccess($ignore) { + $this->ignoreAccess = (bool) $ignore; + } + + /** + * Tell the cache to call elgg_get_ignore_access() to determing access status. + * + * @return void + */ + public function unsetIgnoreAccess() { + $this->ignoreAccess = null; + } + + /** + * Get the ignore access value + * + * @return bool + */ + protected function getIgnoreAccess() { + if (null === $this->ignoreAccess) { + return elgg_get_ignore_access(); + } else { + return $this->ignoreAccess; + } + } + + /** + * Invalidate based on options passed to the global *_metadata functions + * + * @param string $action Action performed on metadata. "delete", "disable", or "enable" + * @param array $options Options passed to elgg_(delete|disable|enable)_metadata + * "guid" if given, invalidation will be limited to this entity + * "metadata_name" if given, invalidation will be limited to metadata with this name + * @return void + */ + public function invalidateByOptions($action, array $options) { + // remove as little as possible, optimizing for common cases + if (empty($options['guid'])) { + // safest to clear everything unless we want to make this even more complex :( + $this->flush(); + } else { + if (empty($options['metadata_name'])) { + // safest to clear the whole entity + $this->clear($options['guid']); + } else { + switch ($action) { + case 'delete': + $this->markEmpty($options['guid'], $options['metadata_name']); + break; + default: + $this->markUnknown($options['guid'], $options['metadata_name']); + } + } + } + } + + /** + * Populate the cache from a set of entities + * + * @param int|array $guids Array of or single GUIDs + * @return void + */ + public function populateFromEntities($guids) { + if (empty($guids)) { + return; + } + if (!is_array($guids)) { + $guids = array($guids); + } + $guids = array_unique($guids); + + // could be useful at some point in future + //$guids = $this->filterMetadataHeavyEntities($guids); + + $db_prefix = elgg_get_config('dbprefix'); + $options = array( + 'guids' => $guids, + 'limit' => 0, + 'callback' => false, + 'joins' => array( + "JOIN {$db_prefix}metastrings v ON n_table.value_id = v.id", + "JOIN {$db_prefix}metastrings n ON n_table.name_id = n.id", + ), + 'selects' => array('n.string AS name', 'v.string AS value'), + 'order_by' => 'n_table.entity_guid, n_table.time_created ASC', + + // @todo don't know why this is necessary + 'wheres' => array(get_access_sql_suffix('n_table')), + ); + $data = elgg_get_metadata($options); + + // build up metadata for each entity, save when GUID changes (or data ends) + $last_guid = null; + $metadata = array(); + $last_row_idx = count($data) - 1; + foreach ($data as $i => $row) { + $name = $row->name; + $value = ($row->value_type === 'text') ? $row->value : (int) $row->value; + $guid = $row->entity_guid; + if ($guid !== $last_guid) { + if ($last_guid) { + $this->saveAll($last_guid, $metadata); + } + $metadata = array(); + } + if (isset($metadata[$name])) { + $metadata[$name] = (array) $metadata[$name]; + $metadata[$name][] = $value; + } else { + $metadata[$name] = $value; + } + if (($i == $last_row_idx)) { + $this->saveAll($guid, $metadata); + } + $last_guid = $guid; + } + } + + /** + * Filter out entities whose concatenated metadata values (INTs casted as string) + * exceed a threshold in characters. This could be used to avoid overpopulating the + * cache if RAM usage becomes an issue. + * + * @param array $guids GUIDs of entities to examine + * @param int $limit Limit in characters of all metadata (with ints casted to strings) + * @return array + */ + public function filterMetadataHeavyEntities(array $guids, $limit = 1024000) { + $db_prefix = elgg_get_config('dbprefix'); + + $options = array( + 'guids' => $guids, + 'limit' => 0, + 'callback' => false, + 'joins' => "JOIN {$db_prefix}metastrings v ON n_table.value_id = v.id", + 'selects' => array('SUM(LENGTH(v.string)) AS bytes'), + 'order_by' => 'n_table.entity_guid, n_table.time_created ASC', + 'group_by' => 'n_table.entity_guid', + ); + $data = elgg_get_metadata($options); + // don't cache if metadata for entity is over 10MB (or rolled INT) + foreach ($data as $row) { + if ($row->bytes > $limit || $row->bytes < 0) { + array_splice($guids, array_search($row->entity_guid, $guids), 1); + } + } + return $guids; + } +} diff --git a/engine/classes/ElggWidget.php b/engine/classes/ElggWidget.php new file mode 100644 index 000000000..66191bf47 --- /dev/null +++ b/engine/classes/ElggWidget.php @@ -0,0 +1,245 @@ +<?php + +/** + * ElggWidget + * + * Stores metadata in private settings rather than as ElggMetadata + * + * @package Elgg.Core + * @subpackage Widgets + * + * @property-read string $handler internal, do not use + * @property-read string $column internal, do not use + * @property-read string $order internal, do not use + * @property-read string $context internal, do not use + */ +class ElggWidget extends ElggObject { + + /** + * Set subtype to widget. + * + * @return void + */ + protected function initializeAttributes() { + parent::initializeAttributes(); + + $this->attributes['subtype'] = "widget"; + } + + /** + * Override entity get and sets in order to save data to private data store. + * + * @param string $name Name + * + * @return mixed + */ + public function get($name) { + // See if its in our base attribute + if (array_key_exists($name, $this->attributes)) { + return $this->attributes[$name]; + } + + // No, so see if its in the private data store. + $meta = $this->getPrivateSetting($name); + if ($meta) { + return $meta; + } + + // Can't find it, so return null + return null; + } + + /** + * Override entity get and sets in order to save data to private data store. + * + * @param string $name Name + * @param string $value Value + * + * @return bool + */ + public function set($name, $value) { + if (array_key_exists($name, $this->attributes)) { + // Check that we're not trying to change the guid! + if ((array_key_exists('guid', $this->attributes)) && ($name == 'guid')) { + return false; + } + + $this->attributes[$name] = $value; + } else { + return $this->setPrivateSetting($name, $value); + } + + return true; + } + + /** + * Set the widget context + * + * @param string $context The widget context + * @return bool + * @since 1.8.0 + */ + public function setContext($context) { + return $this->setPrivateSetting('context', $context); + } + + /** + * Get the widget context + * + * @return string + * @since 1.8.0 + */ + public function getContext() { + return $this->getPrivateSetting('context'); + } + + /** + * Get the title of the widget + * + * @return string + * @since 1.8.0 + */ + public function getTitle() { + $title = $this->title; + if (!$title) { + global $CONFIG; + $title = $CONFIG->widgets->handlers[$this->handler]->name; + } + return $title; + } + + /** + * Move the widget + * + * @param int $column The widget column + * @param int $rank Zero-based rank from the top of the column + * @return void + * @since 1.8.0 + */ + public function move($column, $rank) { + $options = array( + 'type' => 'object', + 'subtype' => 'widget', + 'container_guid' => $this->container_guid, + 'limit' => false, + 'private_setting_name_value_pairs' => array( + array('name' => 'context', 'value' => $this->getContext()), + array('name' => 'column', 'value' => $column) + ) + ); + $widgets = elgg_get_entities_from_private_settings($options); + if (!$widgets) { + $this->column = (int)$column; + $this->order = 0; + return; + } + + usort($widgets, create_function('$a,$b','return (int)$a->order > (int)$b->order;')); + + // remove widgets from inactive plugins + $widget_types = elgg_get_widget_types($this->context); + $inactive_widgets = array(); + foreach ($widgets as $index => $widget) { + if (!array_key_exists($widget->handler, $widget_types)) { + $inactive_widgets[] = $widget; + unset($widgets[$index]); + } + } + + $bottom_rank = count($widgets); + if ($column == $this->column) { + $bottom_rank--; + } + + if ($rank == 0) { + // top of the column + $this->order = reset($widgets)->order - 10; + } elseif ($rank == $bottom_rank) { + // bottom of the column of active widgets + $this->order = end($widgets)->order + 10; + } else { + // reorder widgets + + // remove the widget that's being moved from the array + foreach ($widgets as $index => $widget) { + if ($widget->guid == $this->guid) { + unset($widgets[$index]); + } + } + + // split the array in two and recombine with the moved widget in middle + $before = array_slice($widgets, 0, $rank); + array_push($before, $this); + $after = array_slice($widgets, $rank); + $widgets = array_merge($before, $after); + ksort($widgets); + $order = 0; + foreach ($widgets as $widget) { + $widget->order = $order; + $order += 10; + } + } + + // put inactive widgets at the bottom + if ($inactive_widgets) { + $bottom = 0; + foreach ($widgets as $widget) { + if ($widget->order > $bottom) { + $bottom = $widget->order; + } + } + $bottom += 10; + foreach ($inactive_widgets as $widget) { + $widget->order = $bottom; + $bottom += 10; + } + } + + $this->column = $column; + } + + /** + * Saves the widget's settings + * + * Plugins can override the save mechanism using the plugin hook: + * 'widget_settings', <widget handler identifier>. The widget and + * the parameters are passed. The plugin hook handler should return + * true to indicate that it has successfully saved the settings. + * + * @warning The values in the parameter array cannot be arrays + * + * @param array $params An array of name => value parameters + * + * @return bool + * @since 1.8.0 + */ + public function saveSettings($params) { + if (!$this->canEdit()) { + return false; + } + + // plugin hook handlers should return true to indicate the settings have + // been saved so that default code does not run + $hook_params = array( + 'widget' => $this, + 'params' => $params + ); + if (elgg_trigger_plugin_hook('widget_settings', $this->handler, $hook_params, false) == true) { + return true; + } + + if (is_array($params) && count($params) > 0) { + foreach ($params as $name => $value) { + if (is_array($value)) { + // private settings cannot handle arrays + return false; + } else { + $this->$name = $value; + } + } + $this->save(); + } + + return true; + } +} diff --git a/engine/classes/ElggXMLElement.php b/engine/classes/ElggXMLElement.php new file mode 100644 index 000000000..cbd3fc5ce --- /dev/null +++ b/engine/classes/ElggXMLElement.php @@ -0,0 +1,131 @@ +<?php +/** + * A parser for XML that uses SimpleXMLElement + * + * @package Elgg.Core + * @subpackage XML + */ +class ElggXMLElement { + /** + * @var SimpleXMLElement + */ + private $_element; + + /** + * Creates an ElggXMLParser from a string or existing SimpleXMLElement + * + * @param string|SimpleXMLElement $xml The XML to parse + */ + public function __construct($xml) { + if ($xml instanceof SimpleXMLElement) { + $this->_element = $xml; + } else { + // do not load entities + $disable_load_entities = libxml_disable_entity_loader(true); + + $this->_element = new SimpleXMLElement($xml); + + libxml_disable_entity_loader($disable_load_entities); + } + } + + /** + * @return string The name of the element + */ + public function getName() { + return $this->_element->getName(); + } + + /** + * @return string[] The attributes + */ + public function getAttributes() { + //include namespace declarations as attributes + $xmlnsRaw = $this->_element->getNamespaces(); + $xmlns = array(); + foreach ($xmlnsRaw as $key => $val) { + $label = 'xmlns' . ($key ? ":$key" : $key); + $xmlns[$label] = $val; + } + //get attributes and merge with namespaces + $attrRaw = $this->_element->attributes(); + $attr = array(); + foreach ($attrRaw as $key => $val) { + $attr[$key] = $val; + } + $attr = array_merge((array) $xmlns, (array) $attr); + $result = array(); + foreach ($attr as $key => $val) { + $result[$key] = (string) $val; + } + return $result; + } + + /** + * @return string CData + */ + public function getContent() { + return (string) $this->_element; + } + + /** + * @return ElggXMLElement[] Child elements + */ + public function getChildren() { + $children = $this->_element->children(); + $result = array(); + foreach ($children as $val) { + $result[] = new ElggXMLElement($val); + } + + return $result; + } + + /** + * Override -> + * + * @param string $name Property name + * @return mixed + */ + function __get($name) { + switch ($name) { + case 'name': + return $this->getName(); + break; + case 'attributes': + return $this->getAttributes(); + break; + case 'content': + return $this->getContent(); + break; + case 'children': + return $this->getChildren(); + break; + } + return null; + } + + /** + * Override isset + * + * @param string $name Property name + * @return boolean + */ + function __isset($name) { + switch ($name) { + case 'name': + return $this->getName() !== null; + break; + case 'attributes': + return $this->getAttributes() !== null; + break; + case 'content': + return $this->getContent() !== null; + break; + case 'children': + return $this->getChildren() !== null; + break; + } + return false; + } +} diff --git a/engine/classes/ErrorResult.php b/engine/classes/ErrorResult.php new file mode 100644 index 000000000..afad4c740 --- /dev/null +++ b/engine/classes/ErrorResult.php @@ -0,0 +1,54 @@ +<?php +/** + * ErrorResult + * The error result class. + * + * @package Elgg.Core + * @subpackage WebServicesAPI + */ +class ErrorResult extends GenericResult { + // Fail with no specific code + public static $RESULT_FAIL = -1 ; + + public static $RESULT_FAIL_APIKEY_DISABLED = -30; + public static $RESULT_FAIL_APIKEY_INACTIVE = -31; + public static $RESULT_FAIL_APIKEY_INVALID = -32; + + // Invalid, expired or missing auth token + public static $RESULT_FAIL_AUTHTOKEN = -20; + + /** + * A new error result + * + * @param string $message Message + * @param int $code Error Code + * @param Exception $exception Exception object + * + * @return void + */ + public function __construct($message, $code = "", Exception $exception = NULL) { + if ($code == "") { + $code = ErrorResult::$RESULT_FAIL; + } + + if ($exception != NULL) { + $this->setResult($exception->__toString()); + } + + $this->setStatusCode($code, $message); + } + + /** + * Get a new instance of the ErrorResult. + * + * @param string $message Message + * @param int $code Code + * @param Exception $exception Optional exception for generating a stack trace. + * + * @return ErrorResult + */ + public static function getInstance($message, $code = "", Exception $exception = NULL) { + // Return a new error object. + return new ErrorResult($message, $code, $exception); + } +} diff --git a/engine/classes/ExportException.php b/engine/classes/ExportException.php new file mode 100644 index 000000000..ae8a8e41b --- /dev/null +++ b/engine/classes/ExportException.php @@ -0,0 +1,9 @@ +<?php +/** + * Export exception + * + * @package Elgg.Core + * @subpackage Exception + * + */ +class ExportException extends DataFormatException {} diff --git a/engine/classes/Exportable.php b/engine/classes/Exportable.php new file mode 100644 index 000000000..0c1ea5282 --- /dev/null +++ b/engine/classes/Exportable.php @@ -0,0 +1,23 @@ +<?php +/** + * Define an interface for all ODD exportable objects. + * + * @package Elgg.Core + * @subpackage ODD + */ +interface Exportable { + /** + * This must take the contents of the object and convert it to exportable ODD + * + * @return object or array of objects. + */ + public function export(); + + /** + * Return a list of all fields that can be exported. + * This should be used as the basis for the values returned by export() + * + * @return array + */ + public function getExportableValues(); +} diff --git a/engine/classes/Friendable.php b/engine/classes/Friendable.php new file mode 100644 index 000000000..c308b4598 --- /dev/null +++ b/engine/classes/Friendable.php @@ -0,0 +1,104 @@ +<?php +/** + * An interface for objects that behave as elements within a social network that have a profile. + * + * @package Elgg.Core + * @subpackage SocialModel.Friendable + */ +interface Friendable { + /** + * Adds a user as a friend + * + * @param int $friend_guid The GUID of the user to add + * + * @return bool + */ + public function addFriend($friend_guid); + + /** + * Removes a user as a friend + * + * @param int $friend_guid The GUID of the user to remove + * + * @return bool + */ + public function removeFriend($friend_guid); + + /** + * Determines whether or not the current user is a friend of this entity + * + * @return bool + */ + public function isFriend(); + + /** + * Determines whether or not this entity is friends with a particular entity + * + * @param int $user_guid The GUID of the entity this entity may or may not be friends with + * + * @return bool + */ + public function isFriendsWith($user_guid); + + /** + * Determines whether or not a foreign entity has made this one a friend + * + * @param int $user_guid The GUID of the foreign entity + * + * @return bool + */ + public function isFriendOf($user_guid); + + /** + * Returns this entity's friends + * + * @param string $subtype The subtype of entity to return + * @param int $limit The number of entities to return + * @param int $offset Indexing offset + * + * @return array|false + */ + public function getFriends($subtype = "", $limit = 10, $offset = 0); + + /** + * Returns entities that have made this entity a friend + * + * @param string $subtype The subtype of entity to return + * @param int $limit The number of entities to return + * @param int $offset Indexing offset + * + * @return array|false + */ + public function getFriendsOf($subtype = "", $limit = 10, $offset = 0); + + /** + * Returns objects in this entity's container + * + * @param string $subtype The subtype of entity to return + * @param int $limit The number of entities to return + * @param int $offset Indexing offset + * + * @return array|false + */ + public function getObjects($subtype = "", $limit = 10, $offset = 0); + + /** + * Returns objects in the containers of this entity's friends + * + * @param string $subtype The subtype of entity to return + * @param int $limit The number of entities to return + * @param int $offset Indexing offset + * + * @return array|false + */ + public function getFriendsObjects($subtype = "", $limit = 10, $offset = 0); + + /** + * Returns the number of object entities in this entity's container + * + * @param string $subtype The subtype of entity to count + * + * @return int + */ + public function countObjects($subtype = ""); +} diff --git a/engine/classes/GenericResult.php b/engine/classes/GenericResult.php new file mode 100644 index 000000000..e42e924d1 --- /dev/null +++ b/engine/classes/GenericResult.php @@ -0,0 +1,125 @@ +<?php +/** + * GenericResult Result superclass. + * + * @package Elgg.Core + * @subpackage WebServicesAPI + */ +abstract class GenericResult { + /** + * The status of the result. + * @var int + */ + private $status_code; + + /** + * Message returned along with the status which is almost always an error message. + * This must be human readable, understandable and localised. + * @var string + */ + private $message; + + /** + * Result store. + * Attach result specific informaton here. + * + * @var mixed. Should probably be an object of some sort. + */ + private $result; + + /** + * Set a status code and optional message. + * + * @param int $status The status code. + * @param string $message The message. + * + * @return void + */ + protected function setStatusCode($status, $message = "") { + $this->status_code = $status; + $this->message = $message; + } + + /** + * Set the result. + * + * @param mixed $result The result + * + * @return void + */ + protected function setResult($result) { + $this->result = $result; + } + + /** + * Return the current status code + * + * @return string + */ + protected function getStatusCode() { + return $this->status_code; + } + + /** + * Return the current status message + * + * @return string + */ + protected function getStatusMessage() { + return $this->message; + } + + /** + * Return the current result + * + * @return string + */ + protected function getResult() { + return $this->result; + } + + /** + * Serialise to a standard class. + * + * DEVNOTE: The API is only interested in data, we can not easily serialise + * custom classes without the need for 1) the other side being PHP, 2) you need to have the class + * definition installed, 3) its the right version! + * + * Therefore, I'm not bothering. + * + * Override this to include any more specific information, however api results + * should be attached to the class using setResult(). + * + * if $CONFIG->debug is set then additional information about the runtime environment and + * authentication will be returned. + * + * @return stdClass Object containing the serialised result. + */ + public function export() { + global $ERRORS, $CONFIG, $_PAM_HANDLERS_MSG; + + $result = new stdClass; + + $result->status = $this->getStatusCode(); + if ($this->getStatusMessage() != "") { + $result->message = $this->getStatusMessage(); + } + + $resultdata = $this->getResult(); + if (isset($resultdata)) { + $result->result = $resultdata; + } + + if (isset($CONFIG->debug)) { + if (count($ERRORS)) { + $result->runtime_errors = $ERRORS; + } + + if (count($_PAM_HANDLERS_MSG)) { + $result->pam = $_PAM_HANDLERS_MSG; + } + } + + return $result; + } +} diff --git a/engine/classes/IOException.php b/engine/classes/IOException.php new file mode 100644 index 000000000..57403f44c --- /dev/null +++ b/engine/classes/IOException.php @@ -0,0 +1,9 @@ +<?php +/** + * IOException + * An IO Exception, throw when an IO Exception occurs. Subclass for specific IO Exceptions. + * + * @package Elgg.Core + * @subpackage Exception + */ +class IOException extends Exception {} diff --git a/engine/classes/ImportException.php b/engine/classes/ImportException.php new file mode 100644 index 000000000..909c599d5 --- /dev/null +++ b/engine/classes/ImportException.php @@ -0,0 +1,8 @@ +<?php +/** + * Import exception + * + * @package Elgg.Core + * @subpackage Exception + */ +class ImportException extends DataFormatException {} diff --git a/engine/classes/Importable.php b/engine/classes/Importable.php new file mode 100644 index 000000000..23b2ce2c8 --- /dev/null +++ b/engine/classes/Importable.php @@ -0,0 +1,19 @@ +<?php +/** + * Define an interface for all ODD importable objects. + * + * @package Elgg.Core + * @subpackage DataModel.Importable + */ +interface Importable { + /** + * Accepts an array of data to import, this data is parsed from the XML produced by export. + * The function should return the constructed object data, or NULL. + * + * @param ODD $data Data in ODD format + * + * @return bool + * @throws ImportException if there was a critical error importing data. + */ + public function import(ODD $data); +} diff --git a/engine/classes/IncompleteEntityException.php b/engine/classes/IncompleteEntityException.php new file mode 100644 index 000000000..8c86edcc6 --- /dev/null +++ b/engine/classes/IncompleteEntityException.php @@ -0,0 +1,10 @@ +<?php +/** + * IncompleteEntityException + * Thrown when constructing an entity that is missing its secondary entity table + * + * @package Elgg.Core + * @subpackage Exception + * @access private + */ +class IncompleteEntityException extends Exception {} diff --git a/engine/classes/InstallationException.php b/engine/classes/InstallationException.php new file mode 100644 index 000000000..1dad6c1e5 --- /dev/null +++ b/engine/classes/InstallationException.php @@ -0,0 +1,9 @@ +<?php +/** + * InstallationException + * Thrown when there is a major problem with the installation. + * + * @package Elgg.Core + * @subpackage Exception + */ +class InstallationException extends ConfigurationException {} diff --git a/engine/classes/InvalidClassException.php b/engine/classes/InvalidClassException.php new file mode 100644 index 000000000..12f353b9a --- /dev/null +++ b/engine/classes/InvalidClassException.php @@ -0,0 +1,9 @@ +<?php +/** + * InvalidClassException + * An invalid class Exception, throw when a class is invalid. + * + * @package Elgg.Core + * @subpackage Exception + */ +class InvalidClassException extends ClassException {} diff --git a/engine/classes/InvalidParameterException.php b/engine/classes/InvalidParameterException.php new file mode 100644 index 000000000..fbc9bffc9 --- /dev/null +++ b/engine/classes/InvalidParameterException.php @@ -0,0 +1,9 @@ +<?php +/** + * InvalidParameterException + * A parameter is invalid. + * + * @package Elgg.Core + * @subpackage Exception + */ +class InvalidParameterException extends CallException {} diff --git a/engine/classes/Locatable.php b/engine/classes/Locatable.php new file mode 100644 index 000000000..7287d9798 --- /dev/null +++ b/engine/classes/Locatable.php @@ -0,0 +1,49 @@ +<?php + +/** + * Define an interface for geo-tagging entities. + * + * @package Elgg.Core + * @subpackage SocialModel.Locatable + */ +interface Locatable { + /** + * Set a location text + * + * @param string $location Textual representation of location + * + * @return bool + */ + public function setLocation($location); + + /** + * Set latitude and longitude tags for a given entity. + * + * @param float $lat Latitude + * @param float $long Longitude + * + * @return bool + */ + public function setLatLong($lat, $long); + + /** + * Get the contents of the ->geo:lat field. + * + * @return int + */ + public function getLatitude(); + + /** + * Get the contents of the ->geo:lat field. + * + * @return int + */ + public function getLongitude(); + + /** + * Get the ->location metadata. + * + * @return string + */ + public function getLocation(); +} diff --git a/engine/classes/Loggable.php b/engine/classes/Loggable.php new file mode 100644 index 000000000..b9e8bf26b --- /dev/null +++ b/engine/classes/Loggable.php @@ -0,0 +1,65 @@ +<?php +/** + * Interface that provides an interface which must be implemented by all objects wishing to be + * recorded in the system log (and by extension the river). + * + * This interface defines a set of methods that permit the system log functions to + * hook in and retrieve the necessary information and to identify what events can + * actually be logged. + * + * To have events involving your object to be logged simply implement this interface. + * + * @package Elgg.Core + * @subpackage DataModel.Loggable + */ +interface Loggable { + /** + * Return an identification for the object for storage in the system log. + * This id must be an integer. + * + * @return int + */ + public function getSystemLogID(); + + /** + * Return the class name of the object. + * Added as a function because get_class causes errors for some reason. + * + * @return string + */ + public function getClassName(); + + /** + * Return the type of the object - eg. object, group, user, relationship, metadata, annotation etc + * + * @return string + */ + public function getType(); + + /** + * Return a subtype. For metadata & annotations this is the 'name' and for relationship this is the + * relationship type. + * + * @return string + */ + public function getSubtype(); + + /** + * For a given ID, return the object associated with it. + * This is used by the river functionality primarily. + * This is useful for checking access permissions etc on objects. + * + * @param int $id GUID of an entity + * + * @return ElggEntity + */ + public function getObjectFromID($id); + + /** + * Return the GUID of the owner of this object. + * + * @return int + * @deprecated 1.8 Use getOwnerGUID() instead + */ + public function getObjectOwnerGUID(); +} diff --git a/engine/classes/LoginException.php b/engine/classes/LoginException.php new file mode 100644 index 000000000..7546fa36f --- /dev/null +++ b/engine/classes/LoginException.php @@ -0,0 +1,10 @@ +<?php +/** + * Login Exception Stub + * + * Generic parent class for login exceptions. + * + * @package Elgg.Core + * @subpackage Exceptions.Stub + */ +class LoginException extends Exception {} diff --git a/engine/classes/NotImplementedException.php b/engine/classes/NotImplementedException.php new file mode 100644 index 000000000..d1decf75c --- /dev/null +++ b/engine/classes/NotImplementedException.php @@ -0,0 +1,10 @@ +<?php +/** + * NotImplementedException + * Thrown when a method or function has not been implemented, primarily used + * in development... you should not see these! + * + * @package Elgg.Core + * @subpackage Exception + */ +class NotImplementedException extends CallException {} diff --git a/engine/classes/Notable.php b/engine/classes/Notable.php new file mode 100644 index 000000000..0c21af27d --- /dev/null +++ b/engine/classes/Notable.php @@ -0,0 +1,41 @@ +<?php +/** + * Calendar interface for events. + * + * @package Elgg.Core + * @subpackage DataModel.Notable + * + * @todo Implement or remove. + */ +interface Notable { + /** + * Calendar functionality. + * This function sets the time of an object on a calendar listing. + * + * @param int $hour If ommitted, now is assumed. + * @param int $minute If ommitted, now is assumed. + * @param int $second If ommitted, now is assumed. + * @param int $day If ommitted, now is assumed. + * @param int $month If ommitted, now is assumed. + * @param int $year If ommitted, now is assumed. + * @param int $duration Duration of event, remainder of the day is assumed. + * + * @return bool + */ + public function setCalendarTimeAndDuration($hour = NULL, $minute = NULL, $second = NULL, + $day = NULL, $month = NULL, $year = NULL, $duration = NULL); + + /** + * Return the start timestamp. + * + * @return int + */ + public function getCalendarStartTime(); + + /** + * Return the end timestamp. + * + * @return int + */ + public function getCalendarEndTime(); +} diff --git a/engine/classes/NotificationException.php b/engine/classes/NotificationException.php new file mode 100644 index 000000000..71c742f17 --- /dev/null +++ b/engine/classes/NotificationException.php @@ -0,0 +1,8 @@ +<?php +/** + * Notification exception. + * + * @package Elgg.Core + * @subpackage Exception + */ +class NotificationException extends Exception {} diff --git a/engine/classes/ODD.php b/engine/classes/ODD.php new file mode 100644 index 000000000..fa5b616fc --- /dev/null +++ b/engine/classes/ODD.php @@ -0,0 +1,131 @@ +<?php +/** + * Open Data Definition (ODD) superclass. + * + * @package Elgg.Core + * @subpackage ODD + */ +abstract class ODD { + /** + * Attributes. + */ + private $attributes = array(); + + /** + * Optional body. + */ + private $body; + + /** + * Construct an ODD document with initial values. + */ + public function __construct() { + $this->body = ""; + } + + /** + * Returns an array of attributes + * + * @return array + */ + public function getAttributes() { + return $this->attributes; + } + + /** + * Sets an attribute + * + * @param string $key Name + * @param mixed $value Value + * + * @return void + */ + public function setAttribute($key, $value) { + $this->attributes[$key] = $value; + } + + /** + * Returns an attribute + * + * @param string $key Name + * + * @return mixed + */ + public function getAttribute($key) { + if (isset($this->attributes[$key])) { + return $this->attributes[$key]; + } + + return NULL; + } + + /** + * Sets the body of the ODD. + * + * @param mixed $value Value + * + * @return void + */ + public function setBody($value) { + $this->body = $value; + } + + /** + * Gets the body of the ODD. + * + * @return mixed + */ + public function getBody() { + return $this->body; + } + + /** + * Set the published time. + * + * @param int $time Unix timestamp + * + * @return void + */ + public function setPublished($time) { + $this->attributes['published'] = date("r", $time); + } + + /** + * Return the published time as a unix timestamp. + * + * @return int or false on failure. + */ + public function getPublishedAsTime() { + return strtotime($this->attributes['published']); + } + + /** + * For serialisation, implement to return a string name of the tag eg "header" or "metadata". + * + * @return string + */ + abstract protected function getTagName(); + + /** + * Magic function to generate valid ODD XML for this item. + * + * @return string + */ + public function __toString() { + // Construct attributes + $attr = ""; + foreach ($this->attributes as $k => $v) { + $attr .= ($v != "") ? "$k=\"$v\" " : ""; + } + + $body = $this->getBody(); + $tag = $this->getTagName(); + + $end = "/>"; + if ($body != "") { + $end = "><![CDATA[$body]]></{$tag}>"; + } + + return "<{$tag} $attr" . $end . "\n"; + } +} diff --git a/engine/classes/ODDDocument.php b/engine/classes/ODDDocument.php new file mode 100644 index 000000000..540c35a3b --- /dev/null +++ b/engine/classes/ODDDocument.php @@ -0,0 +1,202 @@ +<?php +/** + * This class is used during import and export to construct. + * + * @package Elgg.Core + * @subpackage ODD + */ +class ODDDocument implements Iterator { + /** + * ODD Version + * + * @var string + */ + private $ODDSupportedVersion = "1.0"; + + /** + * Elements of the document. + */ + private $elements; + + /** + * Optional wrapper factory. + */ + private $wrapperfactory; + + /** + * Create a new ODD Document. + * + * @param array $elements Elements to add + * + * @return void + */ + public function __construct(array $elements = NULL) { + if ($elements) { + if (is_array($elements)) { + $this->elements = $elements; + } else { + $this->addElement($elements); + } + } else { + $this->elements = array(); + } + } + + /** + * Return the version of ODD being used. + * + * @return string + */ + public function getVersion() { + return $this->ODDSupportedVersion; + } + + /** + * Returns the number of elements + * + * @return int + */ + public function getNumElements() { + return count($this->elements); + } + + /** + * Add an element + * + * @param ODD $element An ODD element + * + * @return void + */ + public function addElement(ODD $element) { + if (!is_array($this->elements)) { + $this->elements = array(); + } + $this->elements[] = $element; + } + + /** + * Add multiple elements at once + * + * @param array $elements Array of ODD elements + * + * @return void + */ + public function addElements(array $elements) { + foreach ($elements as $element) { + $this->addElement($element); + } + } + + /** + * Return all elements + * + * @return array + */ + public function getElements() { + return $this->elements; + } + + /** + * Set an optional wrapper factory to optionally embed the ODD document in another format. + * + * @param ODDWrapperFactory $factory The factory + * + * @return void + */ + public function setWrapperFactory(ODDWrapperFactory $factory) { + $this->wrapperfactory = $factory; + } + + /** + * Magic function to generate valid ODD XML for this item. + * + * @return string + */ + public function __toString() { + $xml = ""; + + if ($this->wrapperfactory) { + // A wrapper has been provided + $wrapper = $this->wrapperfactory->getElementWrapper($this); // Get the wrapper for this element + + $xml = $wrapper->wrap($this); // Wrap this element (and subelements) + } else { + // Output begin tag + $generated = date("r"); + $xml .= "<odd version=\"{$this->ODDSupportedVersion}\" generated=\"$generated\">\n"; + + // Get XML for elements + foreach ($this->elements as $element) { + $xml .= "$element"; + } + + // Output end tag + $xml .= "</odd>\n"; + } + + return $xml; + } + + // ITERATOR INTERFACE ////////////////////////////////////////////////////////////// + /* + * This lets an entity's attributes be displayed using foreach as a normal array. + * Example: http://www.sitepoint.com/print/php5-standard-library + */ + + private $valid = FALSE; + + /** + * Iterator interface + * + * @see Iterator::rewind() + * + * @return void + */ + function rewind() { + $this->valid = (FALSE !== reset($this->elements)); + } + + /** + * Iterator interface + * + * @see Iterator::current() + * + * @return void + */ + function current() { + return current($this->elements); + } + + /** + * Iterator interface + * + * @see Iterator::key() + * + * @return void + */ + function key() { + return key($this->elements); + } + + /** + * Iterator interface + * + * @see Iterator::next() + * + * @return void + */ + function next() { + $this->valid = (FALSE !== next($this->elements)); + } + + /** + * Iterator interface + * + * @see Iterator::valid() + * + * @return void + */ + function valid() { + return $this->valid; + } +} diff --git a/engine/classes/ODDEntity.php b/engine/classes/ODDEntity.php new file mode 100644 index 000000000..e9bb5da6a --- /dev/null +++ b/engine/classes/ODDEntity.php @@ -0,0 +1,34 @@ +<?php + +/** + * ODD Entity class. + * + * @package Elgg.Core + * @subpackage ODD + */ +class ODDEntity extends ODD { + + /** + * New ODD Entity + * + * @param string $uuid A universally unique ID + * @param string $class Class + * @param string $subclass Subclass + */ + function __construct($uuid, $class, $subclass = "") { + parent::__construct(); + + $this->setAttribute('uuid', $uuid); + $this->setAttribute('class', $class); + $this->setAttribute('subclass', $subclass); + } + + /** + * Returns entity. + * + * @return 'entity' + */ + protected function getTagName() { + return "entity"; + } +} diff --git a/engine/classes/ODDMetaData.php b/engine/classes/ODDMetaData.php new file mode 100644 index 000000000..09b653582 --- /dev/null +++ b/engine/classes/ODDMetaData.php @@ -0,0 +1,39 @@ +<?php +/** + * ODD Metadata class. + * + * @package Elgg.Core + * @subpackage ODD + */ +class ODDMetaData extends ODD { + + /** + * New ODD metadata + * + * @param string $uuid Unique ID + * @param string $entity_uuid Another unique ID + * @param string $name Name + * @param string $value Value + * @param string $type Type + * @param string $owner_uuid Owner ID + */ + function __construct($uuid, $entity_uuid, $name, $value, $type = "", $owner_uuid = "") { + parent::__construct(); + + $this->setAttribute('uuid', $uuid); + $this->setAttribute('entity_uuid', $entity_uuid); + $this->setAttribute('name', $name); + $this->setAttribute('type', $type); + $this->setAttribute('owner_uuid', $owner_uuid); + $this->setBody($value); + } + + /** + * Returns 'metadata' + * + * @return string 'metadata' + */ + protected function getTagName() { + return "metadata"; + } +} diff --git a/engine/classes/ODDRelationship.php b/engine/classes/ODDRelationship.php new file mode 100644 index 000000000..8b1fe217b --- /dev/null +++ b/engine/classes/ODDRelationship.php @@ -0,0 +1,33 @@ +<?php +/** + * ODD Relationship class. + * + * @package Elgg + * @subpackage Core + */ +class ODDRelationship extends ODD { + + /** + * New ODD Relationship + * + * @param string $uuid1 First UUID + * @param string $type Type of telationship + * @param string $uuid2 Second UUId + */ + function __construct($uuid1, $type, $uuid2) { + parent::__construct(); + + $this->setAttribute('uuid1', $uuid1); + $this->setAttribute('type', $type); + $this->setAttribute('uuid2', $uuid2); + } + + /** + * Returns 'relationship' + * + * @return string 'relationship' + */ + protected function getTagName() { + return "relationship"; + } +} diff --git a/engine/classes/PluginException.php b/engine/classes/PluginException.php new file mode 100644 index 000000000..a74303695 --- /dev/null +++ b/engine/classes/PluginException.php @@ -0,0 +1,11 @@ +<?php +/** + * PluginException + * + * A plugin Exception, thrown when an Exception occurs relating to the plugin mechanism. + * Subclass for specific plugin Exceptions. + * + * @package Elgg.Core + * @subpackage Exception + */ +class PluginException extends Exception {} diff --git a/engine/classes/RegistrationException.php b/engine/classes/RegistrationException.php new file mode 100644 index 000000000..5246efc25 --- /dev/null +++ b/engine/classes/RegistrationException.php @@ -0,0 +1,9 @@ +<?php +/** + * RegistrationException + * Could not register a new user for whatever reason. + * + * @package Elgg.Core + * @subpackage Exceptions + */ +class RegistrationException extends InstallationException {} diff --git a/engine/classes/SecurityException.php b/engine/classes/SecurityException.php new file mode 100644 index 000000000..3b6382f9e --- /dev/null +++ b/engine/classes/SecurityException.php @@ -0,0 +1,10 @@ +<?php +/** + * SecurityException + * An Security Exception, throw when a Security Exception occurs. Subclass for + * specific Security Execeptions (access problems etc) + * + * @package Elgg.Core + * @subpackage Exception + */ +class SecurityException extends Exception {} diff --git a/engine/classes/SuccessResult.php b/engine/classes/SuccessResult.php new file mode 100644 index 000000000..ab5468ad8 --- /dev/null +++ b/engine/classes/SuccessResult.php @@ -0,0 +1,34 @@ +<?php +/** + * SuccessResult + * Generic success result class, extend if you want to do something special. + * + * @package Elgg.Core + * @subpackage WebServicesAPI + */ +class SuccessResult extends GenericResult { + // Do not change this from 0 + public static $RESULT_SUCCESS = 0; + + /** + * A new success result + * + * @param string $result The result + */ + public function __construct($result) { + $this->setResult($result); + $this->setStatusCode(SuccessResult::$RESULT_SUCCESS); + } + + /** + * Returns a new instance of this class + * + * @param unknown $result A result of some kind? + * + * @return SuccessResult + */ + public static function getInstance($result) { + // Return a new error object. + return new SuccessResult($result); + } +} diff --git a/engine/classes/XMLRPCArrayParameter.php b/engine/classes/XMLRPCArrayParameter.php new file mode 100644 index 000000000..a8edccba7 --- /dev/null +++ b/engine/classes/XMLRPCArrayParameter.php @@ -0,0 +1,56 @@ +<?php + +/** + * An array containing other XMLRPCParameter objects. + * + * @package Elgg.Core + * @subpackage XMLRPC + * + */ +class XMLRPCArrayParameter extends XMLRPCParameter +{ + /** + * Construct an array. + * + * @param array $parameters Optional array of parameters, if not provided + * then addField must be used. + */ + function __construct($parameters = NULL) { + parent::__construct(); + + if (is_array($parameters)) { + foreach ($parameters as $v) { + $this->addField($v); + } + } + } + + /** + * Add a field to the container. + * + * @param XMLRPCParameter $value The value. + * + * @return void + */ + public function addField(XMLRPCParameter $value) { + if (!is_array($this->value)) { + $this->value = array(); + } + + $this->value[] = $value; + } + + /** + * Converts XML array to string + * + * @return string + */ + function __toString() { + $params = ""; + foreach ($this->value as $value) { + $params .= "$value"; + } + + return "<array><data>$params</data></array>"; + } +} diff --git a/engine/classes/XMLRPCBase64Parameter.php b/engine/classes/XMLRPCBase64Parameter.php new file mode 100644 index 000000000..7db0a761c --- /dev/null +++ b/engine/classes/XMLRPCBase64Parameter.php @@ -0,0 +1,28 @@ +<?php +/** + * A base 64 encoded blob of binary. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCBase64Parameter extends XMLRPCParameter { + /** + * Construct a base64 encoded block + * + * @param string $blob Unencoded binary blob + */ + function __construct($blob) { + parent::__construct(); + + $this->value = base64_encode($blob); + } + + /** + * Convert to string + * + * @return string + */ + function __toString() { + return "<value><base64>{$value}</base64></value>"; + } +} diff --git a/engine/classes/XMLRPCBoolParameter.php b/engine/classes/XMLRPCBoolParameter.php new file mode 100644 index 000000000..607841cb8 --- /dev/null +++ b/engine/classes/XMLRPCBoolParameter.php @@ -0,0 +1,30 @@ +<?php +/** + * A boolean. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCBoolParameter extends XMLRPCParameter { + + /** + * New bool parameter + * + * @param bool $value Value + */ + function __construct($value) { + parent::__construct(); + + $this->value = (bool)$value; + } + + /** + * Convert to string + * + * @return string + */ + function __toString() { + $code = ($this->value) ? "1" : "0"; + return "<value><boolean>{$code}</boolean></value>"; + } +} diff --git a/engine/classes/XMLRPCCall.php b/engine/classes/XMLRPCCall.php new file mode 100644 index 000000000..fd28f1e3e --- /dev/null +++ b/engine/classes/XMLRPCCall.php @@ -0,0 +1,62 @@ +<?php +/** + * An XMLRPC call + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCCall { + /** Method name */ + private $methodname; + + /** Parameters */ + private $params; + + /** + * Construct a new XML RPC Call + * + * @param string $xml XML + */ + function __construct($xml) { + $this->parse($xml); + } + + /** + * Return the method name associated with the call. + * + * @return string + */ + public function getMethodName() { return $this->methodname; } + + /** + * Return the parameters. + * Returns a nested array of XmlElement. + * + * @see XmlElement + * @return array + */ + public function getParameters() { return $this->params; } + + /** + * Parse the xml into its components according to spec. + * This first version is a little primitive. + * + * @param string $xml XML + * + * @return void + */ + private function parse($xml) { + $xml = xml_to_object($xml); + + // sanity check + if ((isset($xml->name)) && (strcasecmp($xml->name, "methodCall") != 0)) { + throw new CallException(elgg_echo('CallException:NotRPCCall')); + } + + // method name + $this->methodname = $xml->children[0]->content; + + // parameters + $this->params = $xml->children[1]->children; + } +} diff --git a/engine/classes/XMLRPCDateParameter.php b/engine/classes/XMLRPCDateParameter.php new file mode 100644 index 000000000..93bbbd8f5 --- /dev/null +++ b/engine/classes/XMLRPCDateParameter.php @@ -0,0 +1,33 @@ +<?php +/** + * An ISO8601 data and time. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCDateParameter extends XMLRPCParameter { + /** + * Construct a date + * + * @param int $timestamp The unix timestamp, or blank for "now". + */ + function __construct($timestamp = 0) { + parent::__construct(); + + $this->value = $timestamp; + + if (!$timestamp) { + $this->value = time(); + } + } + + /** + * Convert to string + * + * @return string + */ + function __toString() { + $value = date('c', $this->value); + return "<value><dateTime.iso8601>{$value}</dateTime.iso8601></value>"; + } +} diff --git a/engine/classes/XMLRPCDoubleParameter.php b/engine/classes/XMLRPCDoubleParameter.php new file mode 100644 index 000000000..b7834650e --- /dev/null +++ b/engine/classes/XMLRPCDoubleParameter.php @@ -0,0 +1,29 @@ +<?php +/** + * A double precision signed floating point number. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCDoubleParameter extends XMLRPCParameter { + + /** + * New XML Double + * + * @param int $value Value + */ + function __construct($value) { + parent::__construct(); + + $this->value = (float)$value; + } + + /** + * Convert to string + * + * @return string + */ + function __toString() { + return "<value><double>{$this->value}</double></value>"; + } +} diff --git a/engine/classes/XMLRPCErrorResponse.php b/engine/classes/XMLRPCErrorResponse.php new file mode 100644 index 000000000..425c075cc --- /dev/null +++ b/engine/classes/XMLRPCErrorResponse.php @@ -0,0 +1,36 @@ +<?php + +/** + * XMLRPC Error Response + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCErrorResponse extends XMLRPCResponse { + /** + * Set the error response and error code. + * + * @param string $message The message + * @param int $code Error code (default = system error as defined by + * http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php) + */ + function __construct($message, $code = -32400) { + $this->addParameter( + new XMLRPCStructParameter( + array ( + 'faultCode' => new XMLRPCIntParameter($code), + 'faultString' => new XMLRPCStringParameter($message) + ) + ) + ); + } + + /** + * Output to XML. + * + * @return string + */ + public function __toString() { + return "<methodResponse><fault><value>{$this->parameters[0]}</value></fault></methodResponse>"; + } +} diff --git a/engine/classes/XMLRPCIntParameter.php b/engine/classes/XMLRPCIntParameter.php new file mode 100644 index 000000000..0fc146165 --- /dev/null +++ b/engine/classes/XMLRPCIntParameter.php @@ -0,0 +1,29 @@ +<?php +/** + * An Integer. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCIntParameter extends XMLRPCParameter { + + /** + * A new XML int + * + * @param int $value Value + */ + function __construct($value) { + parent::__construct(); + + $this->value = (int)$value; + } + + /** + * Convert to string + * + * @return string + */ + function __toString() { + return "<value><i4>{$this->value}</i4></value>"; + } +} diff --git a/engine/classes/XMLRPCParameter.php b/engine/classes/XMLRPCParameter.php new file mode 100644 index 000000000..ffbad8082 --- /dev/null +++ b/engine/classes/XMLRPCParameter.php @@ -0,0 +1,16 @@ +<?php +/** + * Superclass for all RPC parameters. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +abstract class XMLRPCParameter { + protected $value; + + /** + * Set initial values + */ + function __construct() { } + +} diff --git a/engine/classes/XMLRPCResponse.php b/engine/classes/XMLRPCResponse.php new file mode 100644 index 000000000..a6256d385 --- /dev/null +++ b/engine/classes/XMLRPCResponse.php @@ -0,0 +1,71 @@ +<?php + +/** + * XML-RPC Response. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +abstract class XMLRPCResponse { + /** An array of parameters */ + protected $parameters = array(); + + /** + * Add a parameter here. + * + * @param XMLRPCParameter $param The parameter. + * + * @return void + */ + public function addParameter(XMLRPCParameter $param) { + if (!is_array($this->parameters)) { + $this->parameters = array(); + } + + $this->parameters[] = $param; + } + + /** + * Add an integer + * + * @param int $value Value + * + * @return void + */ + public function addInt($value) { + $this->addParameter(new XMLRPCIntParameter($value)); + } + + /** + * Add a string + * + * @param string $value Value + * + * @return void + */ + public function addString($value) { + $this->addParameter(new XMLRPCStringParameter($value)); + } + + /** + * Add a double + * + * @param int $value Value + * + * @return void + */ + public function addDouble($value) { + $this->addParameter(new XMLRPCDoubleParameter($value)); + } + + /** + * Add a boolean + * + * @param bool $value Value + * + * @return void + */ + public function addBoolean($value) { + $this->addParameter(new XMLRPCBoolParameter($value)); + } +} diff --git a/engine/classes/XMLRPCStringParameter.php b/engine/classes/XMLRPCStringParameter.php new file mode 100644 index 000000000..35b28214b --- /dev/null +++ b/engine/classes/XMLRPCStringParameter.php @@ -0,0 +1,30 @@ +<?php +/** + * A string. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCStringParameter extends XMLRPCParameter { + + /** + * A new XML string + * + * @param string $value Value + */ + function __construct($value) { + parent::__construct(); + + $this->value = $value; + } + + /** + * Convert to XML string + * + * @return string + */ + function __toString() { + $value = htmlentities($this->value); + return "<value><string>{$value}</string></value>"; + } +} diff --git a/engine/classes/XMLRPCStructParameter.php b/engine/classes/XMLRPCStructParameter.php new file mode 100644 index 000000000..694ddf5df --- /dev/null +++ b/engine/classes/XMLRPCStructParameter.php @@ -0,0 +1,55 @@ +<?php + +/** + * A structure containing other XMLRPCParameter objects. + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCStructParameter extends XMLRPCParameter { + /** + * Construct a struct. + * + * @param array $parameters Optional associated array of parameters, if + * not provided then addField must be used. + */ + function __construct($parameters = NULL) { + parent::__construct(); + + if (is_array($parameters)) { + foreach ($parameters as $k => $v) { + $this->addField($k, $v); + } + } + } + + /** + * Add a field to the container. + * + * @param string $name The name of the field. + * @param XMLRPCParameter $value The value. + * + * @return void + */ + public function addField($name, XMLRPCParameter $value) { + if (!is_array($this->value)) { + $this->value = array(); + } + + $this->value[$name] = $value; + } + + /** + * Convert to string + * + * @return string + */ + function __toString() { + $params = ""; + foreach ($this->value as $k => $v) { + $params .= "<member><name>$k</name>$v</member>"; + } + + return "<value><struct>$params</struct></value>"; + } +} diff --git a/engine/classes/XMLRPCSuccessResponse.php b/engine/classes/XMLRPCSuccessResponse.php new file mode 100644 index 000000000..e02e82c5c --- /dev/null +++ b/engine/classes/XMLRPCSuccessResponse.php @@ -0,0 +1,22 @@ +<?php +/** + * Success Response + * + * @package Elgg.Core + * @subpackage XMLRPC + */ +class XMLRPCSuccessResponse extends XMLRPCResponse { + /** + * Output to XML. + * + * @return string + */ + public function __toString() { + $params = ""; + foreach ($this->parameters as $param) { + $params .= "<param>$param</param>\n"; + } + + return "<methodResponse><params>$params</params></methodResponse>"; + } +} diff --git a/engine/classes/XmlElement.php b/engine/classes/XmlElement.php new file mode 100644 index 000000000..280bba664 --- /dev/null +++ b/engine/classes/XmlElement.php @@ -0,0 +1,20 @@ +<?php +/** + * A class representing an XML element for import. + * + * @package Elgg.Core + * @subpackage XML + */ +class XmlElement { + /** The name of the element */ + public $name; + + /** The attributes */ + public $attributes; + + /** CData */ + public $content; + + /** Child elements */ + public $children; +}; |