/[refeed]/trunk/library/RF/Controller.class.php
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Contents of /trunk/library/RF/Controller.class.php

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2 - (show annotations)
Wed Jul 5 00:27:49 2006 UTC (17 years, 10 months ago) by dpavlin
File size: 102930 byte(s)
make working copy of trunk
1 <?php
2 // vim: ts=4 foldcolumn=4 foldmethod=marker
3 /**
4 * RF_Controller class found here.
5 *
6 * This file is part of Reblog,
7 * a derivative work of Feed On Feeds.
8 *
9 * Distributed under the Gnu Public License.
10 *
11 * @package Refeed
12 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
13 * @author Michal Migurski <mike@stamen.com>
14 * @author Michael Frumin <mfrumin@eyebeam.org>
15 * @copyright ©2004 Michael Frumin, Michal Migurski
16 * @link http://reblog.org Reblog
17 * @link http://feedonfeeds.com Feed On Feeds
18 * @version $Revision: 1.41 $
19 */
20
21 /**
22 * RF_Controller primarily handles the database connection and most
23 * database interaction, and mediates connections between users and data.
24 *
25 * RF_Controller does most of the heavy-lifting, including:
26 * - Keeping of database and output interfaces
27 * - Linking users and user information, e.g. feed subscriptions
28 * - Retrieving feed and item model objects
29 * - Adding and saving feed and item model objects to the database
30 * - Checking for feed or item existence when processing new information
31 * - Modifying and retrieving per-user metadata for items and feeds
32 * - Keeping and invoking plugins
33 *
34 * Other controller classes provide
35 * {@link RF_Input_Controller RSS subscription code},
36 * {@link RF_Install_Controller installation procedures},
37 * and {@link RF_Userdata_Controller higher-level feed & item per-user metadata access}.
38 */
39 class RF_Controller
40 {
41 /**
42 * Database connection for reading
43 * @var DB_common
44 */
45 var $dbhr;
46
47 /**
48 * Database connection for writing
49 * @var DB_common
50 */
51 var $dbhw;
52
53 /**
54 * Flag determines whether transactions are supported in the current database or not.
55 * @var boolean
56 */
57 var $db_transactions = false;
58
59 /**
60 * Array of plug-ins object instances.
61 * @var array
62 */
63 var $plugins = array();
64
65 /**
66 * Flag determines whether to attempt to close database instances on request completion.
67 * @var boolean
68 */
69 var $no_dbh_disconnect = false;
70
71 /**
72 * @param DB_common $dbhr Database connection for reading
73 * @param DB_common $dbhw Database connection for writing
74 * @param array $args Optional arguments array
75 *
76 * @uses RF_Page::$controller Assigned after instantiation
77 * @uses RF_Controller::$no_dbh_disconnect Assigned based on arguments, defaults to false
78 * @uses RF_Controller::$dbhr Assigned on instantiation, from {@link DB_common $dbh}.
79 * @uses RF_Controller::$dbhw Assigned on instantiation, from {@link DB_common $dbh}.
80 * @uses RF_Controller::$db_transactions Assigned on instantiation, if database is tested to support START TRANSACTION.
81 * @uses RF_Controller::finishRequest() Designated a shutdown function.
82 */
83 function RF_Controller(&$dbhr, &$dbhw, $args = array())
84 {
85 // database handler; assuming to behave like PEAR::DB_common
86 $this->dbhr =& $dbhr;
87 $this->dbhw =& $dbhw;
88
89 $this->loadPlugins();
90 register_shutdown_function(array(&$this, 'finishRequest'));
91
92 if(strtolower(get_class($this->dbhw)) == 'db_mysql_logged') {
93
94 // don't use transactions when logging queries
95 $this->db_transactions = false;
96
97 } else {
98
99 // determine whether transactions are aupported by current MySQL
100 $result = $this->dbhw->query("START TRANSACTION"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
101
102 $this->db_transactions = (DB::isError($result) && $result->getCode() == DB_ERROR_SYNTAX)
103 ? false
104 : true;
105
106 // just in case...
107 $this->dbhw->query("COMMIT"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
108
109 }
110
111 $this->no_dbh_disconnect = empty($args['no_dbh_disconnect']) ? false : true;
112 }
113
114 /**
115 * As a result of register_shutdown_function(), called at the end of a request.
116 *
117 * @uses RF_Controller::invokePlugin(), "finishedRequest" event.
118 * @uses RF_Controller::$no_dbh_disconnect If not true, don't disconnect databas instances.
119 * @uses RF_Controller::$dbhr Disconnected
120 * @uses RF_Controller::$dbhw Disconnected after a commit of any open transactions
121 */
122 function finishRequest()
123 {
124 $this->invokePlugin('finishedRequest');
125
126 // just in case...
127 if($this->db_transactions)
128 $this->dbhw->query("COMMIT"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
129
130 if(!$this->no_dbh_disconnect) {
131 $this->dbhw->disconnect();
132 $this->dbhr->disconnect();
133 }
134
135 }
136
137 /**
138 * Irrevokably deletes a feed from the database.
139 *
140 * @param RF_Feed $feed Feed to delete
141 *
142 * @uses RF_Controller::getReadHandle()
143 * @uses RF_Controller::writeToDatabase()
144 * @uses RF_Controller::invokePlugin() "feedDeleted" event,
145 * parameters: {@link RF_Feed $feed}.
146 */
147 function deleteFeed(&$feed)
148 {
149 $dbhr =& $this->getReadHandle();
150
151 $query = sprintf("DELETE FROM %s
152 WHERE id = %d",
153 $dbhr->quoteIdentifier($feed->tableName()),
154 $feed->getID());
155
156 $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
157
158 $query = sprintf("DELETE FROM %s
159 WHERE feed_id = %d",
160 $dbhr->quoteIdentifier(RF_Item::tableName()),
161 $feed->getID());
162
163 $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
164
165 $this->invokePlugin('feedDeleted', array(&$feed));
166 }
167
168 /**
169 * Unsubscribes user from a feed.
170 *
171 * Removes all ties between a user and a feed. Only deletes the
172 * feed itself if there are no remaining subscribers to the feed.
173 *
174 * @param RF_User $user User to unsubscribe
175 * @param RF_Feed $feed Feed to unsubscribe
176 *
177 * @uses RF_Controller::getReadHandle()
178 * @uses RF_Feed::userdataTableName()
179 * @uses RF_Controller::writeToDatabase()
180 * @uses RF_Controller::getUserItems()
181 * @uses RF_Item::userdataTableName()
182 * @uses RF_Controller::getFeedUsers()
183 * @uses RF_Controller::deleteFeed()
184 * @uses RF_Controller::invokePlugin() "unsubscribedUserFromFeed" event,
185 * parameters: {@link RF_User $user}, {@link RF_Feed $feed}.
186 */
187 function unsubscribeUserFromFeed(&$user, &$feed)
188 {
189 $dbhr =& $this->getReadHandle();
190
191 $query = sprintf("DELETE FROM %s
192 WHERE feed_id = %d
193 AND user_id = %d",
194 $dbhr->quoteIdentifier($feed->userdataTableName()),
195 $feed->getID(),
196 $user->getID());
197
198 $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
199
200 $item_ids = array();
201
202 foreach($this->getUserItems($user, array('feed' => $feed->getID())) as $item)
203 $item_ids[] = $item->getID();
204
205 if(count($item_ids) > 0) {
206
207 $query = sprintf("DELETE FROM %s
208 WHERE user_id = %d
209 AND item_id IN (%s)",
210 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
211 $user->getID(),
212 join(', ', $item_ids));
213
214 $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
215 }
216
217 $this->invokePlugin('unsubscribedUserFromFeed', array(&$user, &$feed));
218
219 if(count($this->getFeedUsers($feed)) == 0)
220 $this->deleteFeed($feed);
221 }
222
223 /**
224 * Subscribe a user to a feed.
225 *
226 * @param RF_User $user User to subscribe
227 * @param RF_Feed $feed Feed to subscribe
228 *
229 * @uses RF_Controller::setFeedUserdata()
230 * @uses RF_Controller::invokePlugin() "subscribedUserToFeed" event,
231 * parameters: {@link RF_User $user}, {@link RF_Feed $feed}.
232 */
233 function subscribeUserToFeed(&$user, &$feed)
234 {
235 $result = $this->setFeedUserdata($feed, $user, 'subscribed', 1, 'numeric');
236
237 $this->invokePlugin('subscribedUserToFeed', array(&$user, &$feed));
238 return $result;
239 }
240
241 /**
242 * Check whether a user is subscribed to a feed.
243 *
244 * @param RF_User $user User to check
245 * @param RF_Feed $feed Feed to check
246 * @return integer 1 if subscribed, 0 if not
247 *
248 * @uses RF_Controller::userSubscribedToFeed()
249 */
250 function userSubscribedToFeed(&$user, &$feed)
251 {
252 return end($this->getFeedUserdata($feed, $user, 'subscribed', 'numeric'));
253 }
254
255 /**
256 * Get a list of feed subscribers.
257 *
258 * @param RF_Feed $feed Feed to check
259 * @return array Array of {@link RF_User users}
260 *
261 * @uses RF_Controller::getReadHandle()
262 * @uses RF_Feed::userdataTableName()
263 * @uses RF_Controller::readFromDatabase()
264 * @uses RF_User::RF_User()
265 * @uses RF_Controller::invokePlugin() "gotFeedUsers" event,
266 * parameters: {@link RF_Feed $feed}, {@link RF_User $users}.
267 */
268 function getFeedUsers(&$feed)
269 {
270 $dbhr =& $this->getReadHandle();
271
272 $query = sprintf("SELECT user_id
273 FROM %s
274 WHERE feed_id = %d
275 AND label = 'subscribed'
276 AND value_numeric = 1
277 AND user_id > 0",
278 $dbhr->quoteIdentifier($feed->userdataTableName()),
279 $feed->getID());
280
281 $result = $this->readFromDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
282
283 $users = array();
284
285 # Since this is super generic and just used for updating/cleaning
286 # we use the generic use class
287
288 while($subscriber = $result->fetchRow(DB_FETCHMODE_ASSOC))
289 {
290 $users[] = new RF_User(array('id' => $subscriber['user_id']));
291 }
292
293 $this->invokePlugin('gotFeedUsers', array(&$feed, &$users));
294
295 return $users;
296 }
297
298 /**
299 * Get a list of feed items.
300 *
301 * @param RF_Feed $feed Feed to check
302 * @return array Array of {@link RF_Item items}
303 *
304 * @uses RF_Controller::getReadHandle()
305 * @uses RF_Item::tableName()
306 * @uses RF_Controller::readFromDatabase()
307 * @uses RF_Item::RF_Item()
308 * @uses RF_Controller::invokePlugin() "gotFeedItems" event,
309 * parameters: {@link RF_Feed $feed}, array {@link RF_Item $items}.
310 */
311 function getFeedItems(&$feed)
312 {
313 $dbhr =& $this->getReadHandle();
314
315 $query = sprintf("SELECT * FROM %s
316 WHERE feed_id = %d",
317 $dbhr->quoteIdentifier(RF_Item::tableName()),
318 $feed->getID());
319
320 $result = $this->readFromDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
321
322 $items = array();
323
324 while($item = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
325 $item = new RF_Item($item);
326 $item->feed =& $feed;
327 $items[] = $item;
328 }
329
330 $this->invokePlugin('gotFeedItems', array(&$feed, &$items));
331
332 return $items;
333 }
334
335 /**
336 * Get a list of feeds.
337 *
338 * @param mixed $order Order to return results:
339 * - blank: database-determined order
340 * - "random": random order
341 * @return array Array of {@link RF_Feed feeds}
342 *
343 * @uses RF_Controller::getReadHandle()
344 * @uses RF_Feed::tableName()
345 * @uses RF_Controller::readFromDatabase()
346 * @uses RF_Feed::RF_Feed()
347 * @uses RF_Controller::invokePlugin() "gotFeeds" event,
348 * parameters: array {@link RF_Feed $feeds}.
349 */
350 function getFeeds($order=false)
351 {
352 $dbhr =& $this->getReadHandle();
353
354 switch($order) {
355 case 'random':
356 $order_clause = 'ORDER BY RAND()';
357 break;
358 default:
359 $order_clause = '';
360 break;
361 }
362
363 $query = sprintf("SELECT * FROM %s %s",
364 $dbhr->quoteIdentifier(RF_Feed::tableName()),
365 $order_clause);
366
367 $result = $this->readFromDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
368
369 $feeds = array();
370
371 while($feed = $result->fetchRow(DB_FETCHMODE_ASSOC))
372 $feeds[] = new RF_Feed($feed);
373
374 $this->invokePlugin('gotFeeds', array(&$feeds));
375
376 return $feeds;
377 }
378
379 /**
380 * Pre-process arguments used to retrieve user's items.
381 *
382 * @param RF_User $user User whose items to retrieve.
383 * @param array $args Name/value argument pairs,
384 * often resulting from HTTP client input.
385 * @return array Name/value argument pairs, cleaned up a bit.
386 *
387 * @see RF_Controller::getUserItems()
388 * @uses RF_User::itemPageCount()
389 * @uses RF_Controller::invokePlugin() "gotUserItemsQueryArguments" event,
390 * parameters: array $args.
391 */
392 function getUserItemsQueryArguments(&$user, $args)
393 {
394 $default_args = array(
395 'item' => null,
396 'how' => null,
397 'offset' => 0,
398 'howmany' => $user->itemPageCount(),
399 'order' => 'out',
400 'feed' => null,
401 'what' => null,
402 'when' => null,
403 'itemtag' => null,
404 'feedtag' => null,
405 'search' => /*isset($_SESSION['search']) ? $_SESSION['search'] :*/ null
406 );
407
408 foreach($default_args as $key => $value)
409 $args[$key] = isset($args[$key]) ? $args[$key] : $default_args[$key];
410
411 $this->invokePlugin('gotUserItemsQueryArguments', array(&$args));
412
413 return $args;
414 }
415
416 /**
417 * generate SQL query conditions used to retrieve user's items.
418 *
419 * @param RF_User $user User whose items to retrieve.
420 * @param array $args Name/value argument pairs,
421 * often resulting from HTTP client input.
422 * @return array Name/value pairs of SQL conditions and statements.
423 *
424 * @see RF_Controller::getUserItems()
425 * @uses RF_Controller::getReadHandle()
426 * @uses RF_Controller::getUserItemsQueryArguments()
427 * @uses RF_Controller::invokePlugin() "gotUserItemsQueryConditions" event,
428 * parameters: {@link RF_User $user}, array $conditions, array $args.
429 */
430 function getUserItemsQueryConditions(&$user, &$args)
431 {
432 $dbhr =& $this->getReadHandle();
433
434 $args = $this->getUserItemsQueryArguments($user, $args);
435
436 /* if "how" == "paged", limit returned results:
437 if "offset" exists, start there.
438 if "howmany" exists, use that */
439 $limit_clause = $args['how'] == 'paged'
440 ? sprintf("LIMIT %d, %d", $args['offset'], $args['howmany'])
441 : '';
442
443 // if "order" == "out", order by timestamp, otherwise by modified
444 $order_clause = $args['order'] == 'out'
445 ? 'ORDER BY `timestamp` DESC, i.modified DESC'
446 : 'ORDER BY i.modified DESC, `timestamp` DESC';
447
448 // If a search is being performed it's probably good to order by relevance
449 $order_clause = empty($args['search'])
450 ? $order_clause
451 : sprintf('ORDER BY (MATCH(i.link, i.title, i.content, i.author, i.category) AGAINST(%s)) DESC', $dbhr->quoteSmart($args['search']));
452
453 // if "item" is numeric, set the item_id
454 $item_test = is_numeric($args['item']) && $args['item'] >= 0
455 ? sprintf("id.item_id = %d", $args['item'])
456 : '1'; // always true if no item specified
457
458 // if "feed" is numeric, set the feed_id
459 $feed_test = is_numeric($args['feed']) && $args['feed'] >= 0
460 ? sprintf("(f.id = %d)", $args['feed'])
461 : false; // don't check the feeds table first!
462
463 // if "what" == "new", check for "read" == 0
464 $unread_test = $args['what'] == 'new'
465 ? '(id.value_numeric = 0)'
466 : '1'; // always true if no unread state specified
467
468 // if "what" == "published", check for "published" == 1
469 $published_test = preg_match('/\bpublished\b/', $args['what'])
470 ? '(id_published.value_numeric = 1)'
471 : '1'; // always true if no published state specified
472
473 // if "what" == "self", check for "self" == 1
474 $self_test = preg_match('/\bself\b/', $args['what'])
475 ? '(id_self.value_numeric = 1)'
476 : '1'; // always true if no self state specified
477
478 /* if "when" exists:
479 if "when" == "today"
480 if not, use "when" as a date */
481 if(empty($args['when'])) {
482 $when_test = '1'; // always true if no date limit specified
483
484 } else {
485
486 switch($args['when']) {
487 case 'today':
488 $args['when_start'] = date("Y-m-d");
489 break;
490 default:
491 $args['when_start'] = $args['when'];
492 break;
493 }
494
495 $args['when_start'] = strtotime($args['when_start']);
496 $args['when_end'] = $args['when_start'] + (24 * 60 * 60);
497
498 $when_test = sprintf("((UNIX_TIMESTAMP(id.timestamp) >= %d AND UNIX_TIMESTAMP(id.timestamp) < %d)
499 OR (UNIX_TIMESTAMP(id_published.timestamp) >= %d AND UNIX_TIMESTAMP(id_published.timestamp) < %d))",
500 $args['when_start'], $args['when_end'],
501 $args['when_start'], $args['when_end']);
502 }
503
504 // if "itemtag" exists, check for is encoded version
505 $itemtag_test = empty($args['itemtag'])
506 ? '1'
507 : sprintf('(id_tag.value_short = %s)', $dbhr->quoteSmart($args['itemtag']));
508
509 if(!empty($args['feedtag'])) {
510
511 $feed_ids = array();
512
513 foreach($this->getUserFeeds($user, array('feedtag' => $args['feedtag']), true) as $feed)
514 $feed_ids[] = $feed->getID();
515
516 $feed_tagtest = '(f.id IN ('.join(',', $feed_ids).'))';
517
518 $feed_test = $feed_test
519 ? "({$feed_test} && {$feed_tagtest})"
520 : $feed_tagtest;
521 }
522
523 // TODO: if "search" exists, match titles, links, content, author and category against it
524 $search_test = empty($args['search'])
525 ? '1'
526 : sprintf('(MATCH(i.link, i.title, i.content, i.author, i.category) AGAINST(%s))', $dbhr->quoteSmart($args['search']));
527
528 $conditions = compact('limit_clause', 'feed_test', 'item_test', 'unread_test', 'published_test', 'self_test', 'when_test', 'order_clause', 'itemtag_test', 'search_test');
529
530 $this->invokePlugin('gotUserItemsQueryConditions', array(&$user, &$conditions, &$args));
531
532 return $conditions;
533 }
534
535 /**
536 * generate a complete SQL query used to retrieve user's items.
537 *
538 * @param RF_User $user User whose items to retrieve.
539 * @param array $args Name/value argument pairs,
540 * often resulting from HTTP client input.
541 * @return string SQL SELECT query.
542 *
543 * @see RF_Controller::getUserItems()
544 * @uses RF_Controller::getReadHandle()
545 * @uses RF_Controller::getUserItemsQueryConditions()
546 * @uses RF_Item::userdataTableName()
547 * @uses RF_Item::tableName()
548 * @uses RF_Feed::tableName()
549 * @uses RF_Controller::invokePlugin() "gotUserItemsQuery" event,
550 * parameters: {@link RF_User $user}, string $query.
551 */
552 function getUserItemsQuery(&$user, $args)
553 {
554 $dbhr =& $this->getReadHandle();
555
556 // expect back an array with these keys defined:
557 // 'limit_clause', 'feed_test', 'item_test', 'unread_test', 'published_test', 'when_test', 'order_clause'
558 $conditions = $this->getUserItemsQueryConditions($user, $args);
559
560 // extra joins for extra metadata on each item
561 $meta = array('join_clauses' => array(), 'column_clauses' => array(), 'where_clauses' => array(1));
562
563 // $args[metadata] like [{'format': s, 'label': s}, ..., {'format': s, 'label': s}]
564 // if a match is required, may look like {'format': s, 'label': s, 'value': s}
565 if(is_array($args['metadata']))
566 foreach($args['metadata'] as $m => $metadatum)
567 if(is_string($metadatum['format']) && is_string($metadatum['label'])) {
568
569 $meta['join_clauses'][] = sprintf("
570 LEFT JOIN %s AS id{$m}
571 ON id{$m}.item_id = i.id
572 AND id{$m}.user_id = %d
573 AND id{$m}.label = %s
574 ",
575 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
576 $user->getID(),
577 $dbhr->quoteSmart($metadatum['label']));
578
579 $meta['column_clauses'][] = sprintf(
580 "id{$m}.value_%s AS %s,",
581 $dbhr->escapeSimple($metadatum['format']),
582 $dbhr->quoteIdentifier("metadatum_{$metadatum['label']}"));
583
584 if(isset($metadatum['value']))
585 if(is_string($metadatum['value']) || is_numeric($metadatum['value']))
586 $meta['where_clauses'][] = sprintf(
587 "(id{$m}.value_%s = %s)",
588 $dbhr->escapeSimple($metadatum['format']),
589 $dbhr->quoteSmart($metadatum['value']));
590 }
591
592 $core_join_clauses = sprintf("
593 LEFT JOIN %s AS id_published
594 ON id_published.item_id = i.id
595 AND id_published.user_id = %d
596 AND id_published.label = 'published'
597
598 LEFT JOIN %s AS id_self
599 ON id_self.item_id = i.id
600 AND id_self.user_id = %d
601 AND id_self.label = 'self'
602
603 LEFT JOIN %s AS id_title
604 ON id_title.item_id = i.id
605 AND id_title.user_id = %d
606 AND id_title.label = 'title'
607
608 LEFT JOIN %s AS id_content
609 ON id_content.item_id = i.id
610 AND id_content.user_id = %d
611 AND id_content.label = 'content'
612
613 LEFT JOIN %s AS id_link
614 ON id_link.item_id = i.id
615 AND id_link.user_id = %d
616 AND id_link.label = 'link'
617
618 LEFT JOIN %s AS fd
619 ON fd.feed_id = f.id
620 AND fd.user_id = %d
621 AND fd.label = 'tags'
622 ",
623 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
624 $user->getID(),
625 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
626 $user->getID(),
627 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
628 $user->getID(),
629 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
630 $user->getID(),
631 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
632 $user->getID(),
633 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
634 $user->getID());
635
636 if($conditions['itemtag_test'] != '1')
637 $core_join_clauses .= sprintf("
638 LEFT JOIN %s AS id_tag
639 ON id_tag.item_id = i.id
640 AND id_tag.user_id = %d
641 AND id_tag.label = 'tag'
642 ",
643 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
644 $user->getID());
645
646 $meta['join_clauses'] = join("\n\n", $meta['join_clauses']);
647 $meta['where_clauses'] = join(' AND ', $meta['where_clauses']);
648
649 // each of these queries will work fine, but the former is
650 // optimized for searches on particular metadata values,
651 // and the latter is optimized for typical per-feed viewing
652 if($conditions['feed_test'] === false) {
653 // this query optimized for metadata searches
654 $table_clauses = sprintf("
655 FROM %s AS id
656
657 LEFT JOIN %s AS i
658 ON i.id = id.item_id
659
660 LEFT JOIN %s AS f
661 ON f.id = i.feed_id
662
663 {$core_join_clauses}
664 {$meta['join_clauses']}
665
666 WHERE id.user_id = %d
667 AND id.label = 'read'
668 AND {$conditions['item_test']}
669 AND {$conditions['unread_test']}
670 AND {$conditions['published_test']}
671 AND {$conditions['self_test']}
672 AND {$conditions['when_test']}
673 AND {$conditions['itemtag_test']}
674 AND {$conditions['search_test']}
675 AND {$meta['where_clauses']}
676 AND i.id IS NOT NULL
677 ",
678 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
679 $dbhr->quoteIdentifier(RF_Item::tableName()),
680 $dbhr->quoteIdentifier(RF_Feed::tableName()),
681 $user->getID());
682
683 } else {
684 // this query optimized for in-feed searches
685 $table_clauses = sprintf("
686 FROM %s AS f
687
688 LEFT JOIN %s AS i
689 ON i.feed_id = f.id
690
691 LEFT JOIN %s AS id
692 ON id.item_id = i.id
693 AND id.user_id = %d
694 AND id.label = 'read'
695
696 {$core_join_clauses}
697 {$meta['join_clauses']}
698
699 WHERE {$conditions['feed_test']}
700 AND {$conditions['item_test']}
701 AND {$conditions['unread_test']}
702 AND {$conditions['published_test']}
703 AND {$conditions['self_test']}
704 AND {$conditions['when_test']}
705 AND {$conditions['itemtag_test']}
706 AND {$conditions['search_test']}
707 AND i.id IS NOT NULL
708 ",
709 $dbhr->quoteIdentifier(RF_Feed::tableName()),
710 $dbhr->quoteIdentifier(RF_Item::tableName()),
711 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
712 $user->getID());
713
714 }
715
716 $meta['column_clauses'] = join(' ', $meta['column_clauses']);
717
718 $q = sprintf("
719 SELECT
720
721 ### Core item properties
722 i.id,
723 i.guid,
724 IFNULL(id_link.value_long, i.link) AS link,
725 IFNULL(id_title.value_long, i.title) AS title,
726 IFNULL(id_content.value_long, i.content) AS content,
727 i.author,
728 i.category,
729 i.modified,
730
731 # use the timestamp of the most recent read or publish action
732 IF(UNIX_TIMESTAMP(id.timestamp) < UNIX_TIMESTAMP(id_published.timestamp),
733 UNIX_TIMESTAMP(id_published.timestamp),
734 UNIX_TIMESTAMP(id.timestamp)) AS `timestamp`,
735
736 ### Original item properties
737 i.link AS original_link,
738 i.title AS original_title,
739 i.content AS original_content,
740
741 ### Core feed properties
742 f.id AS feed_id,
743 f.url AS feed_url,
744 f.title AS feed_title,
745 f.link AS feed_link,
746 f.description AS feed_description,
747 fd.value_long AS feed_metadatum_tags,
748
749 ### Extra item properties
750 {$meta['column_clauses']}
751 id_published.value_numeric AS published,
752 id.value_numeric AS `read`
753
754 {$table_clauses}
755
756 GROUP BY id.item_id
757
758 {$conditions['order_clause']}
759 {$conditions['limit_clause']}
760 ");
761
762 $this->invokePlugin('gotUserItemsQuery', array(&$user, &$q));
763
764 return $q;
765 }
766
767 /**
768 * retrieve a user's items based on variable criteria.
769 *
770 * @param RF_User $user User whose items to retrieve.
771 * @param array $args Name/value argument pairs,
772 * often resulting from HTTP client input.
773 * @return array Array of {@link RF_Item items}
774 *
775 * @uses RF_Controller::getUserItemsQuery()
776 * @uses RF_Controller::readFromDatabase()
777 * @uses RF_Item::RF_Item()
778 * @uses RF_Feed::RF_Feed()
779 * @uses RF_Controller::invokePlugin() "gotUserItems" event,
780 * parameters: {@link RF_User $user}, array {@link RF_Item $items}.
781 */
782 function getUserItems(&$user, $args)
783 {
784 $result = $this->readFromDatabase($this->getUserItemsQuery($user, $args)."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
785
786 $items = array();
787
788 while($item = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
789
790 // `modified' is returned in "YYYY-MM-DD HH:MM:SS" format
791 $item['modified'] = strtotime($item['modified']);
792
793 $item['feed'] = array();
794 $item['feed']['metadata'] = array();
795 $item['metadata'] = array();
796 $item['original'] = array();
797
798 foreach(array_keys($item) as $property) {
799
800 if(substr($property, 0, 15) == 'feed_metadatum_') {
801 $item['feed']['metadata'][substr($property, 15)] = $item[$property];
802 unset($item[$property]);
803
804 } elseif(substr($property, 0, 5) == 'feed_') {
805 $item['feed'][substr($property, 5)] = $item[$property];
806 unset($item[$property]);
807
808 } elseif(substr($property, 0, 10) == 'metadatum_') {
809 $item['metadata'][substr($property, 10)] = $item[$property];
810 unset($item[$property]);
811
812 } elseif(substr($property, 0, 9) == 'original_') {
813 $item['original'][substr($property, 9)] = $item[$property];
814 unset($item[$property]);
815
816 }
817
818 }
819
820 $item['feed'] = new RF_Feed($item['feed']);
821
822 $items[] = new RF_Item($item);
823 }
824
825 $this->invokePlugin('gotUserItems', array(&$user, &$items));
826
827 return $items;
828 }
829
830 /**
831 * retrieve a user's item
832 *
833 * @param RF_User $user User whose items to retrieve.
834 * @param mixed $item Instance of RF_Item, or numeric item ID
835 * @return RF_Item Single {@link RF_Item item}
836 *
837 * @uses RF_Item::RF_Item()
838 * @uses RF_Controller::getUserItems()
839 */
840 function getUserItem(&$user, $item)
841 {
842 if(is_numeric($item))
843 $item = new RF_Item(array('id' => $item));
844
845 $items = $this->getUserItems($user, array('item' => $item->getID()));
846
847 if(count($items) == 1)
848 return $items[0];
849 }
850
851 /**
852 * generate SQL query conditions used to retrieve user's feeds.
853 *
854 * @param RF_User $user User whose feeds to retrieve.
855 * @param array $args Name/value argument pairs,
856 * often resulting from HTTP client input.
857 * @param boolean $tag_only Optional flag: if 'feedtag' is passed
858 * in $args, get /only/ those feeds with
859 * the tag, or all of them? Default no.
860 * @return array Name/value pairs of SQL conditions and statements.
861 *
862 * @see RF_Controller::getUserFeeds()
863 * @uses RF_Controller::getReadHandle()
864 * @uses RF_Controller::invokePlugin() "gotUserFeedsQueryArguments" event,
865 * parameters: array $args.
866 * @uses RF_Controller::invokePlugin() "gotUserFeedsQueryConditions" event,
867 * parameters: {@link RF_User $user}, array $conditions, array $args.
868 */
869 function getUserFeedsQueryConditions(&$user, &$args, $tag_only=false)
870 {
871 $dbhr =& $this->getReadHandle();
872
873 $this->invokePlugin('gotUserFeedsQueryArguments', array(&$args));
874
875 switch($_SESSION['feed_sort']) {
876 case 'age':
877 $order_clause = 'ORDER BY usage_last_update DESC, f.title ASC';
878 break;
879
880 case 'items':
881 $order_clause = 'ORDER BY usage_unread DESC, f.title ASC';
882 break;
883
884 case 'random':
885 $order_clause = 'ORDER BY RAND()';
886 break;
887
888 default:
889 $order_clause = 'ORDER BY f.title ASC';
890 break;
891 }
892
893 // if "feed" is numeric, set the feed_id
894 $feed_test = is_numeric($args['feed']) && $args['feed'] >= 0
895 ? sprintf("(f.id = %d)", $args['feed'])
896 : '1'; // always true if no feed specified
897
898 /*$args['search'] = isset($args['search'])
899 ? $args['search']
900 : $_SESSION['search'];*/
901
902 // TODO: if "search" exists, match titles, links, content, author and category against it
903 $search_test = empty($args['search'])
904 ? '1'
905 : sprintf('(MATCH(f.url, f.title, f.link, f.description) AGAINST(%s))', $dbhr->quoteSmart($args['search']));
906
907 $feedtag_jointest = empty($args['feedtag'])
908 ? '1'
909 : sprintf('(fd_tag.value_short = %s)', $dbhr->quoteSmart($args['feedtag']));
910
911 $feedtag_test = $tag_only
912 ? $feedtag_jointest
913 : '1';
914
915 $conditions = compact('order_clause', 'feed_test', 'search_test', 'feedtag_test', 'feedtag_jointest');
916
917 $this->invokePlugin('gotUserFeedsQueryConditions', array(&$user, &$conditions, &$args));
918
919 return $conditions;
920 }
921
922 /**
923 * generate a complete SQL query used to retrieve user's feeds.
924 *
925 * @param RF_User $user User whose feeds to retrieve.
926 * @param array $args Name/value argument pairs,
927 * often resulting from HTTP client input.
928 * @param boolean $tag_only Optional flag: if 'feedtag' is passed
929 * in $args, get /only/ those feeds with
930 * the tag, or all of them? Default no.
931 * @return string SQL SELECT query.
932 *
933 * @see RF_Controller::getUserFeeds()
934 * @uses RF_Controller::getReadHandle()
935 * @uses RF_Controller::getUserFeedsQueryConditions()
936 * @uses RF_Feed::userdataTableName()
937 * @uses RF_Item::userdataTableName()
938 * @uses RF_Feed::tableName()
939 * @uses RF_Item::tableName()
940 * @uses RF_Controller::invokePlugin() "gotUserFeedsQuery" event,
941 * parameters: {@link RF_User $user}, string $query.
942 */
943 function getUserFeedsQuery(&$user, $args, $tag_only=false)
944 {
945 $dbhr =& $this->getReadHandle();
946 $conditions = $this->getUserFeedsQueryConditions($user, $args, $tag_only);
947
948 // extra joins for extra metadata on each item
949 $meta = array('join_clauses' => array(), 'column_clauses' => array(), 'where_clauses' => array(1));
950
951 // $args[metadata] like [{'format': s, 'label': s}, ..., {'format': s, 'label': s}]
952 // if a match is required, may look like {'format': s, 'label': s, 'value': s}
953 if(isset($args['metadata']) && is_array($args['metadata']))
954 foreach($args['metadata'] as $m => $metadatum)
955 if(is_string($metadatum['format']) && is_string($metadatum['label'])) {
956
957 $meta['join_clauses'][] = sprintf("
958 LEFT JOIN %s AS fd{$m}
959 ON fd{$m}.feed_id = f.id
960 AND fd{$m}.user_id = %d
961 AND fd{$m}.label = %s
962 ",
963 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
964 $user->getID(),
965 $dbhr->quoteSmart($metadatum['label']));
966
967 $meta['column_clauses'][] = sprintf(
968 "fd{$m}.value_%s AS %s,",
969 $dbhr->escapeSimple($metadatum['format']),
970 $dbhr->quoteIdentifier("metadatum_{$metadatum['label']}"));
971
972 if(is_string($metadatum['value']) || is_numeric($metadatum['value']))
973 $meta['where_clauses'][] = sprintf(
974 "(fd{$m}.value_%s = %s)",
975 $dbhr->escapeSimple($metadatum['format']),
976 $dbhr->quoteSmart($metadatum['value']));
977 }
978
979 $meta['join_clauses'] = join("\n\n", $meta['join_clauses']);
980 $meta['where_clauses'] = join(' AND ', $meta['where_clauses']);
981 $meta['column_clauses'] = join(' ', $meta['column_clauses']);
982
983 $q = sprintf("SELECT
984
985 ### Core feed properties
986 f.id,
987 f.url,
988 f.title,
989 f.link,
990 f.description,
991
992 ### Feed usage properties
993 fd_usage_last_update.value_numeric AS usage_last_update,
994 fd_usage_total.value_numeric AS usage_items,
995 fd_usage_unread.value_numeric AS usage_unread,
996 fd_usage_published.value_numeric AS usage_published,
997 IF(fd_tag.value_short IS NULL, 0, 1) AS usage_tagged, # flag - tagged or no?
998
999 ### Extra feed properties
1000 {$meta['column_clauses']}
1001 fd_published.value_numeric AS published
1002
1003 FROM %s AS fd
1004
1005 LEFT JOIN %s AS f
1006 ON f.id = fd.feed_id
1007
1008 LEFT JOIN %s AS fd_tag
1009 ON fd_tag.feed_id = f.id
1010 AND fd_tag.user_id = %d
1011 AND fd_tag.label = 'tag'
1012 AND {$conditions['feedtag_jointest']}
1013
1014 LEFT JOIN %s AS fd_published
1015 ON fd_published.feed_id = f.id
1016 AND fd_published.user_id = %d
1017 AND fd_published.label = 'published'
1018
1019 LEFT JOIN %s AS fd_usage_last_update
1020 ON fd_usage_last_update.feed_id = f.id
1021 AND fd_usage_last_update.user_id = %d
1022 AND fd_usage_last_update.label = 'usage_last_update'
1023
1024 LEFT JOIN %s AS fd_usage_total
1025 ON fd_usage_total.feed_id = f.id
1026 AND fd_usage_total.user_id = %d
1027 AND fd_usage_total.label = 'usage_unread'
1028
1029 LEFT JOIN %s AS fd_usage_unread
1030 ON fd_usage_unread.feed_id = f.id
1031 AND fd_usage_unread.user_id = %d
1032 AND fd_usage_unread.label = 'usage_unread'
1033
1034 LEFT JOIN %s AS fd_usage_published
1035 ON fd_usage_published.feed_id = f.id
1036 AND fd_usage_published.user_id = %d
1037 AND fd_usage_published.label = 'usage_published'
1038
1039 {$meta['join_clauses']}
1040
1041 WHERE fd.label = 'subscribed'
1042 AND fd.user_id = %d
1043 AND {$conditions['feed_test']}
1044 AND {$conditions['search_test']}
1045 AND {$meta['where_clauses']}
1046 AND {$conditions['feedtag_test']}
1047
1048 GROUP BY fd.feed_id
1049
1050 {$conditions['order_clause']}
1051 ",
1052 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
1053 $dbhr->quoteIdentifier(RF_Feed::tableName()),
1054 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
1055 $user->getID(),
1056 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
1057 $user->getID(),
1058 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
1059 $user->getID(),
1060 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
1061 $user->getID(),
1062 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
1063 $user->getID(),
1064 $dbhr->quoteIdentifier(RF_Feed::userdataTableName()),
1065 $user->getID(),
1066 $user->getID());
1067
1068 $this->invokePlugin('gotUserFeedsQuery', array(&$user, &$q));
1069
1070 return $q;
1071 }
1072
1073 /**
1074 * retrieve a user's feeds based on variable criteria.
1075 *
1076 * @param RF_User $user User whose feeds to retrieve.
1077 * @param array $args Name/value argument pairs,
1078 * often resulting from HTTP client input.
1079 * @param boolean $tag_only Optional flag: if 'feedtag' is passed
1080 * in $args, get /only/ those feeds with
1081 * the tag, or all of them? Default no.
1082 * @return array Array of {@link RF_Feed feeds}
1083 *
1084 * @uses RF_Controller::getUserFeedsQuery()
1085 * @uses RF_Controller::readFromDatabase()
1086 * @uses RF_Feed::RF_Feed()
1087 * @uses RF_Controller::invokePlugin() "gotUserFeeds" event,
1088 * parameters: {@link RF_User $user}, array {@link RF_Feed $feeds}.
1089 */
1090 function getUserFeeds(&$user, $args, $tag_only=false)
1091 {
1092 $result = $this->readFromDatabase($this->getUserFeedsQuery($user, $args, $tag_only)."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1093
1094 $feeds = array();
1095
1096 while($feed = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
1097
1098 $feed['usage'] = array();
1099 $feed['metadata'] = array();
1100
1101 foreach(array_keys($feed) as $property) {
1102
1103 $short_property = substr($property, 6);
1104
1105 if(substr($property, 0, 6) == 'usage_') {
1106 $feed['usage'][$short_property] = $feed[$property];
1107 unset($feed[$property]);
1108
1109 } elseif(substr($property, 0, 10) == 'metadatum_') {
1110 $feed['metadata'][substr($property, 10)] = $feed[$property];
1111 unset($feed[$property]);
1112
1113 }
1114
1115 }
1116
1117 $feeds[] = new RF_Feed($feed);
1118 }
1119
1120 $this->invokePlugin('gotUserFeeds', array(&$user, &$feeds));
1121
1122 return $feeds;
1123 }
1124
1125 /**
1126 * retrieve a user's feed
1127 *
1128 * @param RF_User $user User whose feeds to retrieve.
1129 * @param mixed $feed Instance of RF_Feed, or numeric feed ID
1130 * @return RF_Feed Single {@link RF_Feed feed}
1131 *
1132 * @uses RF_Feed::RF_Feed()
1133 * @uses RF_Controller::getUserFeeds()
1134 */
1135 function getUserFeed(&$user, $feed)
1136 {
1137 if(is_numeric($feed))
1138 $feed = new RF_Feed(array('id' => $feed));
1139
1140 $feeds = $this->getUserFeeds($user, array('feed' => $feed->getID()));
1141
1142 if(count($feeds) == 1)
1143 return $feeds[0];
1144 }
1145
1146 /**
1147 * @uses RF_Controller::getReadHandle()
1148 */
1149 function getUserSelfFeedQuery(&$user, $args)
1150 {
1151 $dbhr =& $this->getReadHandle();
1152 $conditions = $this->getUserFeedsQueryConditions($user, $args);
1153
1154 $q = sprintf("SELECT
1155
1156 ### Self-feed usage properties
1157
1158 UNIX_TIMESTAMP(
1159 MAX(IF(id_read.value_numeric = 0,
1160 id_read.timestamp, 0))) AS usage_last_update, # timestamp of newest unread item
1161
1162 COUNT(i.id) AS usage_items, # number of items total
1163
1164 SUM(id_published.value_numeric) AS usage_published # number of published items
1165
1166 FROM %s AS id
1167
1168 LEFT JOIN %s AS i
1169 ON i.id = id.item_id
1170
1171 LEFT JOIN %s AS id_read
1172 ON id_read.item_id = i.id
1173 AND id_read.user_id = %d
1174 AND id_read.label = 'read'
1175
1176 LEFT JOIN %s AS id_published
1177 ON id_published.item_id = i.id
1178 AND id_published.user_id = %d
1179 AND id_published.label = 'published'
1180
1181 WHERE id.label = 'self'
1182 AND id.user_id = %d
1183 AND id.value_numeric = 1
1184 AND {$conditions['feed_test']}
1185
1186 GROUP BY i.feed_id
1187 ",
1188 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
1189 $dbhr->quoteIdentifier(RF_Item::tableName()),
1190 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
1191 $user->getID(),
1192 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
1193 $user->getID(),
1194 $user->getID());
1195
1196 $this->invokePlugin('gotUserSelfFeedQuery', array(&$user, &$q));
1197
1198 return $q;
1199 }
1200
1201 /**
1202 *
1203 */
1204 function getUserSelfFeed(&$user, $args)
1205 {
1206 $result = $this->readFromDatabase($this->getUserSelfFeedQuery($user, $args)."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1207
1208 if($feed = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
1209
1210 $feed['usage'] = array();
1211
1212 foreach(array_keys($feed) as $property) {
1213
1214 $short_property = substr($property, 6);
1215
1216 if(substr($property, 0, 6) == 'usage_') {
1217 $feed['usage'][$short_property] = $feed[$property];
1218
1219 switch($short_property) {
1220 case 'last_update':
1221 $total_usage[$short_property] = max($total_usage[$short_property], $feed[$property]);
1222 break;
1223 default:
1224 $total_usage[$short_property] = $total_usage[$short_property] + $feed[$property];
1225 break;
1226 }
1227
1228 unset($feed[$property]);
1229 }
1230
1231 }
1232
1233 $feed = new RF_Feed($feed);
1234
1235 $this->invokePlugin('gotUserSelfFeed', array(&$user, &$feed));
1236 }
1237
1238 return $feed;
1239 }
1240
1241 /**
1242 * Retrive a list of all user's tags
1243 *
1244 * @param RF_User $user -
1245 * @param string $referent Entity referenced by the tag
1246 * - "item" (default)
1247 * - "feed"
1248 * @param string $order Tag return order:
1249 * - "alphabet": Alphabetically, A - Z (default)
1250 * - "usage": By usage frequency, most to least
1251 * @return array Array of strings
1252 *
1253 * @uses RF_Controller::getReadHandle()
1254 * @uses RF_Controller::readFromDatabase()
1255 * @uses RF_Controller::invokePlugin() "gotUserTags" event,
1256 * parameters: {@link RF_User $user}, string $referent, array $tags.
1257 */
1258 function getUserTags(&$user, $referent='item', $order='alphabet')
1259 {
1260 $dbhr =& $this->getReadHandle();
1261
1262 $order_clauses = array('alphabet' => 'tag ASC',
1263 'usage' => 'items DESC');
1264
1265 $order_clause = $order_clauses[$order];
1266
1267 $tables = array('item' => RF_Item::userdataTableName(),
1268 'feed' => RF_Feed::userdataTableName());
1269
1270 $query = sprintf("SELECT value_short AS tag,
1271 COUNT(%s) AS referents
1272 FROM %s
1273 WHERE label = 'tag'
1274 AND user_id = %d
1275 GROUP BY tag
1276 ORDER BY {$order_clause}
1277 ",
1278 $dbhr->quoteIdentifier("{$referent}_id"),
1279 $dbhr->quoteIdentifier($tables[$referent]),
1280 $user->getID());
1281
1282 $tags = array();
1283 $result = $this->readFromDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1284
1285 while($tag = $result->fetchRow(DB_FETCHMODE_ASSOC))
1286 $tags[] = $tag['tag'];
1287
1288 $this->invokePlugin('gotUserTags', array(&$user, $referent, &$tags));
1289
1290 return $tags;
1291 }
1292
1293 /**
1294 * Return a usage summary for tags on a given set of items.
1295 *
1296 * @param array $objects Instances of {@link RF_Item RF_Item} or {@link RF_Feed RF_Feed}
1297 * @return array Associative array - keys are tags, values are usage counts.
1298 * Sorted in descending order of usage.
1299 */
1300 function tagUsage($objects)
1301 {
1302 $usage = array();
1303
1304 foreach($objects as $object)
1305 if(isset($object->metadata['tags'])) {
1306 $tags = preg_split('#\s+#', $object->metadata['tags']);
1307
1308 foreach($tags as $tag)
1309 if($tag)
1310 $usage[$tag] = $usage[$tag]
1311 ? $usage[$tag] + 1
1312 : 1;
1313 }
1314
1315 arsort($usage);
1316 return $usage;
1317 }
1318
1319 /**
1320 * Mark new or changed items in a feed as unread and perhaps published.
1321 * Try to set tags in accordance with feed tags as well
1322 *
1323 * @param RF_Feed $feed Feed to which items belong
1324 * @param array $items Instances of {@link RF_Item RF_Item}
1325 *
1326 * @uses RF_Controller::getFeedUsers()
1327 * @uses RF_Userdata_Controller::RF_Userdata_Controller()
1328 * @uses RF_Userdata_Controller::markItemsUnread()
1329 * @uses RF_Userdata_Controller::markItemsPublished()
1330 * @uses RF_Controller::freshenUserFeedUsage()
1331 * @uses RF_Controller::invokePlugin() "freshenedUserFeedItems" event,
1332 * parameters: {@link RF_User $user}, {@link RF_Feed $feed}, array {@link RF_Item $items}.
1333 */
1334 function freshenFeedItems(&$feed, &$items)
1335 {
1336 foreach($this->getFeedUsers($feed) as $user) {
1337
1338 $userdata_controller = new RF_Userdata_Controller($this, $user);
1339
1340 if(count($items)) {
1341 $userdata_controller->markItemsUnread($items);
1342
1343 $published = isset($feed->published)
1344 ? $feed->published
1345 : reset($this->getFeedUserdata($feed, $user, 'published', 'numeric'));
1346
1347 if($published)
1348 $userdata_controller->markItemsPublished($items);
1349
1350 $this->invokePlugin('freshenedUserFeedItems', array(&$user, &$feed, &$items));
1351 }
1352
1353 $this->freshenUserFeedUsage($user, $feed);
1354 }
1355 }
1356
1357 /**
1358 * Delete feed items that are are too old.
1359 *
1360 * Criteria are based on a user's specified deletion behavior:
1361 * - {@link RF_User::itemKeepDays() Number of days} that items should be kept around
1362 * - {@link RF_User::itemKeepUnread() Whether to consider unread items} for deletion
1363 *
1364 * @param RF_Feed $feed Feed to which items belong
1365 * @param array $except_items Instances of {@link RF_Item RF_Item} to keep
1366 *
1367 * @uses RF_Controller::getWriteHandle()
1368 * @uses RF_Controller::getFeedUsers()
1369 * @uses RF_User::itemKeepDays()
1370 * @uses RF_User::itemKeepUnread()
1371 * @uses RF_Controller::readFromDatabase()
1372 * @uses RF_Controller::writeToDatabase()
1373 * @uses RF_Controller::freshenUserFeedUsage()
1374 * @uses RF_Controller::$db_transactions
1375 * @uses RF_Controller::invokePlugin() "flushedObsoleteUserItems" event,
1376 * parameters: {@link RF_User $user}, {@link RF_Feed $feed}, array {@link RF_Item $items}.
1377 * @uses RF_Controller::invokePlugin() "flushedObsoleteItems" event,
1378 * parameters: {@link RF_Feed $feed}, array {@link RF_Item $items}.
1379 */
1380 function flushObsoleteFeedItems(&$feed, &$except_items)
1381 {
1382 //error_log('flushing old items from feed #'.$feed->getID().' (except '.count($except_items).' items)...');
1383
1384 $dbhw =& $this->getWriteHandle();
1385
1386 $flushed_item_ids = array();
1387
1388 foreach($this->getFeedUsers($feed) as $user) {
1389
1390 if($user->itemKeepDays() === false)
1391 continue;
1392
1393 $except_item_ids = array();
1394
1395 foreach($except_items as $except_item)
1396 $except_item_ids[] = intval($except_item->getID());
1397
1398 $except_test = count($except_item_ids)
1399 ? sprintf('i.id NOT IN (%s)', join(',', $except_item_ids))
1400 : '1';
1401
1402 $read_test = $user->itemKeepUnread()
1403 ? '`read` = 1'
1404 : '`read` IN (1, 0)';
1405
1406 // look for items that are old and unloved
1407 $q = sprintf("SELECT i.id AS id,
1408 id.value_numeric AS `read`,
1409 IF(id_pub.value_numeric, 1, 0) AS published
1410
1411 FROM %s AS i
1412
1413 LEFT JOIN %s AS id
1414 ON id.item_id = i.id
1415 AND id.user_id = %d
1416 AND id.label = 'read'
1417
1418 LEFT JOIN %s AS id_pub
1419 ON id_pub.item_id = i.id
1420 AND id_pub.user_id = %d
1421 AND id_pub.label = 'published'
1422
1423 WHERE i.feed_id = %d
1424 AND i.timestamp < NOW() - INTERVAL %d DAY
1425 AND {$except_test}
1426
1427 HAVING {$read_test}
1428 AND published = 0",
1429 $dbhw->quoteIdentifier(RF_Item::tableName()),
1430 $dbhw->quoteIdentifier(RF_Item::userdataTableName()),
1431 $user->getID(),
1432 $dbhw->quoteIdentifier(RF_Item::userdataTableName()),
1433 $user->getID(),
1434 $feed->getID(),
1435 $user->itemKeepDays());
1436
1437 $result = $this->readFromDatabase($q."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__); //error_log($q);
1438
1439 $items = array();
1440 $item_ids = array();
1441
1442 while($item = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
1443 $items[] = new RF_Item($item);
1444 $item_ids[] = intval($item['id']);
1445 }
1446
1447 $flushed_item_ids = array_merge($flushed_item_ids, $item_ids);
1448
1449 //error_log('Going to remove '.count($item_ids).' items from feed #'.$feed->getID().' for user #'.$user->getID());
1450
1451 if(count($item_ids)) {
1452
1453 $q = sprintf("DELETE FROM %s
1454 WHERE item_id IN (%s)
1455 AND user_id = %d",
1456 $dbhw->quoteIdentifier(RF_Item::userdataTableName()),
1457 join(',', $item_ids),
1458 $user->getID());
1459
1460 $this->writeToDatabase($q."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__); //error_log($q);
1461
1462 $this->invokePlugin('flushedObsoleteUserItems', array(&$user, &$feed, &$items));
1463 $this->freshenUserFeedUsage($user, $feed);
1464 }
1465 }
1466
1467 // So far, only users' item data has been deleted - the items table remains untouched.
1468 // If any items no longer have associated user data, delete them completely.
1469
1470 if(count($flushed_item_ids)) {
1471
1472 if($this->db_transactions)
1473 $this->writeToDatabase("START TRANSACTION"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1474
1475 $flushed_item_ids = array_unique($flushed_item_ids);
1476
1477 //error_log('Going to consider '.count($flushed_item_ids).' items from feed #'.$feed->getID().' for total removal');
1478
1479 // look for items that lack associated user data
1480 $q = sprintf("SELECT i.id AS id
1481 FROM %s AS i
1482 LEFT JOIN %s AS id
1483 ON id.item_id = i.id
1484 WHERE i.id IN (%s)
1485 AND id.item_id IS NULL",
1486 $dbhw->quoteIdentifier(RF_Item::tableName()),
1487 $dbhw->quoteIdentifier(RF_Item::userdataTableName()),
1488 join(',', $flushed_item_ids));
1489
1490 // technically, we're reading here - just want to make
1491 // sure to use the right database connection for the
1492 // transaction we're wrapped up in
1493 $result = $this->writeToDatabase($q."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__); //error_log($q);
1494
1495 $items = array();
1496 $obliterated_item_ids = array();
1497
1498 while($item = $result->fetchRow(DB_FETCHMODE_ASSOC)) {
1499 $items[] = new RF_Item($item);
1500 $obliterated_item_ids[] = intval($item['id']);
1501 }
1502
1503 if(count($obliterated_item_ids)) {
1504
1505 //error_log('Going to obliterate '.count($obliterated_item_ids).' items from feed #'.$feed->getID());
1506
1507 $q = sprintf("DELETE FROM %s
1508 WHERE id IN (%s)",
1509 $dbhw->quoteIdentifier(RF_Item::tableName()),
1510 join(',', $obliterated_item_ids));
1511
1512 $this->writeToDatabase($q."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__); //error_log($q);
1513
1514 $this->invokePlugin('flushedObsoleteItems', array(&$feed, &$items));
1515 }
1516 }
1517
1518 if($this->db_transactions)
1519 $this->writeToDatabase("COMMIT"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1520 }
1521
1522 /**
1523 * Update usage statistics regarding item counts (unread, published, etc.)
1524 * and update times in feed userdata table.
1525 *
1526 * @param RF_User $user -
1527 * @param RF_Feed $feed Feed to which items belong
1528 *
1529 * @uses RF_Controller::getReadHandle()
1530 * @uses RF_Controller::setFeedUserdata()
1531 */
1532 function freshenUserFeedUsage(&$user, &$feed)
1533 {
1534 // Feed might not exist for self-published items
1535 if(!$feed->getID())
1536 return;
1537
1538 $dbhr =& $this->getReadHandle();
1539
1540 $query = sprintf("SELECT # NULL's don't count toward the total & unread counts
1541 SUM(IF(id.value_numeric IS NOT NULL,
1542 1, 0)) AS total,
1543 SUM(IF(id.value_numeric = 0, 1, 0)) AS unread,
1544
1545 SUM(IF(id_pub.value_numeric, 1, 0)) AS published,
1546
1547 UNIX_TIMESTAMP(
1548 MAX(IF(id.value_numeric = 0,
1549 id.timestamp, 0))) AS last_update
1550
1551 FROM %s AS i
1552
1553 LEFT JOIN %s AS id
1554 ON id.item_id = i.id
1555 AND id.user_id = %d
1556 AND id.label = 'read'
1557
1558 LEFT JOIN %s AS id_pub
1559 ON id_pub.item_id = i.id
1560 AND id_pub.user_id = %d
1561 AND id_pub.label = 'published'
1562
1563 WHERE i.feed_id = %d",
1564
1565 $dbhr->quoteIdentifier(RF_Item::tableName()),
1566 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
1567 $user->getID(),
1568 $dbhr->quoteIdentifier(RF_Item::userdataTableName()),
1569 $user->getID(),
1570 $feed->getID());
1571
1572 $result = $dbhr->getRow($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__, DB_FETCHMODE_ASSOC);
1573
1574 if(DB::isError($result))
1575 die(sprintf('<p><b>%s.</b></p><pre>%s</pre>', htmlspecialchars($result->getMessage()), htmlspecialchars($result->getDebugInfo())));
1576
1577 foreach($result as $metric => $value)
1578 $this->setFeedUserdata($feed, $user, "usage_{$metric}", intval($value), 'numeric');
1579 }
1580
1581 /**
1582 * Check whether a feed exists in the database, based on its URL.
1583 *
1584 * @param string $url Address of feed to be checked.
1585 *
1586 * @return mixed Requested {@link RF_Feed feed}, or false if none found.
1587 *
1588 * @uses RF_Controller::getReadHandle()
1589 */
1590 function feedExistsWithURL($url)
1591 {
1592 $dbhr =& $this->getReadHandle();
1593
1594 $query = sprintf("SELECT id, url, title, link, description
1595 FROM %s
1596 WHERE url = %s",
1597 $dbhr->quoteIdentifier(RF_Feed::tableName()),
1598 $dbhr->quoteSmart($url));
1599
1600 $result = $dbhr->getRow($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__, DB_FETCHMODE_ASSOC);
1601
1602 return $result
1603 ? new RF_Feed($result)
1604 : false;
1605 }
1606
1607 /**
1608 * Check whether an item exists in the database, based on its GUID
1609 *
1610 * @param RF_Feed $feed Feed to be checked for the item.
1611 * @param string $guid GUID of the item in question.
1612 *
1613 * @return mixed Requested {@link RF_Item item}, or false if none found.
1614 *
1615 * @uses RF_Controller::getReadHandle()
1616 */
1617 function itemExistsWithGUID($feed, $guid)
1618 {
1619 $dbhr =& $this->getReadHandle();
1620
1621 $query = sprintf("SELECT i.id, i.feed_id, i.guid, i.title, i.link, i.content, i.modified, i.author, i.category
1622 FROM %s AS i
1623 WHERE i.feed_id = %d
1624 AND i.guid = %s",
1625 $dbhr->quoteIdentifier(RF_Item::tableName()),
1626 $feed->getID(),
1627 $dbhr->quoteSmart($guid));
1628
1629 $result = $dbhr->getRow($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__, DB_FETCHMODE_ASSOC);
1630
1631 // `modified' is returned in "YYYY-MM-DD HH:MM:SS" format
1632 if(isset($result['modified']))
1633 $result['modified'] = strtotime($result['modified']);
1634
1635 return $result
1636 ? new RF_Item($result)
1637 : false;
1638 }
1639
1640 /**
1641 * Save a feed into the database.
1642 * @param RF_Feed $feed -
1643 * @return boolean True
1644 * @uses RF_Controller::getWriteHandle()
1645 * @uses RF_Controller::getReadHandle()
1646 */
1647 function saveFeed(&$feed)
1648 {
1649 $dbhw =& $this->getWriteHandle();
1650 $data = $feed->columnNamesValues();
1651
1652 // the insert timestamp only gets set the first time.
1653 if(!$feed->getID())
1654 $data['insert_timestamp'] = date('YmdHis');
1655
1656 $query = $feed->getID()
1657 ? $dbhw->autoPrepare($feed->tableName(), array_keys($data), DB_AUTOQUERY_UPDATE, 'id = '.$dbhw->quoteSmart($feed->getID()))
1658 : $dbhw->autoPrepare($feed->tableName(), array_keys($data), DB_AUTOQUERY_INSERT);
1659
1660 $result = $dbhw->execute($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__, array_values($data));
1661
1662 if(DB::isError($result))
1663 die(join(":", array($result->getMessage(), $result->getDebugInfo())));
1664
1665 $feed->setID($feed->getID()
1666 ? $feed->getID()
1667 : $dbhw->getOne("SELECT LAST_INSERT_ID()"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__));
1668
1669 return true;
1670 }
1671
1672 /**
1673 * Save an item into the database.
1674 * @param RF_Item $item -
1675 * @return boolean True
1676 * @uses RF_Controller::getWriteHandle()
1677 * @uses RF_Controller::getReadHandle()
1678 */
1679 function saveItem(&$item)
1680 {
1681 $dbhw =& $this->getWriteHandle();
1682 $data = $item->columnNamesValues();
1683
1684 // sanitize all links being saved with single quote
1685 // Thanks to TDavid: http://www.php-scripts.com/20060617/86/
1686 $data['link'] = str_replace("'","%27",$data['link']);
1687
1688 // the insert timestamp only gets set the first time.
1689 if(!$item->getID())
1690 $data['insert_timestamp'] = date('YmdHis');
1691
1692 $query = $item->getID()
1693 ? $dbhw->autoPrepare($item->tableName(), array_keys($data), DB_AUTOQUERY_UPDATE, 'id = '.$dbhw->quoteSmart($item->getID()))
1694 : $dbhw->autoPrepare($item->tableName(), array_keys($data), DB_AUTOQUERY_INSERT);
1695
1696 $result = $dbhw->execute($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__, array_values($data));
1697
1698 if(DB::isError($result))
1699 die(join(":", array($result->getMessage(), $result->getDebugInfo())));
1700
1701 $item->setID($item->getID()
1702 ? $item->getID()
1703 : $dbhw->getOne("SELECT LAST_INSERT_ID()"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__));
1704
1705 return true;
1706 }
1707
1708 /**
1709 * Retrieve labeled metadata for a given user's RSS item.
1710 *
1711 * @param RF_Item $item Item to match
1712 * @param RF_User $user User to match
1713 * @param string $label Label to match
1714 * @param string $format Format to match, one of:
1715 * - "numeric" for integer value
1716 * - "short" for short string
1717 * - "long" for long string
1718 *
1719 * @return array Array of retrieved values.
1720 *
1721 * @uses RF_Controller::getReadHandle()
1722 * @uses RF_Controller::readFromDatabase()
1723 * @uses RF_Item::userdataTableName()
1724 * @see RF_Controller::setItemUserdata()
1725 * @see RF_Controller::setItemsUserdata()
1726 * @see RF_Controller::removeItemUserdata()
1727 */
1728 function getItemUserdata($item, $user, $label, $format)
1729 {
1730 $dbhr =& $this->getReadHandle();
1731
1732 $query = sprintf("SELECT value_%s AS value
1733 FROM %s
1734 WHERE user_id = %d
1735 AND item_id = %d
1736 AND label = %s",
1737 $dbhr->escapeSimple($format),
1738 $dbhr->quoteIdentifier($item->userdataTableName()),
1739 (is_null($user) ? 0 : $user->getID()),
1740 $item->getID(),
1741 $dbhr->quoteSmart($label));
1742
1743 $values = array();
1744 $result = $this->readFromDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1745
1746 while($value = $result->fetchRow(DB_FETCHMODE_ASSOC))
1747 $values[] = $value['value'];
1748
1749 return $values;
1750 }
1751
1752 /**
1753 * Retrieve labeled metadata for a given user's RSS feed.
1754 *
1755 * @param RF_Feed $feed Feed to match
1756 * @param RF_User $user User to match
1757 * @param string $label Label to match
1758 * @param string $format Format to match, one of:
1759 * - "numeric" for integer value
1760 * - "short" for short string
1761 * - "long" for long string
1762 *
1763 * @return array Array of retrieved values.
1764 *
1765 * @uses RF_Controller::getReadHandle()
1766 * @uses RF_Controller::readFromDatabase()
1767 * @uses RF_Feed::userdataTableName()
1768 * @see RF_Controller::setFeedUserdata()
1769 * @see RF_Controller::setFeedsUserdata()
1770 * @see RF_Controller::removeFeedUserdata()
1771 */
1772 function getFeedUserdata($feed, $user, $label, $format)
1773 {
1774 $dbhr =& $this->getReadHandle();
1775
1776 $query = sprintf("SELECT value_%s AS value
1777 FROM %s
1778 WHERE user_id = %d
1779 AND feed_id = %d
1780 AND label = %s",
1781 $dbhr->escapeSimple($format),
1782 $dbhr->quoteIdentifier($feed->userdataTableName()),
1783 (is_null($user) ? 0 : $user->getID()),
1784 $feed->getID(),
1785 $dbhr->quoteSmart($label));
1786
1787 $values = array();
1788 $result = $this->readFromDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1789
1790 while($value = $result->fetchRow(DB_FETCHMODE_ASSOC))
1791 $values[] = $value['value'];
1792
1793 return $values;
1794 }
1795
1796 /**
1797 * Assign labeled metadata to a given user's RSS item.
1798 *
1799 * @param RF_Item $item Item to assign to
1800 * @param RF_User $user User to assign to
1801 * @param string $label Label to assign to
1802 * @param mixed $values Value or array of values to assign to - note format!
1803 * @param string $format Format to assign to, one of:
1804 * - "numeric" for integer value
1805 * - "short" for short string
1806 * - "long" for long string
1807 *
1808 * @return boolean Return value of {@link RF_Controller::setItemsUserdata() RF_Controller::setItemsUserdata()}
1809 *
1810 * @uses RF_Controller::setItemsUserdata()
1811 * @see RF_Controller::getItemUserdata()
1812 * @see RF_Controller::removeItemUserdata()
1813 */
1814 function setItemUserdata(&$item, &$user, $label, $values, $format)
1815 {
1816 return $this->setItemsUserdata(array(&$item), $user, $label, $values, $format);
1817 }
1818
1819 /**
1820 * Assign labeled metadata to a given user's RSS feed.
1821 *
1822 * @param RF_Feed $feed Feed to assign to
1823 * @param RF_User $user User to assign to
1824 * @param string $label Label to assign to
1825 * @param mixed $values Value or array of values to assign to - note format!
1826 * @param string $format Format to assign to, one of:
1827 * - "numeric" for integer value
1828 * - "short" for short string
1829 * - "long" for long string
1830 *
1831 * @return boolean Return value of {@link RF_Controller::setFeedsUserdata() RF_Controller::setFeedsUserdata()}
1832 *
1833 * @uses RF_Controller::setFeedsUserdata()
1834 * @see RF_Controller::getFeedUserdata()
1835 * @see RF_Controller::removeFeedUserdata()
1836 */
1837 function setFeedUserdata(&$feed, &$user, $label, $values, $format)
1838 {
1839 return $this->setFeedsUserdata(array(&$feed), $user, $label, $values, $format);
1840 }
1841
1842 /**
1843 * Remove labeled metadata from a given user's RSS feed.
1844 *
1845 * @param RF_Feed $feed Feed to assign to
1846 * @param RF_User $user User to assign to
1847 * @param string $label Label to assign to
1848 *
1849 * @return boolean Return value of {@link RF_Controller::writeToDatabase() RF_Controller::writeToDatabase()}
1850 *
1851 * @uses RF_Controller::getReadHandle()
1852 * @uses RF_Controller::writeToDatabase()
1853 * @see RF_Controller::getFeedUserdata()
1854 * @see RF_Controller::setFeedUserdata()
1855 * @see RF_Controller::setFeedsUserdata()
1856 */
1857 function removeFeedUserdata(&$feed, &$user, $label)
1858 {
1859 $dbhr =& $this->getReadHandle();
1860
1861 $query = sprintf("DELETE FROM %s
1862 WHERE user_id = %d
1863 AND feed_id = %d
1864 AND label = %s",
1865 $dbhr->quoteIdentifier($feed->userdataTableName()),
1866 (is_null($user) ? 0 : $user->getID()),
1867 $feed->getID(),
1868 $dbhr->quoteSmart($label));
1869
1870 return $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1871 }
1872
1873 /**
1874 * Remove labeled metadata from a given user's RSS item.
1875 *
1876 * @param RF_Item $item Item to assign to
1877 * @param RF_User $user User to assign to
1878 * @param string $label Label to assign to
1879 *
1880 * @return boolean Return value of {@link RF_Controller::writeToDatabase() RF_Controller::writeToDatabase()}
1881 *
1882 * @uses RF_Controller::getReadHandle()
1883 * @uses RF_Item::userdataTableName()
1884 * @uses RF_Controller::writeToDatabase()
1885 * @see RF_Controller::getItemUserdata()
1886 * @see RF_Controller::setItemUserdata()
1887 * @see RF_Controller::setItemsUserdata()
1888 */
1889 function removeItemUserdata(&$item, &$user, $label)
1890 {
1891 $dbhr =& $this->getReadHandle();
1892
1893 $query = sprintf("DELETE FROM %s
1894 WHERE user_id = %d
1895 AND item_id = %d
1896 AND label = %s",
1897 $dbhr->quoteIdentifier($item->userdataTableName()),
1898 (is_null($user) ? 0 : $user->getID()),
1899 $item->getID(),
1900 $dbhr->quoteSmart($label));
1901
1902 return $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1903 }
1904
1905 /**
1906 * Assign labeled metadata to a given user's RSS items.
1907 *
1908 * @param array $items Items to assign to - instances of RF_Item
1909 * @param RF_User $user User to assign to
1910 * @param string $label Label to assign to
1911 * @param mixed $values Value or array of values to assign to - note format!
1912 * @param string $format Format to assign to, one of:
1913 * - "numeric" for integer value
1914 * - "short" for short string
1915 * - "long" for long string
1916 *
1917 * @return boolean True
1918 *
1919 * @uses RF_Controller::getReadHandle()
1920 * @uses RF_Item::userdataTableName()
1921 * @uses RF_Controller::$db_transactions
1922 * @uses RF_Controller::writeToDatabase()
1923 * @see RF_Controller::getItemUserdata()
1924 * @see RF_Controller::setItemUserdata()
1925 * @see RF_Controller::removeItemUserdata()
1926 */
1927 function setItemsUserdata($items, $user, $label, $values, $format)
1928 {
1929 $dbhr =& $this->getReadHandle();
1930
1931 if(!count($items))
1932 return true;
1933
1934 if(!is_array($values))
1935 $values = array($values);
1936
1937 $item_ids = array();
1938
1939 foreach($items as $item)
1940 $item_ids[] = $item->getID();
1941
1942 if($this->db_transactions)
1943 $this->writeToDatabase("START TRANSACTION"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1944
1945 // delete existing userdata
1946 $query = sprintf("DELETE FROM %s
1947 WHERE user_id = %d
1948 AND item_id IN (%s)
1949 AND label = %s",
1950 $dbhr->quoteIdentifier($item->userdataTableName()),
1951 (is_null($user) ? 0 : $user->getID()),
1952 join(', ', $item_ids),
1953 $dbhr->quoteSmart($label));
1954
1955 $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1956
1957 if(count($values) > 0) {
1958
1959 // generate SQL value tuples
1960 $value_tuples = array();
1961
1962 foreach($items as $item)
1963 foreach($values as $value)
1964 $value_tuples[] = sprintf("(%d, %d, %s, %s)",
1965 (is_null($user) ? 0 : $user->getID()),
1966 $item->getID(),
1967 $dbhr->quoteSmart($label),
1968 $dbhr->quoteSmart($value));
1969
1970 // add new user data
1971 $query = sprintf("INSERT INTO %s
1972 (user_id, item_id, label, value_%s)
1973 VALUES %s",
1974 $dbhr->quoteIdentifier($items[0]->userdataTableName()),
1975 $dbhr->escapeSimple($format),
1976 join(', ', $value_tuples));
1977
1978 $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1979 }
1980
1981 if($this->db_transactions)
1982 $this->writeToDatabase("COMMIT"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
1983
1984 return true;
1985 }
1986
1987 /**
1988 * Assign labeled metadata to a given user's RSS feeds.
1989 *
1990 * @param array $feeds Feeds to assign to - instances of RF_Feed
1991 * @param RF_User $user User to assign to
1992 * @param string $label Label to assign to
1993 * @param mixed $values Value or array of values to assign to - note format!
1994 * @param string $format Format to assign to, one of:
1995 * - "numeric" for integer value
1996 * - "short" for short string
1997 * - "long" for long string
1998 *
1999 * @return boolean True
2000 *
2001 * @uses RF_Controller::getReadHandle()
2002 * @uses RF_Feed::userdataTableName()
2003 * @uses RF_Controller::$db_transactions
2004 * @uses RF_Controller::writeToDatabase()
2005 * @see RF_Controller::getFeedUserdata()
2006 * @see RF_Controller::setFeedUserdata()
2007 * @see RF_Controller::removeFeedUserdata()
2008 */
2009 function setFeedsUserdata($feeds, $user, $label, $values, $format)
2010 {
2011 $dbhr =& $this->getReadHandle();
2012
2013 if(!count($feeds))
2014 return true;
2015
2016 if(!is_array($values))
2017 $values = array($values);
2018
2019 $feed_ids = array();
2020
2021 foreach($feeds as $feed)
2022 $feed_ids[] = $feed->getID();
2023
2024 if($this->db_transactions)
2025 $this->writeToDatabase("START TRANSACTION"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
2026
2027 // delete existing userdata
2028 $query = sprintf("DELETE FROM %s
2029 WHERE user_id = %d
2030 AND feed_id IN (%s)
2031 AND label = %s",
2032 $dbhr->quoteIdentifier($feed->userdataTableName()),
2033 (is_null($user) ? 0 : $user->getID()),
2034 join(', ', $feed_ids),
2035 $dbhr->quoteSmart($label));
2036
2037 $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
2038
2039 if(count($values) > 0) {
2040
2041 // generate SQL value tuples
2042 $value_tuples = array();
2043
2044 foreach($feeds as $feed)
2045 foreach($values as $value)
2046 $value_tuples[] = sprintf("(%d, %d, %s, %s)",
2047 (is_null($user) ? 0 : $user->getID()),
2048 $feed->getID(),
2049 $dbhr->quoteSmart($label),
2050 $dbhr->quoteSmart($value));
2051
2052 // add new user data
2053 $query = sprintf("INSERT INTO %s
2054 (user_id, feed_id, label, value_%s)
2055 VALUES %s",
2056 $dbhr->quoteIdentifier($feeds[0]->userdataTableName()),
2057 $dbhr->escapeSimple($format),
2058 join(', ', $value_tuples));
2059
2060 $this->writeToDatabase($query."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
2061 }
2062
2063 if($this->db_transactions)
2064 $this->writeToDatabase("COMMIT"."\n# Context: ".__CLASS__.":".__FUNCTION__."() ".__FILE__.":".__LINE__);
2065
2066 return true;
2067 }
2068
2069 /**
2070 * Prepare arguments for additional userdata to be
2071 * passed to getUserItems or getUserFeeds.
2072 *
2073 * @param array source Array of metadata information:
2074 * {label: {format: value}, label: {format: value}, ...}
2075 * @return array Array of metadata information:
2076 * [{"format": format, "label": label, "value": value}, ...]
2077 *
2078 * @see RF_Controller::getUserItems()
2079 * @see RF_Controller::getUserFeeds()
2080 */
2081 function prepareUserdataArguments($source)
2082 {
2083 // these seem to get used all the time
2084 $destination = array(array('format' => 'long', 'label' => 'comment'),
2085 array('format' => 'long', 'label' => 'tags'));
2086
2087 /*
2088 source[metadata] should look like:
2089 [{label: {format: value}}, {label: {format: value}}, ...]
2090 */
2091 if(is_array($source))
2092 foreach($source as $label => $metadatum)
2093 if(is_array($metadatum))
2094 foreach($metadatum as $format => $value)
2095 $destination[] = compact('format', 'label', 'value');
2096
2097 /*
2098 return an array composed of triplets:
2099 {'label': label, 'format': format, 'value': value}
2100 'value' is optional, it's used for matches.
2101 this is passed as an element of the $args array,
2102 the second parameter to RF_Controller::getUserItems()
2103 */
2104 return $destination;
2105 }
2106
2107 /**
2108 * Find plugin files.
2109 *
2110 * Looks in plugins directory, and notes every encountered file
2111 * named "*.plugin.php". Looks in subdirectories recursively.
2112 *
2113 * @param string $dir Optional starting directory - omit on initial call
2114 * @return array Array of file names
2115 */
2116 function findPlugins($dir=null)
2117 {
2118 $plugin_files = array();
2119
2120 $plugins_dir = is_null($dir)
2121 ? realpath(join(DIRECTORY_SEPARATOR, array(dirname(__FILE__), '..', '..', 'plugins')))
2122 : $dir;
2123
2124 // Find and remember all plug-in files
2125 if(is_dir($plugins_dir) && $d = opendir($plugins_dir))
2126 while($f = readdir($d)) {
2127
2128 $p = $plugins_dir.DIRECTORY_SEPARATOR.$f;
2129
2130 if(is_file($p) && preg_match('/\.plugin\.php$/i', $f))
2131 $plugin_files[] = $p;
2132
2133 if(is_dir($p) && !in_array($f, array('.', '..')))
2134 $plugin_files = array_merge($plugin_files, $this->findPlugins($p));
2135 }
2136
2137 if($d)
2138 closedir($d);
2139
2140 return $plugin_files;
2141 }
2142
2143 /**
2144 * Load plugin classes.
2145 *
2146 * Finds all plugin files and includes them. Then, looks through all
2147 * declared classes and instantiates those with $activePlugin === true
2148 * into {@link RF_Controller::$plugins $plugins} array.
2149 *
2150 * @uses RF_Controller::findPlugins()
2151 * @uses RF_Controller::$plugins Assigned after finding active plugins.
2152 * @uses RF_Controller::invokePlugin() "loadedPlugins" event.
2153 */
2154 function loadPlugins()
2155 {
2156 // first include each plug-in file - hope they're all valid php!
2157 foreach($this->findPlugins() as $plugin_file)
2158 include_once($plugin_file);
2159
2160 // loop through all declared classes, and instantiate them
2161 // into the plugins array if they have $activePlugin === true
2162 foreach(get_declared_classes() as $c)
2163 if(is_array(get_class_vars($c)))
2164 foreach(get_class_vars($c) as $var => $val)
2165 if($var == 'activePlugin' && $val === true)
2166 $this->plugins[$c] = new $c($this);
2167
2168 // while we're at it, mash all the members of remoteMethods
2169 // to lowercase so invokePlugin can figure out what to do.
2170 foreach($this->plugins as $p => $plugin)
2171 if(isset($plugin->remoteMethods) && is_array($plugin->remoteMethods))
2172 foreach($plugin->remoteMethods as $m => $method)
2173 $this->plugins[$p]->remoteMethods[$m] = strtolower($method);
2174
2175 $this->invokePlugin('loadedPlugins');
2176 }
2177
2178 /**
2179 * Invoke plugins.
2180 *
2181 * Invokes methods matching $event on each plugin in the plugins array.
2182 * If a given plugin lacks the named method, it is skipped.
2183 * Alternatively, a specific plug=in may be invoked by using dot-syntax
2184 * method names, e.g. "plugin_name.event", where the plug-in's name
2185 * is its class.
2186 *
2187 * @param string $event Method to invoke on each plugin.
2188 * @param array $params Arguments to pass to invoked method.
2189 * @param boolean $remote Was this invokation the result of a remote API call?
2190 *
2191 * @return mixed Array of return values from each responsive plug-in,
2192 * or just one return value if a specific lpug-in is named.
2193 *
2194 * @uses RF_Controller::$plugins Searched for plugins with method names matching $event.
2195 */
2196 function invokePlugin($event, $params=array(), $remote=false)
2197 {
2198 if(strpos($event, '.')) {
2199 list($plugin, $event) = split('\.', strtolower($event));
2200
2201 if($plugin =& $this->plugins[$plugin])
2202 if(method_exists($plugin, $event) && (!$remote || is_array($plugin->remoteMethods) && in_array(strtolower($event), $plugin->remoteMethods)))
2203 return call_user_func_array(array(&$plugin, $event), $params);
2204
2205 } else {
2206 $results = array();
2207
2208 foreach($this->plugins as $p => $plugin)
2209 if(method_exists($plugin, $event) && (!$remote || is_array($plugin->remoteMethods) && in_array(strtolower($event), $plugin->remoteMethods)))
2210 $results[$p] = call_user_func_array(array(&$this->plugins[$p], $event), $params);
2211
2212 return $results;
2213 }
2214 }
2215
2216 /**
2217 * Grab a reference to the read DB handle
2218 *
2219 * @return DB instance of PEAR DB handle suitable for reading from the database
2220 *
2221 * @uses RF_Controller::$dbhr Returned upon request
2222 */
2223 function &getReadHandle()
2224 {
2225 return $this->dbhr;
2226 }
2227
2228 /**
2229 * Grab a reference to the write DB handle
2230 *
2231 * @return DB instance of PEAR DB handle suitable for writing to the database
2232 *
2233 * @uses RF_Controller::$dbhw Returned upon request
2234 */
2235 function &getWriteHandle()
2236 {
2237 return $this->dbhw;
2238 }
2239
2240 /**
2241 * Pass a read query to {@link RF_Controller::$dbhr the database}, and get the results back.
2242 *
2243 * @param string $query SQL query to execute.
2244 * @param mixed $params Array, string or numeric data to be used in execution
2245 * of the statement. Quantity of items passed must match
2246 * quantity of placeholders in query: meaning 1 placeholder
2247 * for non-array parameters or 1 placeholder per array element.
2248 * @param array $acceptable_errors Normally any error would cause the script to die here,
2249 * but this array can be populated with errors (constants
2250 * from {@link DB.php DB.php}) that are quietly ignored.
2251 * @return DB_result Results of query execution.
2252 *
2253 * @uses RF_Controller::getReadHandle()
2254 */
2255 function readFromDatabase($query, $params=array(), $acceptable_errors=array())
2256 {
2257 $dbhr =& $this->getReadHandle();
2258
2259 $result = $dbhr->query($query, $params);
2260
2261 if(DB::isError($result) && !in_array($result->getCode(), $acceptable_errors))
2262 if($result->getCode() == DB_ERROR_NOSUCHTABLE || $result->getCode() == DB_ERROR_NOSUCHDB) {
2263 die(sprintf('<p><b>%s.</b> Have you tried <a href="install.php">installing Refeed</a>?</p>', htmlspecialchars($result->getMessage())));
2264
2265 } else {
2266 die(sprintf('<p><b>%s.</b></p><pre>%s</pre>', htmlspecialchars($result->getMessage()), htmlspecialchars($result->getDebugInfo())));
2267
2268 }
2269
2270 return $result;
2271 }
2272
2273 /**
2274 * Pass a write query to {@link RF_Controller::$dbhw the database}, and get the results back.
2275 *
2276 * @param string $query SQL query to execute.
2277 * @param mixed $params Array, string or numeric data to be used in execution
2278 * of the statement. Quantity of items passed must match
2279 * quantity of placeholders in query: meaning 1 placeholder
2280 * for non-array parameters or 1 placeholder per array element.
2281 * @param array $acceptable_errors Normally any error would cause the script to die here,
2282 * but this array can be populated with errors (constants
2283 * from {@link DB.php DB.php}) that are quietly ignored.
2284 * @return DB_result Results of query execution.
2285 */
2286 function writeToDatabase($query, $params=array(), $acceptable_errors=array())
2287 {
2288 $dbhr =& $this->getWriteHandle();
2289
2290 $result = $dbhr->query($query, $params);
2291
2292 if(DB::isError($result) && !in_array($result->getCode(), $acceptable_errors))
2293 if($result->getCode() == DB_ERROR_NOSUCHTABLE || $result->getCode() == DB_ERROR_NOSUCHDB) {
2294 die(sprintf('<p><b>%s.</b> Have you tried <a href="install.php">installing Refeed</a>?</p>', htmlspecialchars($result->getMessage())));
2295
2296 } else {
2297 die(sprintf('<p><b>%s.</b></p><pre>%s</pre>', htmlspecialchars($result->getMessage()), htmlspecialchars($result->getDebugInfo())));
2298
2299 }
2300
2301 return $result;
2302 }
2303 }
2304
2305 ?>

  ViewVC Help
Powered by ViewVC 1.1.26