1: <?php
2: /**
3: * The Ansel_Tagger:: class wraps Ansel's interaction with the Content/Tagger
4: * system.
5: *
6: * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
7: *
8: * See the enclosed file COPYING for license information (GPL). If you
9: * did not receive this file, see http://www.horde.org/licenses/gpl.
10: *
11: * @author Michael J Rubinsky <mrubinsk@horde.org>
12: * @category Horde
13: * @license http://www.horde.org/licenses/gpl GPL
14: * @package Ansel
15: */
16: class Ansel_Tagger
17: {
18: /**
19: * Local cache of the type name => ids from Content, so we don't have to
20: * query for them each time.
21: *
22: * @var array
23: */
24: protected $_type_ids = array();
25:
26: /**
27: * Local reference to the tagger.
28: *
29: * @var Content_Tagger
30: */
31: protected $_tagger;
32:
33: /**
34: * Constructor.
35: *
36: * @return Ansel_Tagger
37: */
38: public function __construct(Content_Tagger $tagger)
39: {
40: // Remember the types to avoid having Content query them again.
41: $key = 'ansel.tagger.type_ids';
42: $ids = $GLOBALS['injector']->getInstance('Horde_Cache')->get($key, 360);
43: if ($ids) {
44: $this->_type_ids = unserialize($ids);
45: } else {
46: $type_mgr = $GLOBALS['injector']
47: ->getInstance('Content_Types_Manager');
48: try {
49: $types = $type_mgr->ensureTypes(array('gallery', 'image'));
50: } catch (Content_Exception $e) {
51: throw new Ansel_Exception($e);
52: }
53: $this->_type_ids = array(
54: 'gallery' => (int)$types[0],
55: 'image' => (int)$types[1]);
56: $GLOBALS['injector']->getInstance('Horde_Cache')
57: ->set($key, serialize($this->_type_ids));
58: }
59:
60: $this->_tagger = $tagger;
61: }
62:
63: /**
64: * Tags an ansel object with any number of tags.
65: *
66: * @param string $localId The identifier of the ansel object.
67: * @param string|array $tags Either a single tag string or an array of
68: * tags.
69: * @param string $owner The tag owner (should normally be the owner
70: * of the resource).
71: * @param string $content_type The type of object we are tagging
72: * (gallery/image).
73: *
74: * @return void
75: * @throws Ansel_Exception
76: */
77: public function tag($localId, $tags, $owner, $content_type = 'image')
78: {
79: if (empty($tags)) {
80: return;
81: }
82:
83: if (!is_array($tags)) {
84: $tags = $this->_tagger->splitTags($tags);
85: }
86:
87: try {
88: $this->_tagger->tag(
89: $owner,
90: array(
91: 'object' => $localId,
92: 'type' => $this->_type_ids[$content_type]),
93: $tags);
94: } catch (Content_Exception $e) {
95: throw new Ansel_Exception($e);
96: }
97: }
98:
99: /**
100: * Retrieves the tags on given object(s).
101: *
102: * @param mixed $localId Either the identifier of the ansel object or
103: * an array of identifiers.
104: * @param string $type The type of object $localId represents.
105: *
106: * @return array A tag_id => tag_name hash, possibly wrapped in a localid hash.
107: * @throws Ansel_Exception
108: */
109: public function getTags($localId, $type = 'image')
110: {
111: try {
112: if (is_array($localId)) {
113: foreach ($localId as &$lid) {
114: $lid = (string)$lid;
115: }
116: return $this->_tagger->getTagsByObjects($localId, $type);
117: }
118:
119: return $this->_tagger->getTags(
120: array(
121: 'objectId' => array(
122: 'object' => (string)$localId,
123: 'type' => $this->_type_ids[$type]
124: )
125: )
126: );
127: } catch (Content_Exception $e) {
128: throw new Ansel_Exception($e);
129: }
130: }
131:
132: /**
133: * Retrieve a set of tags that are related to the specifed set. A tag is
134: * related if resources tagged with the specified set are also tagged with
135: * the tag being considered. Used to "browse" tagged resources.
136: *
137: * @param array $tags An array of tags to check. This would represent the
138: * current "directory" of tags while browsing.
139: * @param string $user The resource must be owned by this user.
140: *
141: * @return array An tag_id => tag_name hash
142: */
143: public function browseTags($tags, $user)
144: {
145: try {
146: $tags = array_values($this->_tagger->getTagIds($tags));
147: $gtags = $this->_tagger->browseTags($tags, $this->_type_ids['gallery'], $user);
148: $itags = $this->_tagger->browseTags($tags, $this->_type_ids['image'], $user);
149: } catch (Content_Exception $e) {
150: throw new Ansel_Exception($e);
151: }
152: /* Can't use array_merge here since it would renumber the array keys */
153: foreach ($gtags as $id => $name) {
154: if (empty($itags[$id])) {
155: $itags[$id] = $name;
156: }
157: }
158:
159: return $itags;
160: }
161:
162: /**
163: * Get tag ids for the specified tag names.
164: *
165: * @param string|array $tags Either a tag_name or array of tag_names.
166: *
167: * @return array A tag_id => tag_name hash.
168: * @throws Ansel_Exception
169: */
170: public function getTagIds($tags)
171: {
172: try {
173: return $this->_tagger->getTagIds($tags);
174: } catch (Content_Exception $e) {
175: throw new Ansel_Exception($e);
176: }
177: }
178:
179: /**
180: * Untag a resource.
181: *
182: * Removes the tag regardless of the user that added the tag.
183: *
184: * @param string $localId The ansel object identifier.
185: * @param mixed $tags Either a tag_id, tag_name or an array.
186: * @param string $content_type The type of object that $localId represents.
187: *
188: * @return void
189: */
190: public function untag($localId, $tags, $content_type = 'image')
191: {
192: try {
193: $this->_tagger->removeTagFromObject(
194: array('object' => $localId, 'type' => $this->_type_ids[$content_type]), $tags);
195: } catch (Content_Exception $e) {
196: throw new Ansel_Exception($e);
197: }
198: }
199:
200: /**
201: * Tags the given resource with *only* the tags provided, removing any
202: * tags that are already present but not in the list.
203: *
204: * @param string $localId The identifier for the ansel object.
205: * @param mixed $tags Either a tag_id, tag_name, or array of tag_ids.
206: * @param string $owner The tag owner - should normally be the resource
207: * owner.
208: * @param $content_type The type of object that $localId represents.
209: */
210: public function replaceTags($localId, $tags, $owner, $content_type = 'image')
211: {
212: /* First get a list of existing tags. */
213: $existing_tags = $this->getTags($localId, $content_type);
214:
215: if (!is_array($tags)) {
216: $tags = $this->_tagger->splitTags($tags);
217: }
218: $remove = array();
219: foreach ($existing_tags as $tag_id => $existing_tag) {
220: $found = false;
221: foreach ($tags as $tag_text) {
222: if ($existing_tag == $tag_text) {
223: $found = true;
224: break;
225: }
226: }
227: /* Remove any tags that were not found in the passed in list. */
228: if (!$found) {
229: $remove[] = $tag_id;
230: }
231: }
232:
233: $this->untag($localId, $remove, $content_type);
234: $add = array();
235: foreach ($tags as $tag_text) {
236: $found = false;
237: foreach ($existing_tags as $existing_tag) {
238: if ($tag_text == $existing_tag) {
239: $found = true;
240: break;
241: }
242: }
243: if (!$found) {
244: $add[] = $tag_text;
245: }
246: }
247:
248: $this->tag($localId, $add, $owner, $content_type);
249: }
250:
251: /**
252: * Returns tags belonging to the current user beginning with $token.
253: *
254: * Used for autocomplete code.
255: *
256: * @param string $token The token to match the start of the tag with.
257: *
258: * @return array A tag_id => tag_name hash
259: * @throws Ansel_Exception
260: */
261: public function listTags($token)
262: {
263: try {
264: return $this->_tagger->getTags(array('q' => $token, 'userId' => $GLOBALS['registry']->getAuth()));
265: } catch (Content_Tagger $e) {
266: throw new Ansel_Exception($e);
267: }
268: }
269:
270: /**
271: * Returns the data needed to build a tag cloud based on the specified
272: * user's tag dataset.
273: *
274: * @param string $user The user whose tags should be included.
275: * If null, all users' tags are returned.
276: * @param integer $limit The maximum number of tags to include.
277: *
278: * @return Array An array of hashes, each containing tag_id, tag_name, and count.
279: * @throws Ansel_Exception
280: */
281: public function getCloud($user, $limit = 5)
282: {
283: $filter = array('limit' => $limit,
284: 'typeId' => array_values($this->_type_ids));
285: if (!empty($user)) {
286: $filter['userId'] = $user;
287: }
288: try {
289: return $this->_tagger->getTagCloud($filter);
290: } catch (Content_Exception $e) {
291: throw new Ansel_Exception($e);
292: }
293: }
294:
295: /**
296: * Returns cloud-like information, but only for a specified set of tags.
297: * Useful for displaying the counts of other images tagged with the same
298: * tag as the currently displayed image.
299: *
300: * @param array $tags An array of either tag names or ids.
301: * @param integer $limit Limit results to this many.
302: *
303: * @return array An array of hashes, tag_id, tag_name, and count.
304: * @throws Ansel_Exception
305: */
306: public function getTagInfo($tags = null, $limit = 500, $type = null, $user = null)
307: {
308: $filter = array(
309: 'typeId' => empty($type) ? array_values($this->_type_ids) : $this->_type_ids[$type],
310: 'tagIds' => $tags,
311: 'limit' => $limit,
312: 'userId' => $user);
313:
314: try {
315: return $this->_tagger->getTagCloud($filter);
316: } catch (Content_Exception $e) {
317: throw new Ansel_Exception($e);
318: }
319: }
320:
321: /**
322: * Searches for resources that are tagged with all of the requested tags.
323: *
324: * @param array $tags Either a tag_id, tag_name or an array.
325: * @param array $filter Array of filter parameters.
326: * - type (string) - 'gallery' or 'image'
327: * - user (array) - only include objects owned by
328: * these users.
329: *
330: * @return A hash of 'gallery' and 'image' ids.
331: * @throws Ansel_Exception
332: */
333: public function search($tags, $filter = array())
334: {
335: $args = array();
336:
337: /* These filters are mutually exclusive */
338: if (!empty($filter['user'])) {
339: $args['userId'] = $filter['user'];
340: } elseif (!empty($filter['gallery'])) {
341: // Only events located in specific galleries
342: if (!is_array($filter['gallery'])) {
343: $filter['gallery'] = array($filter['gallery']);
344: }
345: $args['gallery'] = $filter['gallery'];
346: }
347:
348: try {
349: /* Add the tags to the search */
350: $args['tagId'] = $this->_tagger->getTagIds($tags);
351:
352: /* Restrict to images or galleries */
353: $gal_results = $image_results = array();
354: if (empty($filter['type']) || $filter['type'] == 'gallery') {
355: $args['typeId'] = $this->_type_ids['gallery'];
356: $gal_results = $this->_tagger->getObjects($args);
357: }
358:
359: if (empty($filter['type']) || $filter['type'] == 'image') {
360: $args['typeId'] = $this->_type_ids['image'];
361: $image_results = $this->_tagger->getObjects($args);
362: }
363: } catch (Content_Exception $e) {
364: throw new Ansel_Exception($e);
365: }
366:
367: /* TODO: Filter out images whose gallery has already matched? */
368: $results = array('galleries' => array_values($gal_results),
369: 'images' => array_values($image_results));
370:
371: return $results;
372: }
373:
374: /**
375: * List image ids of images related (via similar tags) to the specified
376: * image
377: *
378: * @param Ansel_Image $image The image to get related images for.
379: * @param bolean $ownerOnly Only return images owned by the specified
380: * image's owner.
381: *
382: * @return array An array of 'image' and 'rank' keys..
383: */
384: public function listRelatedImages(Ansel_Image $image, $ownerOnly = true)
385: {
386: $args = array('typeId' => 'image', 'limit' => 10);
387: if ($ownerOnly) {
388: $gallery = $GLOBALS['injector']->getInstance('Ansel_Storage')->getGallery($image->gallery);
389: $args['userId'] = $gallery->get('owner');
390: }
391:
392: try {
393: $ids = $GLOBALS['injector']->getInstance('Content_Tagger')->getSimilarObjects(array('object' => (string)$image->id, 'type' => 'image'), $args);
394: } catch (Content_Exception $e) {
395: throw new Ansel_Exception($e);
396: }
397:
398: if (count($ids) == 0) {
399: return array();
400: }
401:
402: try {
403: $images = $GLOBALS['injector']->getInstance('Ansel_Storage')->getImages(array('ids' => array_keys($ids)));
404: } catch (Horde_Exception_NotFound $e) {
405: $images = array();
406: }
407:
408: $results = array();
409: foreach ($images as $key => $image) {
410: $results[] = array('image' => $image, 'rank' => $ids[$key]);
411: }
412: return $results;
413: }
414: }
415: