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