1: <?php
2: /**
3: * Kronolith interface to the Horde_Content tagger
4: *
5: * Copyright 2009-2012 Horde LLC (http://www.horde.org/)
6: *
7: * @author Michael J. Rubinsky <mrubinsk@horde.org>
8: *
9: * @package Kronolith
10: */
11: class Kronolith_Tagger
12: {
13: /**
14: * Local cache of the type name => ids from Content, so we don't have to
15: * query for them each time.
16: *
17: * @var array
18: */
19: protected $_type_ids = array();
20:
21: /**
22: * Constructor.
23: *
24: * @return Kronolith_Tagger
25: */
26: public function __construct()
27: {
28: // Remember the types to avoid having Content query them again.
29: $key = 'kronolith.tagger.type_ids';
30: $ids = $GLOBALS['injector']->getInstance('Horde_Cache')->get($key, 360);
31: if ($ids) {
32: $this->_type_ids = unserialize($ids);
33: } else {
34: $type_mgr = $GLOBALS['injector']->getInstance('Content_Types_Manager');
35: $types = $type_mgr->ensureTypes(array('calendar', 'event'));
36: $this->_type_ids = array('calendar' => (int)$types[0],
37: 'event' => (int)$types[1]);
38: $GLOBALS['injector']->getInstance('Horde_Cache')->set($key, serialize($this->_type_ids));
39: }
40: }
41:
42: /**
43: * Tags a kronolith object with any number of tags.
44: *
45: * @param string $localId The identifier of the kronolith object.
46: * @param mixed $tags Either a single tag string or an array of
47: * tags.
48: * @param string $owner The tag owner (should normally be the owner
49: * of the resource).
50: * @param string $content_type The type of object we are tagging
51: * (event/calendar).
52: *
53: * @return void
54: */
55: public function tag($localId, $tags, $owner, $content_type = 'event')
56: {
57: // If we don't have an array - split the string.
58: if (!is_array($tags)) {
59: $tags = $GLOBALS['injector']->getInstance('Content_Tagger')->splitTags($tags);
60: }
61:
62: $GLOBALS['injector']->getInstance('Content_Tagger')->tag(
63: $owner,
64: array('object' => $localId,
65: 'type' => $this->_type_ids[$content_type]),
66: $tags);
67: }
68:
69: /**
70: * Retrieves the tags on given object(s).
71: *
72: * @param mixed $localId Either the identifier of the kronolith object or
73: * an array of identifiers.
74: * @param string $type The type of object $localId represents.
75: *
76: * @return array A tag_id => tag_name hash, possibly wrapped in a localid hash.
77: */
78: public function getTags($localId, $type = 'event')
79: {
80: if (is_array($localId)) {
81: return $GLOBALS['injector']->getInstance('Content_Tagger')->getTagsByObjects($localId, $type);
82: }
83:
84: return $GLOBALS['injector']->getInstance('Content_Tagger')->getTags(array('objectId' => array('object' => $localId, 'type' => $this->_type_ids[$type])));
85: }
86:
87: /**
88: * Removes a tag from a kronolith object.
89: *
90: * Removes *all* tags - regardless of the user that added the tag.
91: *
92: * @param string $localId The kronolith object identifier.
93: * @param mixed $tags Either a tag_id, tag_name or an array of
94: * ids or names to remove.
95: * @param string $content_type The type of object that $localId represents.
96: */
97: public function untag($localId, $tags, $content_type = 'event')
98: {
99: $GLOBALS['injector']->getInstance('Content_Tagger')->removeTagFromObject(
100: array('object' => $localId, 'type' => $this->_type_ids[$content_type]), $tags);
101: }
102:
103: /**
104: * Tags the given resource with *only* the tags provided, removing any
105: * tags that are already present but not in the list.
106: *
107: * @param string $localId The identifier for the kronolith object.
108: * @param mixed $tags Either a tag_id, tag_name, or array of tag_ids.
109: * @param string $owner The tag owner - should normally be the resource
110: * owner.
111: * @param $content_type The type of object that $localId represents.
112: */
113: public function replaceTags($localId, $tags, $owner, $content_type = 'event')
114: {
115: // First get a list of existing tags.
116: $existing_tags = $this->getTags($localId, $content_type);
117:
118: // If we don't have an array - split the string.
119: if (!is_array($tags)) {
120: $tags = $GLOBALS['injector']->getInstance('Content_Tagger')->splitTags($tags);
121: }
122: $remove = array();
123: foreach ($existing_tags as $tag_id => $existing_tag) {
124: $found = false;
125: foreach ($tags as $tag_text) {
126: if ($existing_tag == $tag_text) {
127: $found = true;
128: break;
129: }
130: }
131: // Remove any tags that were not found in the passed in list.
132: if (!$found) {
133: $remove[] = $tag_id;
134: }
135: }
136:
137: $this->untag($localId, $remove, $content_type);
138: $add = array();
139: foreach ($tags as $tag_text) {
140: $found = false;
141: foreach ($existing_tags as $existing_tag) {
142: if ($tag_text == $existing_tag) {
143: $found = true;
144: break;
145: }
146: }
147: if (!$found) {
148: $add[] = $tag_text;
149: }
150: }
151:
152: $this->tag($localId, $add, $owner, $content_type);
153: }
154:
155: /**
156: * Searches for resources that are tagged with all of the requested tags.
157: *
158: * @param array $tags Either a tag_id, tag_name or an array.
159: * @param array $filter Array of filter parameters.
160: * - type (string) - only return either events or
161: * calendars, not both.
162: * - user (array) - only include objects owned by
163: * these users.
164: * - calendar (array) - restrict to events contained
165: * in these calendars.
166: *
167: * @return A hash of 'calendars' and 'events' that each contain an array
168: * of calendar_ids and event_uids respectively.
169: */
170: public function search($tags, $filter = array())
171: {
172: $args = array();
173:
174: /* These filters are mutually exclusive */
175: if (array_key_exists('user', $filter)) {
176: /* semi-hack to see if we are querying for a system-owned share -
177: * will need to get the list of all system owned shares and query
178: * using a calendar filter instead of a user filter. */
179: if (empty($filter['user'])) {
180: // @TODO: No way to get only the system shares the current
181: // user can see?
182: $calendars = $GLOBALS['kronolith_shares']->listSystemShares();
183: $args['calendarId'] = array();
184: foreach ($calendars as $name => $share) {
185: if ($share->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::READ)) {
186: $args['calendarId'][] = $name;
187: }
188: }
189: } else {
190: // Items owned by specific user(s)
191: $args['userId'] = $filter['user'];
192: }
193: } elseif (!empty($filter['calendar'])) {
194: // Only events located in specific calendar(s)
195: if (!is_array($filter['calendar'])) {
196: $filter['calendar'] = array($filter['calendar']);
197: }
198: $args['calendarId'] = $filter['calendar'];
199: }
200:
201: /* Add the tags to the search */
202: $args['tagId'] = $GLOBALS['injector']->getInstance('Content_Tagger')->getTagIds($tags);
203:
204: /* Restrict to events or calendars? */
205: $cal_results = $event_results = array();
206: if (empty($filter['type']) || $filter['type'] == 'calendar') {
207: $args['typeId'] = $this->_type_ids['calendar'];
208: $cal_results = $GLOBALS['injector']->getInstance('Content_Tagger')->getObjects($args);
209: }
210:
211: if (empty($filter['type']) || $filter['type'] == 'event') {
212: $args['typeId'] = $this->_type_ids['event'];
213: $event_results = $GLOBALS['injector']->getInstance('Content_Tagger')->getObjects($args);
214: }
215:
216: $results = array('calendars' => array_values($cal_results),
217: 'events' => (!empty($args['calendarId']) && count($event_results))
218: ? Kronolith::getDriver()->filterEventsByCalendar(array_values($event_results), $args['calendarId'])
219: : array_values($event_results));
220:
221: return $results;
222: }
223:
224: /**
225: * Returns tags belonging to the current user beginning with $token.
226: *
227: * Used for autocomplete code.
228: *
229: * @param string $token The token to match the start of the tag with.
230: *
231: * @return A tag_id => tag_name hash
232: */
233: public function listTags($token)
234: {
235: return $GLOBALS['injector']->getInstance('Content_Tagger')->getTags(
236: array('q' => $token, 'userId' => $GLOBALS['registry']->getAuth()));
237: }
238:
239: /**
240: * Returns the data needed to build a tag cloud based on the passed in
241: * user's tag data set.
242: *
243: * @param string $user The user whose tags should be included.
244: * @param integer $limit The maximum number of tags to include.
245: *
246: * @return An array of hashes, each containing tag_id, tag_name, and count.
247: */
248: public function getCloud($user, $limit = 5)
249: {
250: return $GLOBALS['injector']->getInstance('Content_Tagger')->getTagCloud(
251: array('userId' => $user, 'limit' => $limit));
252: }
253:
254: /**
255: * Returns cloud-like information, but only for a specified set of tags.
256: *
257: * @param array $tags An array of either tag names or ids.
258: * @param integer $limit Limit results to this many.
259: * @param string $type The type of resource (event, calendar, null).
260: * @param string $user Restrict results to those tagged by $user.
261: *
262: * @return array An array of hashes, tag_id, tag_name, and count.
263: * @throws Ansel_Exception
264: */
265: public function getTagInfo($tags = null, $limit = 500, $type = null, $user = null)
266: {
267: $filter = array(
268: 'typeId' => empty($type) ? array_values($this->_type_ids) : $this->_type_ids[$type],
269: 'tagIds' => $tags,
270: 'limit' => $limit,
271: 'userId' => $user
272: );
273:
274: try {
275: return $GLOBALS['injector']
276: ->getInstance('Content_Tagger')
277: ->getTagCloud($filter);
278: } catch (Content_Exception $e) {
279: throw new Ansel_Exception($e);
280: }
281: }
282:
283: }
284: