diff options
Diffstat (limited to 'mod/linkup/start.php')
| -rw-r--r-- | mod/linkup/start.php | 413 | 
1 files changed, 413 insertions, 0 deletions
| diff --git a/mod/linkup/start.php b/mod/linkup/start.php new file mode 100644 index 000000000..05e4bdb66 --- /dev/null +++ b/mod/linkup/start.php @@ -0,0 +1,413 @@ +<?php +/** + * Linkup -- Simple markup to link entities across applications. + * + * It recognizes: @username, !group-alias, !group-guid, #task-guid, #hashtag, + * and *entity-guid. + * + * @package        Lorea + * @subpackage     Linkup + * @homepage       http://lorea.org/plugin/linkup + * @copyright      2012,2013 Lorea Faeries <federation@lorea.org> + * @license        COPYING, http://www.gnu.org/licenses/agpl + * + * Copyright 2012-2013 Lorea Faeries <federation@lorea.org> + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + */ + +elgg_register_event_handler('init', 'system', 'linkup_init'); + +function linkup_init() { +    // Register tests +    elgg_register_plugin_hook_handler('unit_test', 'system', 'linkup_test'); + +	// Register CSS +	elgg_extend_view('css.elgg', 'linkup/css'); + +    // Register hook +    elgg_register_plugin_hook_handler('output:before', 'layout', 'linkup_event_handler'); + +    // Extend CSS +    elgg_extend_view('css/elgg', 'linkup/css'); +} + +/** + * @todo I'm ashamed + */ +function linkup_test($hook, $type, $value, $params) { +	//    $value[] = elgg_get_config('pluginspath') . "linkup/tests/linkup_test.php"; +    return $value; +} + +/** + * Markup function + * + * Detect linkup patterns and make links where needed. + * + * Matches a marker character followed by printable characters, and + * prefixed to avoid matching CSS DOM IDs. First match is the prefix, + * second the marker, and third is the subject. + */ +function linkup_event_handler($hook, $type, $returnvalue, $params) { + +	if (elgg_get_viewtype() != 'default') { +		// only mess with html views +		return $returnvalue; +	} +	$html = mb_convert_encoding($returnvalue['content'], 'HTML-ENTITIES', 'UTF-8'); + +	if (empty($html)) { +		return $returnvalue; +	} + +	libxml_use_internal_errors(TRUE); +	 +	try { + +		$dom  = new DOMDocument(); +		$dom->loadHTML($html); + +		foreach ($dom->childNodes AS $node) { + +			linkup_dom_recurse($node, 'linkup_dom_replace'); + +		} + +		// Remove tags added by DOMDocument +		$matches = array('/^\<\!DOCTYPE.*?<html[^>]*><body[^>]*>/isu', +						 '|</body></html>$|isu'); + +		$content = preg_replace($matches, '', $dom->saveHTML()); + +		// convert back to original encoding +		$returnvalue['content'] = mb_convert_encoding($content, 'UTF-8', 'HTML-ENTITIES'); + +	} catch (Exception $e) { +	    +		error_log("===== linkup error $e->class $e->message"); + +	} + +	return $returnvalue; + +} + +/** + * Utility to traverse a DOMDocument and execute a callback function + * on all children. + * + * @param $root      a DOMNode + * @param $callback  a function + * @return void + */ +function linkup_dom_recurse($root, $callback) { +	 +	if ($root->hasChildNodes()) { +		 +		foreach ($root->childNodes AS $node) { +			 +			linkup_dom_recurse($node, $callback); + +		} + +	} else { + +		if (is_callable($callback)) { + +			$callback($root); + +		} +	}	 + +} + +/** + * linkup marking callback will markup only a safe set of conditions: + * + * - only TextNodes will be affected + * - only if their parent is not an attribute (e.g., title, href) + * - only if the parent tag can contain an A tag  + * + * @internal + * @see linkup_skip_tags() + */ +function linkup_dom_replace($node) { + +	if ($node->nodeType == XML_TEXT_NODE  +		&& $node->parentNode->nodeType != XML_ATTRIBUTE_NODE +		&& !in_array($node->parentNode->tagName, linkup_skip_tags())) { + +		$html = linkup_regexp($node->nodeValue); + +		if (!empty($html) && $node->nodeValue != $html) { + +			// Get what's inside the formatted <body> node from a new DOMDocument +			$fix = new DOMDocument(); +			$fix->loadHTML(trim($html)); +			// get <body>(.*)</body> +			$fix = $fix->documentElement->firstChild; + +			if (!is_null($fix)) { +				$parent   = $node->parentNode; +				$new_node = $parent->ownerDocument->importNode($fix, TRUE); +				$parent->replaceChild($new_node, $node); +			} + +		} +		 +	} + +} + +/** + * linkup_regexp -- markup text with HTML links + * + * This function expects plain text with a specific markup. + * It is called internally by the event handler to execute on selected + * strings: that means, do not try it on any HTML. + * + * @see README.org for markup details + * + * @internal + * @param String $text + * @return String HTML markup as a string + */ +function linkup_regexp($text) { +	return preg_replace_callback("/(^|[^\w&\"\]])([@!\#\*:])([a-z0-9+-]+)(\b)/u", +								 "linkup_markup", $text); + +} + +/** + * linkup_skip_tags -- tags to skip wjen looking for markup + * + * @todo the whole range of tag that can contain A are 'flow elements' + * and 'phrasing elements' as defined at + * + * http://dev.w3.org/html5/markup/common-models.html + */ +function linkup_skip_tags() { +	static $skip = array('a',  +						 'button', +						 'canvas', +						 'class', +						 'code', +						 'embed', +						 'head', +						 'iframe', +						 'input', +						 'link', +						 'meta', +						 'object', +						 'param', +						 'pre', +						 'script', +						 'style', +						 'textarea', +						 ''); + +	return $skip; +} + +/** + * Markup callback + * + * Generate HTML markup from linkup patterns, for a single matched + * pattern. It is used as a regular expression callback. + * + * It also takes care of permissions and entity validity, so that no + * link will be made to unauthorized or non-existing entities. + *  + * @return String, the resulting markup + */ +function linkup_markup($matches) { + +    $prefix  = $matches[1]; +    $marker  = $matches[2]; +    $subject = $matches[3]; +    $text    = "$marker$subject"; +     +    switch($marker) { +    case "@": // user +        if (preg_match("/^[a-z][\w\.-]+/i", $subject)) { +            $entity = get_user_by_username($subject); +            $text   = linkup_markup_user($entity, $text); +            // TODO trigger_plugin_hook('mention', 'user'); +        } +        break; + +    case "!": // group +        if (preg_match("/^[0-9]+$/", $subject)) { +            // Get group by GUID +            $entity = get_entity($subject); +            // TODO if user can access group +        } else if (preg_match("/^[a-z][\w+-]+$/i", $subject)) { +            // Get group by alias +            if (!elgg_is_active_plugin('group_alias')) { break; } +            $entity = get_group_from_group_alias($subject); +        } +        $text = linkup_markup_group($entity, $text); +        break; + +    case "#": // hashtag or task +		if (preg_match("/^[0-9]+$/", $subject) && elgg_is_active_plugin('tasks')) { +        	// Get task by GUID +			$entity = get_entity($subject); +			if (elgg_instanceof($entity, 'object', 'task') || +				elgg_instanceof($entity, 'object', 'tasklist') || +				elgg_instanceof($entity, 'object', 'tasklist_top')) { +				// Linkup task +				$text = linkup_markup_task($entity, $text); +				break; +			} +		} +        // Linkup hashtag +        $text = linkup_link($text, "hashtag", "/tag/$subject"); +        break; + +    case "*": // entity +        // Check that the subject is a GUID and current user can access +        // the corresponding entity +        if (!preg_match("/^[0-9]+$/", $subject)) { break; } +        $entity = get_entity($subject); +        if (elgg_instanceof($entity, 'user')) { +            $text = linkup_markup_user($entity); +        } +		else if (elgg_instanceof($entity, 'group')) { +            $text = linkup_markup_group($entity); +        } +		else if (elgg_instanceof($entity, 'object', 'task') || +				elgg_instanceof($entity, 'object', 'tasklist') || +				elgg_instanceof($entity, 'object', 'tasklist_top')) { +			$text = linkup_markup_task($entity, $text); +		} +		else if (elgg_instanceof($entity, 'object')) { +			// get subtype, check permissions, linkup +			if (!elgg_trigger_plugin_hook('linkup', "object:{$entity->getSubtype()}", array('entity' => $entity))) { +				// Default object view +				$text = linkup_markup_object($entity); +			} +        } +        break; +    } + +    return "$prefix$text"; +} + + +// TODO Move the following to a library + +/** + * Generic linkup markup + * + */ +function linkup_link($anchor, $css, $url, $title = "") { + +    $vars = array('class' => "linkup $css", 'href' => $url, 'text' => $anchor); +    if (!empty($title)) { +        $vars['title'] = $title; +    } + +    return elgg_view('output/url', $vars); + +} + +/** + * Markup for a group + */ +function linkup_markup_group($entity, $default = "") { + +    if (elgg_instanceof($entity, 'group')) { +        if (elgg_is_active_plugin('group_alias')) { +            $anchor = "!{$entity->alias}"; +        } else { +            $anchor = "!{$entity->username}"; +        } +        $css     = "group"; +        $url     = $entity->getURL(); + +        return linkup_link($anchor, $css, $url, elgg_echo('group') . ": {$entity->name}"); +    } +    return $default; +} + +/** + * Markup for a generic object + */ +function linkup_markup_object($entity, $default = "") { + +    if (elgg_instanceof($entity, 'object') && $entity->isEnabled()) { + +		$subtype = $entity->getSubtype(); +		if (empty($subtype)) { +			$subtype = 'default'; +		} + +        $anchor = "{$entity->name}"; +        $css    = elgg_get_friendly_title("object-{$subtype}"); +        $url    = $entity->getURL(); + +        return linkup_link($anchor, $css, $url, elgg_echo("linkup:object:$subtype", array($entity->guid))); +    } + +    return $default; +} + +/** + * Markup for a task + */ +function linkup_markup_task($entity, $default = "") { + +	if (!elgg_instanceof($entity, 'object')) { +		return $default; +	} + +	$subtypes = array('task', 'tasklist', 'tasklist_top'); +	$subtype  = $entity->getSubtype(); + +	if (!in_array($subtype, $subtypes)) { +		return $default; +	} + +	$anchor = "#$entity->guid"; +	$css    = "$subtype"; +	if ('task' == $subtype) { +		$css.= " task-status-{$entity->status}"; +	} +	$url    = $entity->getURL(); +	$title  = elgg_echo('task') . " $entity->title"; + +	return linkup_link($anchor, $css, $url, $title); + +} + +/** + * Markup for a user + */ +function linkup_markup_user($entity, $default = "") { + +    if (elgg_instanceof($entity, 'user') && !$entity->isBanned()) { + +        $anchor = "@{$entity->username}"; +        $css    = "user"; +        $url    = $entity->getURL(); + +        return linkup_link($anchor, $css, $url, $entity->name); +    } + +    return $default; +} + | 
