Profile_list.php 30 KB

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