Profile_list.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. <?php
  2. /**
  3. * StatusNet - the distributed open-source microblogging tool
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. * @category Notices
  19. * @package StatusNet
  20. * @author Shashi Gowda <connect2shashi@gmail.com>
  21. * @license GNU Affero General Public License http://www.gnu.org/licenses/
  22. */
  23. if (!defined('GNUSOCIAL')) { exit(1); }
  24. class Profile_list extends Managed_DataObject
  25. {
  26. public $__table = 'profile_list'; // table name
  27. public $id; // int(4) primary_key not_null
  28. public $tagger; // int(4)
  29. public $tag; // varchar(64)
  30. public $description; // text
  31. public $private; // tinyint(1)
  32. public $created; // datetime not_null default_0000-00-00%2000%3A00%3A00
  33. public $modified; // timestamp not_null default_CURRENT_TIMESTAMP
  34. public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space
  35. public $mainpage; // varchar(191) not 255 because utf8mb4 takes more space
  36. public $tagged_count; // smallint
  37. public $subscriber_count; // smallint
  38. public static function schemaDef()
  39. {
  40. return array(
  41. 'fields' => array(
  42. 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
  43. 'tagger' => array('type' => 'int', 'not null' => true, 'description' => 'user making the tag'),
  44. 'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'people tag'),
  45. 'description' => array('type' => 'text', 'description' => 'description of the people tag'),
  46. 'private' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'is this tag private'),
  47. 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the tag was added'),
  48. 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date the tag was modified'),
  49. 'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universal identifier'),
  50. 'mainpage' => array('type' => 'varchar', 'length' => 191, 'description' => 'page to link to'),
  51. 'tagged_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of people tagged with this tag by this user'),
  52. 'subscriber_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of subscribers to this tag'),
  53. ),
  54. 'primary key' => array('tagger', 'tag'),
  55. 'unique keys' => array(
  56. 'profile_list_id_key' => array('id')
  57. ),
  58. 'foreign keys' => array(
  59. 'profile_list_tagger_fkey' => array('profile', array('tagger' => 'id')),
  60. ),
  61. 'indexes' => array(
  62. 'profile_list_modified_idx' => array('modified'),
  63. 'profile_list_tag_idx' => array('tag'),
  64. 'profile_list_tagger_tag_idx' => array('tagger', 'tag'),
  65. 'profile_list_tagged_count_idx' => array('tagged_count'),
  66. 'profile_list_subscriber_count_idx' => array('subscriber_count'),
  67. ),
  68. );
  69. }
  70. /**
  71. * get the tagger of this profile_list object
  72. *
  73. * @return Profile the tagger
  74. */
  75. function getTagger()
  76. {
  77. return Profile::getByID($this->tagger);
  78. }
  79. /**
  80. * return a string to identify this
  81. * profile_list in the user interface etc.
  82. *
  83. * @return String
  84. */
  85. function getBestName()
  86. {
  87. return $this->tag;
  88. }
  89. /**
  90. * return a uri string for this profile_list
  91. *
  92. * @return String uri
  93. */
  94. function getUri()
  95. {
  96. $uri = null;
  97. if (Event::handle('StartProfiletagGetUri', array($this, &$uri))) {
  98. if (!empty($this->uri)) {
  99. $uri = $this->uri;
  100. } else {
  101. $uri = common_local_url('profiletagbyid',
  102. array('id' => $this->id, 'tagger_id' => $this->tagger));
  103. }
  104. }
  105. Event::handle('EndProfiletagGetUri', array($this, &$uri));
  106. return $uri;
  107. }
  108. /**
  109. * return a url to the homepage of this item
  110. *
  111. * @return String home url
  112. */
  113. function homeUrl()
  114. {
  115. $url = null;
  116. if (Event::handle('StartUserPeopletagHomeUrl', array($this, &$url))) {
  117. // normally stored in mainpage, but older ones may be null
  118. if (!empty($this->mainpage)) {
  119. $url = $this->mainpage;
  120. } else {
  121. $url = common_local_url('showprofiletag',
  122. array('nickname' => $this->getTagger()->nickname,
  123. 'tag' => $this->tag));
  124. }
  125. }
  126. Event::handle('EndUserPeopletagHomeUrl', array($this, &$url));
  127. return $url;
  128. }
  129. /**
  130. * return an immutable url for this object
  131. *
  132. * @return String permalink
  133. */
  134. function permalink()
  135. {
  136. $url = null;
  137. if (Event::handle('StartProfiletagPermalink', array($this, &$url))) {
  138. $url = common_local_url('profiletagbyid',
  139. array('id' => $this->id));
  140. }
  141. Event::handle('EndProfiletagPermalink', array($this, &$url));
  142. return $url;
  143. }
  144. /**
  145. * Query notices by users associated with this tag,
  146. * but first check the cache before hitting the DB.
  147. *
  148. * @param integer $offset offset
  149. * @param integer $limit maximum no of results
  150. * @param integer $since_id=null since this id
  151. * @param integer $max_id=null maximum id in result
  152. *
  153. * @return Notice the query
  154. */
  155. function getNotices($offset, $limit, $since_id=null, $max_id=null)
  156. {
  157. // FIXME: Use something else than Profile::current() to avoid
  158. // possible confusion between session user and queue processing.
  159. $stream = new PeopletagNoticeStream($this, Profile::current());
  160. return $stream->getNotices($offset, $limit, $since_id, $max_id);
  161. }
  162. /**
  163. * Get subscribers (local and remote) to this people tag
  164. * Order by reverse chronology
  165. *
  166. * @param integer $offset offset
  167. * @param integer $limit maximum no of results
  168. * @param integer $since_id=null since unix timestamp
  169. * @param integer $upto=null maximum unix timestamp when subscription was made
  170. *
  171. * @return Profile results
  172. */
  173. function getSubscribers($offset=0, $limit=null, $since=0, $upto=0)
  174. {
  175. $subs = new Profile();
  176. $subs->joinAdd(
  177. array('id', 'profile_tag_subscription:profile_id')
  178. );
  179. $subs->whereAdd('profile_tag_subscription.profile_tag_id = ' . $this->id);
  180. $subs->selectAdd('unix_timestamp(profile_tag_subscription.' .
  181. 'created) as "cursor"');
  182. if ($since != 0) {
  183. $subs->whereAdd('cursor > ' . $since);
  184. }
  185. if ($upto != 0) {
  186. $subs->whereAdd('cursor <= ' . $upto);
  187. }
  188. if ($limit != null) {
  189. $subs->limit($offset, $limit);
  190. }
  191. $subs->orderBy('profile_tag_subscription.created DESC');
  192. $subs->find();
  193. return $subs;
  194. }
  195. /**
  196. * Get all and only local subscribers to this people tag
  197. * used for distributing notices to user inboxes.
  198. *
  199. * @return array ids of users
  200. */
  201. function getUserSubscribers()
  202. {
  203. // XXX: cache this
  204. $user = new User();
  205. if(common_config('db','quote_identifiers'))
  206. $user_table = '"user"';
  207. else $user_table = 'user';
  208. $qry =
  209. 'SELECT id ' .
  210. 'FROM '. $user_table .' JOIN profile_tag_subscription '.
  211. 'ON '. $user_table .'.id = profile_tag_subscription.profile_id ' .
  212. 'WHERE profile_tag_subscription.profile_tag_id = %d ';
  213. $user->query(sprintf($qry, $this->id));
  214. $ids = array();
  215. while ($user->fetch()) {
  216. $ids[] = $user->id;
  217. }
  218. $user->free();
  219. return $ids;
  220. }
  221. /**
  222. * Check to see if a given profile has
  223. * subscribed to this people tag's timeline
  224. *
  225. * @param mixed $id User or Profile object or integer id
  226. *
  227. * @return boolean subscription status
  228. */
  229. function hasSubscriber($id)
  230. {
  231. if (!is_numeric($id)) {
  232. $id = $id->id;
  233. }
  234. $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $this->id,
  235. 'profile_id' => $id));
  236. return !empty($sub);
  237. }
  238. /**
  239. * Get profiles tagged with this people tag,
  240. * include modified timestamp as a "cursor" field
  241. * order by descending order of modified time
  242. *
  243. * @param integer $offset offset
  244. * @param integer $limit maximum no of results
  245. * @param integer $since_id=null since unix timestamp
  246. * @param integer $upto=null maximum unix timestamp when subscription was made
  247. *
  248. * @return Profile results
  249. */
  250. function getTagged($offset=0, $limit=null, $since=0, $upto=0)
  251. {
  252. $tagged = new Profile();
  253. $tagged->joinAdd(array('id', 'profile_tag:tagged'));
  254. #@fixme: postgres
  255. $tagged->selectAdd('unix_timestamp(profile_tag.modified) as "cursor"');
  256. $tagged->whereAdd('profile_tag.tagger = '.$this->tagger);
  257. $tagged->whereAdd("profile_tag.tag = '{$this->tag}'");
  258. if ($since != 0) {
  259. $tagged->whereAdd('cursor > ' . $since);
  260. }
  261. if ($upto != 0) {
  262. $tagged->whereAdd('cursor <= ' . $upto);
  263. }
  264. if ($limit != null) {
  265. $tagged->limit($offset, $limit);
  266. }
  267. $tagged->orderBy('profile_tag.modified DESC');
  268. $tagged->find();
  269. return $tagged;
  270. }
  271. /**
  272. * Gracefully delete one or many people tags
  273. * along with their members and subscriptions data
  274. *
  275. * @return boolean success
  276. */
  277. function delete($useWhere=false)
  278. {
  279. // force delete one item at a time.
  280. if (empty($this->id)) {
  281. $this->find();
  282. while ($this->fetch()) {
  283. $this->delete();
  284. }
  285. }
  286. Profile_tag::cleanup($this);
  287. Profile_tag_subscription::cleanup($this);
  288. self::blow('profile:lists:%d', $this->tagger);
  289. return parent::delete($useWhere);
  290. }
  291. /**
  292. * Update a people tag gracefully
  293. * also change "tag" fields in profile_tag table
  294. *
  295. * @param Profile_list $dataObject Object's original form
  296. *
  297. * @return boolean success
  298. */
  299. function update($dataObject=false)
  300. {
  301. if (!is_object($dataObject) && !$dataObject instanceof Profile_list) {
  302. return parent::update($dataObject);
  303. }
  304. $result = true;
  305. // if original tag was different
  306. // check to see if the new tag already exists
  307. // if not, rename the tag correctly
  308. if($dataObject->tag != $this->tag || $dataObject->tagger != $this->tagger) {
  309. $existing = Profile_list::getByTaggerAndTag($this->tagger, $this->tag);
  310. if(!empty($existing)) {
  311. // TRANS: Server exception.
  312. throw new ServerException(_('The tag you are trying to rename ' .
  313. 'to already exists.'));
  314. }
  315. // move the tag
  316. // XXX: allow OStatus plugin to send out profile tag
  317. $result = Profile_tag::moveTag($dataObject, $this);
  318. }
  319. return parent::update($dataObject);
  320. }
  321. /**
  322. * return an xml string representing this people tag
  323. * as the author of an atom feed
  324. *
  325. * @return string atom author element
  326. */
  327. function asAtomAuthor()
  328. {
  329. $xs = new XMLStringer(true);
  330. $tagger = $this->getTagger();
  331. $xs->elementStart('author');
  332. $xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag);
  333. $xs->element('uri', null, $this->permalink());
  334. $xs->elementEnd('author');
  335. return $xs->getString();
  336. }
  337. /**
  338. * return an xml string to represent this people tag
  339. * as a noun in an activitystreams feed.
  340. *
  341. * @param string $element the xml tag
  342. *
  343. * @return string activitystreams noun
  344. */
  345. function asActivityNoun($element)
  346. {
  347. $noun = ActivityObject::fromPeopletag($this);
  348. return $noun->asString('activity:' . $element);
  349. }
  350. /**
  351. * get the cached number of profiles tagged with this
  352. * people tag, re-count if the argument is true.
  353. *
  354. * @param boolean $recount whether to ignore cache
  355. *
  356. * @return integer count
  357. */
  358. function taggedCount($recount=false)
  359. {
  360. $keypart = sprintf('profile_list:tagged_count:%d:%s',
  361. $this->tagger,
  362. $this->tag);
  363. $count = self::cacheGet($keypart);
  364. if ($count === false) {
  365. $tags = new Profile_tag();
  366. $tags->tag = $this->tag;
  367. $tags->tagger = $this->tagger;
  368. $count = $tags->count('distinct tagged');
  369. self::cacheSet($keypart, $count);
  370. }
  371. return $count;
  372. }
  373. /**
  374. * get the cached number of profiles subscribed to this
  375. * people tag, re-count if the argument is true.
  376. *
  377. * @param boolean $recount whether to ignore cache
  378. *
  379. * @return integer count
  380. */
  381. function subscriberCount($recount=false)
  382. {
  383. $keypart = sprintf('profile_list:subscriber_count:%d',
  384. $this->id);
  385. $count = self::cacheGet($keypart);
  386. if ($count === false) {
  387. $sub = new Profile_tag_subscription();
  388. $sub->profile_tag_id = $this->id;
  389. $count = (int) $sub->count('distinct profile_id');
  390. self::cacheSet($keypart, $count);
  391. }
  392. return $count;
  393. }
  394. /**
  395. * get the cached number of profiles subscribed to this
  396. * people tag, re-count if the argument is true.
  397. *
  398. * @param boolean $recount whether to ignore cache
  399. *
  400. * @return integer count
  401. */
  402. function blowNoticeStreamCache($all=false)
  403. {
  404. self::blow('profile_list:notice_ids:%d', $this->id);
  405. if ($all) {
  406. self::blow('profile_list:notice_ids:%d;last', $this->id);
  407. }
  408. }
  409. /**
  410. * get the Profile_list object by the
  411. * given tagger and with given tag
  412. *
  413. * @param integer $tagger the id of the creator profile
  414. * @param integer $tag the tag
  415. *
  416. * @return integer count
  417. */
  418. static function getByTaggerAndTag($tagger, $tag)
  419. {
  420. $ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag));
  421. return $ptag;
  422. }
  423. /**
  424. * create a profile_list record for a tag, tagger pair
  425. * if it doesn't exist, return it.
  426. *
  427. * @param integer $tagger the tagger
  428. * @param string $tag the tag
  429. * @param string $description description
  430. * @param boolean $private protected or not
  431. *
  432. * @return Profile_list the people tag object
  433. */
  434. static function ensureTag($tagger, $tag, $description=null, $private=false)
  435. {
  436. $ptag = Profile_list::getByTaggerAndTag($tagger, $tag);
  437. if(empty($ptag->id)) {
  438. $args = array(
  439. 'tag' => $tag,
  440. 'tagger' => $tagger,
  441. 'description' => $description,
  442. 'private' => $private
  443. );
  444. $new_tag = Profile_list::saveNew($args);
  445. return $new_tag;
  446. }
  447. return $ptag;
  448. }
  449. /**
  450. * get the maximum number of characters
  451. * that can be used in the description of
  452. * a people tag.
  453. *
  454. * determined by $config['peopletag']['desclimit']
  455. * if not set, falls back to $config['site']['textlimit']
  456. *
  457. * @return integer maximum number of characters
  458. */
  459. static function maxDescription()
  460. {
  461. $desclimit = common_config('peopletag', 'desclimit');
  462. // null => use global limit (distinct from 0!)
  463. if (is_null($desclimit)) {
  464. $desclimit = common_config('site', 'textlimit');
  465. }
  466. return $desclimit;
  467. }
  468. /**
  469. * check if the length of given text exceeds
  470. * character limit.
  471. *
  472. * @param string $desc the description
  473. *
  474. * @return boolean is the descripition too long?
  475. */
  476. static function descriptionTooLong($desc)
  477. {
  478. $desclimit = self::maxDescription();
  479. return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
  480. }
  481. /**
  482. * save a new people tag, this should be always used
  483. * since it makes uri, homeurl, created and modified
  484. * timestamps and performs checks.
  485. *
  486. * @param array $fields an array with fields and their values
  487. *
  488. * @return mixed Profile_list on success, false on fail
  489. */
  490. static function saveNew(array $fields) {
  491. extract($fields);
  492. $ptag = new Profile_list();
  493. $ptag->query('BEGIN');
  494. if (empty($tagger)) {
  495. // TRANS: Server exception saving new tag without having a tagger specified.
  496. throw new Exception(_('No tagger specified.'));
  497. }
  498. if (empty($tag)) {
  499. // TRANS: Server exception saving new tag without having a tag specified.
  500. throw new Exception(_('No tag specified.'));
  501. }
  502. if (empty($mainpage)) {
  503. $mainpage = null;
  504. }
  505. if (empty($uri)) {
  506. // fill in later...
  507. $uri = null;
  508. }
  509. if (empty($mainpage)) {
  510. $mainpage = null;
  511. }
  512. if (empty($description)) {
  513. $description = null;
  514. }
  515. if (empty($private)) {
  516. $private = false;
  517. }
  518. $ptag->tagger = $tagger;
  519. $ptag->tag = $tag;
  520. $ptag->description = $description;
  521. $ptag->private = $private;
  522. $ptag->uri = $uri;
  523. $ptag->mainpage = $mainpage;
  524. $ptag->created = common_sql_now();
  525. $ptag->modified = common_sql_now();
  526. $result = $ptag->insert();
  527. if (!$result) {
  528. common_log_db_error($ptag, 'INSERT', __FILE__);
  529. // TRANS: Server exception saving new tag.
  530. throw new ServerException(_('Could not create profile tag.'));
  531. }
  532. if (!isset($uri) || empty($uri)) {
  533. $orig = clone($ptag);
  534. $ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger));
  535. $result = $ptag->update($orig);
  536. if (!$result) {
  537. common_log_db_error($ptag, 'UPDATE', __FILE__);
  538. // TRANS: Server exception saving new tag.
  539. throw new ServerException(_('Could not set profile tag URI.'));
  540. }
  541. }
  542. if (!isset($mainpage) || empty($mainpage)) {
  543. $orig = clone($ptag);
  544. $user = User::getKV('id', $ptag->tagger);
  545. if(!empty($user)) {
  546. $ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'nickname' => $user->getNickname()));
  547. } else {
  548. $ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
  549. }
  550. $result = $ptag->update($orig);
  551. if (!$result) {
  552. common_log_db_error($ptag, 'UPDATE', __FILE__);
  553. // TRANS: Server exception saving new tag.
  554. throw new ServerException(_('Could not set profile tag mainpage.'));
  555. }
  556. }
  557. return $ptag;
  558. }
  559. /**
  560. * get all items at given cursor position for api
  561. *
  562. * @param callback $fn a function that takes the following arguments in order:
  563. * $offset, $limit, $since_id, $max_id
  564. * and returns a Profile_list object after making the DB query
  565. * @param array $args arguments required for $fn
  566. * @param integer $cursor the cursor
  567. * @param integer $count max. number of results
  568. *
  569. * Algorithm:
  570. * - if cursor is 0, return empty list
  571. * - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0
  572. * - if cursor is +ve get 22 consecutive items before starting at cursor
  573. * - return items[1..20] if items[0] == cursor else return items[0..21]
  574. * - prev_cursor = items[1]
  575. * - next_cursor = id of the last item being returned
  576. *
  577. * - if cursor is -ve get 22 consecutive items after cursor starting at cursor
  578. * - return items[1..20]
  579. *
  580. * @returns array (array (mixed items), int next_cursor, int previous_cursor)
  581. */
  582. // XXX: This should be in Memcached_DataObject... eventually.
  583. static function getAtCursor($fn, array $args, $cursor, $count=20)
  584. {
  585. $items = array();
  586. $since_id = 0;
  587. $max_id = 0;
  588. $next_cursor = 0;
  589. $prev_cursor = 0;
  590. if($cursor > 0) {
  591. // if cursor is +ve fetch $count+2 items before cursor starting at cursor
  592. $max_id = $cursor;
  593. $fn_args = array_merge($args, array(0, $count+2, 0, $max_id));
  594. $list = call_user_func_array($fn, $fn_args);
  595. while($list->fetch()) {
  596. $items[] = clone($list);
  597. }
  598. if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) ||
  599. $items[0]->id == $cursor) {
  600. array_shift($items);
  601. $prev_cursor = isset($items[0]->cursor) ?
  602. -$items[0]->cursor : -$items[0]->id;
  603. } else {
  604. if (count($items) > $count+1) {
  605. array_shift($items);
  606. }
  607. // this means the cursor item has been deleted, check to see if there are more
  608. $fn_args = array_merge($args, array(0, 1, $cursor));
  609. $more = call_user_func($fn, $fn_args);
  610. if (!$more->fetch() || empty($more)) {
  611. // no more items.
  612. $prev_cursor = 0;
  613. } else {
  614. $prev_cursor = isset($items[0]->cursor) ?
  615. -$items[0]->cursor : -$items[0]->id;
  616. }
  617. }
  618. if (count($items)==$count+1) {
  619. // this means there is a next page.
  620. $next = array_pop($items);
  621. $next_cursor = isset($next->cursor) ?
  622. $items[$count-1]->cursor : $items[$count-1]->id;
  623. }
  624. } else if($cursor < -1) {
  625. // if cursor is -ve fetch $count+2 items created after -$cursor-1
  626. $cursor = abs($cursor);
  627. $since_id = $cursor-1;
  628. $fn_args = array_merge($args, array(0, $count+2, $since_id));
  629. $list = call_user_func_array($fn, $fn_args);
  630. while($list->fetch()) {
  631. $items[] = clone($list);
  632. }
  633. $end = count($items)-1;
  634. if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) ||
  635. $items[$end]->id == $cursor) {
  636. array_pop($items);
  637. $next_cursor = isset($items[$end-1]->cursor) ?
  638. $items[$end-1]->cursor : $items[$end-1]->id;
  639. } else {
  640. $next_cursor = isset($items[$end]->cursor) ?
  641. $items[$end]->cursor : $items[$end]->id;
  642. if ($end > $count) array_pop($items); // excess item.
  643. // check if there are more items for next page
  644. $fn_args = array_merge($args, array(0, 1, 0, $cursor));
  645. $more = call_user_func_array($fn, $fn_args);
  646. if (!$more->fetch() || empty($more)) {
  647. $next_cursor = 0;
  648. }
  649. }
  650. if (count($items) == $count+1) {
  651. // this means there is a previous page.
  652. $prev = array_shift($items);
  653. $prev_cursor = isset($prev->cursor) ?
  654. -$items[0]->cursor : -$items[0]->id;
  655. }
  656. } else if($cursor == -1) {
  657. $fn_args = array_merge($args, array(0, $count+1));
  658. $list = call_user_func_array($fn, $fn_args);
  659. while($list->fetch()) {
  660. $items[] = clone($list);
  661. }
  662. if (count($items)==$count+1) {
  663. $next = array_pop($items);
  664. if(isset($next->cursor)) {
  665. $next_cursor = $items[$count-1]->cursor;
  666. } else {
  667. $next_cursor = $items[$count-1]->id;
  668. }
  669. }
  670. }
  671. return array($items, $next_cursor, $prev_cursor);
  672. }
  673. /**
  674. * save a collection of people tags into the cache
  675. *
  676. * @param string $ckey cache key
  677. * @param Profile_list &$tag the results to store
  678. * @param integer $offset offset for slicing results
  679. * @param integer $limit maximum number of results
  680. *
  681. * @return boolean success
  682. */
  683. static function setCache($ckey, &$tag, $offset=0, $limit=null) {
  684. $cache = Cache::instance();
  685. if (empty($cache)) {
  686. return false;
  687. }
  688. $str = '';
  689. $tags = array();
  690. while ($tag->fetch()) {
  691. $str .= $tag->tagger . ':' . $tag->tag . ';';
  692. $tags[] = clone($tag);
  693. }
  694. $str = substr($str, 0, -1);
  695. if ($offset>=0 && !is_null($limit)) {
  696. $tags = array_slice($tags, $offset, $limit);
  697. }
  698. $tag = new ArrayWrapper($tags);
  699. return self::cacheSet($ckey, $str);
  700. }
  701. /**
  702. * get people tags from the cache
  703. *
  704. * @param string $ckey cache key
  705. * @param integer $offset offset for slicing
  706. * @param integer $limit limit
  707. *
  708. * @return Profile_list results
  709. */
  710. static function getCached($ckey, $offset=0, $limit=null) {
  711. $keys_str = self::cacheGet($ckey);
  712. if ($keys_str === false) {
  713. return false;
  714. }
  715. $pairs = explode(';', $keys_str);
  716. $keys = array();
  717. foreach ($pairs as $pair) {
  718. $keys[] = explode(':', $pair);
  719. }
  720. if ($offset>=0 && !is_null($limit)) {
  721. $keys = array_slice($keys, $offset, $limit);
  722. }
  723. return self::getByKeys($keys);
  724. }
  725. /**
  726. * get Profile_list objects from the database
  727. * given their (tag, tagger) key pairs.
  728. *
  729. * @param array $keys array of array(tagger, tag)
  730. *
  731. * @return Profile_list results
  732. */
  733. static function getByKeys(array $keys) {
  734. $cache = Cache::instance();
  735. if (!empty($cache)) {
  736. $tags = array();
  737. foreach ($keys as $key) {
  738. $t = Profile_list::getByTaggerAndTag($key[0], $key[1]);
  739. if (!empty($t)) {
  740. $tags[] = $t;
  741. }
  742. }
  743. return new ArrayWrapper($tags);
  744. } else {
  745. $tag = new Profile_list();
  746. if (empty($keys)) {
  747. //if no IDs requested, just return the tag object
  748. return $tag;
  749. }
  750. $pairs = array();
  751. foreach ($keys as $key) {
  752. $pairs[] = '(' . $key[0] . ', "' . $key[1] . '")';
  753. }
  754. $tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')');
  755. $tag->find();
  756. $temp = array();
  757. while ($tag->fetch()) {
  758. $temp[$tag->tagger.'-'.$tag->tag] = clone($tag);
  759. }
  760. $wrapped = array();
  761. foreach ($keys as $key) {
  762. $id = $key[0].'-'.$key[1];
  763. if (array_key_exists($id, $temp)) {
  764. $wrapped[] = $temp[$id];
  765. }
  766. }
  767. return new ArrayWrapper($wrapped);
  768. }
  769. }
  770. function insert()
  771. {
  772. $result = parent::insert();
  773. if ($result) {
  774. self::blow('profile:lists:%d', $this->tagger);
  775. }
  776. return $result;
  777. }
  778. }