Profile_list.php 29 KB

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