PageRenderTime 60ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/classes/Notice.php

https://gitlab.com/windigo-gs/windigos-gnu-social
PHP | 2694 lines | 1710 code | 487 blank | 497 comment | 299 complexity | c4af22a0db7aebf251d569943d0982b9 MD5 | raw file
Possible License(s): AGPL-3.0, BSD-3-Clause, GPL-2.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2008-2011 StatusNet, Inc.
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. *
  19. * @category Notices
  20. * @package StatusNet
  21. * @author Brenda Wallace <shiny@cpan.org>
  22. * @author Christopher Vollick <psycotica0@gmail.com>
  23. * @author CiaranG <ciaran@ciarang.com>
  24. * @author Craig Andrews <candrews@integralblue.com>
  25. * @author Evan Prodromou <evan@controlezvous.ca>
  26. * @author Gina Haeussge <osd@foosel.net>
  27. * @author Jeffery To <jeffery.to@gmail.com>
  28. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  29. * @author Robin Millette <millette@controlyourself.ca>
  30. * @author Sarven Capadisli <csarven@controlyourself.ca>
  31. * @author Tom Adams <tom@holizz.com>
  32. * @author Mikael Nordfeldth <mmn@hethane.se>
  33. * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
  34. * @license GNU Affero General Public License http://www.gnu.org/licenses/
  35. */
  36. if (!defined('GNUSOCIAL')) { exit(1); }
  37. /**
  38. * Table Definition for notice
  39. */
  40. /* We keep 200 notices, the max number of notices available per API request,
  41. * in the memcached cache. */
  42. define('NOTICE_CACHE_WINDOW', CachingNoticeStream::CACHE_WINDOW);
  43. define('MAX_BOXCARS', 128);
  44. class Notice extends Managed_DataObject
  45. {
  46. ###START_AUTOCODE
  47. /* the code below is auto generated do not remove the above tag */
  48. public $__table = 'notice'; // table name
  49. public $id; // int(4) primary_key not_null
  50. public $profile_id; // int(4) multiple_key not_null
  51. public $uri; // varchar(255) unique_key
  52. public $content; // text
  53. public $rendered; // text
  54. public $url; // varchar(255)
  55. public $created; // datetime multiple_key not_null default_0000-00-00%2000%3A00%3A00
  56. public $modified; // timestamp not_null default_CURRENT_TIMESTAMP
  57. public $reply_to; // int(4)
  58. public $is_local; // int(4)
  59. public $source; // varchar(32)
  60. public $conversation; // int(4)
  61. public $lat; // decimal(10,7)
  62. public $lon; // decimal(10,7)
  63. public $location_id; // int(4)
  64. public $location_ns; // int(4)
  65. public $repeat_of; // int(4)
  66. public $verb; // varchar(255)
  67. public $object_type; // varchar(255)
  68. public $scope; // int(4)
  69. /* the code above is auto generated do not remove the tag below */
  70. ###END_AUTOCODE
  71. public static function schemaDef()
  72. {
  73. $def = array(
  74. 'fields' => array(
  75. 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
  76. 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'who made the update'),
  77. 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'),
  78. 'content' => array('type' => 'text', 'description' => 'update content', 'collate' => 'utf8_general_ci'),
  79. 'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'),
  80. 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'),
  81. 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
  82. 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
  83. 'reply_to' => array('type' => 'int', 'description' => 'notice replied to (usually a guess)'),
  84. 'is_local' => array('type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => 'notice was generated by a user'),
  85. 'source' => array('type' => 'varchar', 'length' => 32, 'description' => 'source of comment, like "web", "im", or "clientname"'),
  86. 'conversation' => array('type' => 'int', 'description' => 'id of root notice in this conversation'),
  87. 'lat' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'),
  88. 'lon' => array('type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'),
  89. 'location_id' => array('type' => 'int', 'description' => 'location id if possible'),
  90. 'location_ns' => array('type' => 'int', 'description' => 'namespace for location'),
  91. 'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'),
  92. 'object_type' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'),
  93. 'verb' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'),
  94. 'scope' => array('type' => 'int',
  95. 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers; null = default'),
  96. ),
  97. 'primary key' => array('id'),
  98. 'unique keys' => array(
  99. 'notice_uri_key' => array('uri'),
  100. ),
  101. 'foreign keys' => array(
  102. 'notice_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
  103. 'notice_reply_to_fkey' => array('notice', array('reply_to' => 'id')),
  104. 'notice_conversation_fkey' => array('conversation', array('conversation' => 'id')), # note... used to refer to notice.id
  105. 'notice_repeat_of_fkey' => array('notice', array('repeat_of' => 'id')), # @fixme: what about repeats of deleted notices?
  106. ),
  107. 'indexes' => array(
  108. 'notice_created_id_is_local_idx' => array('created', 'id', 'is_local'),
  109. 'notice_profile_id_idx' => array('profile_id', 'created', 'id'),
  110. 'notice_repeat_of_created_id_idx' => array('repeat_of', 'created', 'id'),
  111. 'notice_conversation_created_id_idx' => array('conversation', 'created', 'id'),
  112. 'notice_replyto_idx' => array('reply_to')
  113. )
  114. );
  115. if (common_config('search', 'type') == 'fulltext') {
  116. $def['fulltext indexes'] = array('content' => array('content'));
  117. }
  118. return $def;
  119. }
  120. /* Notice types */
  121. const LOCAL_PUBLIC = 1;
  122. const REMOTE = 0;
  123. const LOCAL_NONPUBLIC = -1;
  124. const GATEWAY = -2;
  125. const PUBLIC_SCOPE = 0; // Useful fake constant
  126. const SITE_SCOPE = 1;
  127. const ADDRESSEE_SCOPE = 2;
  128. const GROUP_SCOPE = 4;
  129. const FOLLOWER_SCOPE = 8;
  130. protected $_profile = -1;
  131. public function getProfile()
  132. {
  133. if ($this->_profile === -1) {
  134. $this->_setProfile(Profile::getKV('id', $this->profile_id));
  135. }
  136. return $this->_profile;
  137. }
  138. public function _setProfile(Profile $profile=null)
  139. {
  140. if (!$profile instanceof Profile) {
  141. throw new NoProfileException($this->profile_id);
  142. }
  143. $this->_profile = $profile;
  144. }
  145. function delete($useWhere=false)
  146. {
  147. // For auditing purposes, save a record that the notice
  148. // was deleted.
  149. // @fixme we have some cases where things get re-run and so the
  150. // insert fails.
  151. $deleted = Deleted_notice::getKV('id', $this->id);
  152. if (!$deleted instanceof Deleted_notice) {
  153. $deleted = Deleted_notice::getKV('uri', $this->uri);
  154. }
  155. if (!$deleted instanceof Deleted_notice) {
  156. $deleted = new Deleted_notice();
  157. $deleted->id = $this->id;
  158. $deleted->profile_id = $this->profile_id;
  159. $deleted->uri = $this->uri;
  160. $deleted->created = $this->created;
  161. $deleted->deleted = common_sql_now();
  162. $deleted->insert();
  163. }
  164. if (Event::handle('NoticeDeleteRelated', array($this))) {
  165. // Clear related records
  166. $this->clearReplies();
  167. $this->clearRepeats();
  168. $this->clearFaves();
  169. $this->clearTags();
  170. $this->clearGroupInboxes();
  171. $this->clearFiles();
  172. $this->clearAttentions();
  173. // NOTE: we don't clear queue items
  174. }
  175. $result = parent::delete($useWhere);
  176. $this->blowOnDelete();
  177. return $result;
  178. }
  179. public function getUri()
  180. {
  181. return $this->uri;
  182. }
  183. /*
  184. * @param $root boolean If true, link to just the conversation root.
  185. *
  186. * @return URL to conversation
  187. */
  188. public function getConversationUrl($anchor=true)
  189. {
  190. return Conversation::getUrlFromNotice($this, $anchor);
  191. }
  192. /*
  193. * Get the local representation URL of this notice.
  194. */
  195. public function getLocalUrl()
  196. {
  197. return common_local_url('shownotice', array('notice' => $this->id), null, null, false);
  198. }
  199. /*
  200. * Get the original representation URL of this notice.
  201. */
  202. public function getUrl()
  203. {
  204. // The risk is we start having empty urls and non-http uris...
  205. // and we can't really handle any other protocol right now.
  206. switch (true) {
  207. case common_valid_http_url($this->url): // should we allow non-http/https URLs?
  208. return $this->url;
  209. case $this->isLocal():
  210. // let's generate a valid link to our locally available notice on demand
  211. return common_local_url('shownotice', array('notice' => $this->id), null, null, false);
  212. case common_valid_http_url($this->uri):
  213. return $this->uri;
  214. default:
  215. common_debug('No URL available for notice: id='.$this->id);
  216. throw new InvalidUrlException($this->url);
  217. }
  218. }
  219. public function get_object_type($canonical=false) {
  220. return $canonical
  221. ? ActivityObject::canonicalType($this->object_type)
  222. : $this->object_type;
  223. }
  224. public static function getByUri($uri)
  225. {
  226. $notice = new Notice();
  227. $notice->uri = $uri;
  228. if (!$notice->find(true)) {
  229. throw new NoResultException($notice);
  230. }
  231. return $notice;
  232. }
  233. /**
  234. * Extract #hashtags from this notice's content and save them to the database.
  235. */
  236. function saveTags()
  237. {
  238. /* extract all #hastags */
  239. $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/u', strtolower($this->content), $match);
  240. if (!$count) {
  241. return true;
  242. }
  243. /* Add them to the database */
  244. return $this->saveKnownTags($match[1]);
  245. }
  246. /**
  247. * Record the given set of hash tags in the db for this notice.
  248. * Given tag strings will be normalized and checked for dupes.
  249. */
  250. function saveKnownTags($hashtags)
  251. {
  252. //turn each into their canonical tag
  253. //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag
  254. for($i=0; $i<count($hashtags); $i++) {
  255. /* elide characters we don't want in the tag */
  256. $hashtags[$i] = common_canonical_tag($hashtags[$i]);
  257. }
  258. foreach(array_unique($hashtags) as $hashtag) {
  259. $this->saveTag($hashtag);
  260. self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $hashtag);
  261. }
  262. return true;
  263. }
  264. /**
  265. * Record a single hash tag as associated with this notice.
  266. * Tag format and uniqueness must be validated by caller.
  267. */
  268. function saveTag($hashtag)
  269. {
  270. $tag = new Notice_tag();
  271. $tag->notice_id = $this->id;
  272. $tag->tag = $hashtag;
  273. $tag->created = $this->created;
  274. $id = $tag->insert();
  275. if (!$id) {
  276. // TRANS: Server exception. %s are the error details.
  277. throw new ServerException(sprintf(_('Database error inserting hashtag: %s.'),
  278. $last_error->message));
  279. return;
  280. }
  281. // if it's saved, blow its cache
  282. $tag->blowCache(false);
  283. }
  284. /**
  285. * Save a new notice and push it out to subscribers' inboxes.
  286. * Poster's permissions are checked before sending.
  287. *
  288. * @param int $profile_id Profile ID of the poster
  289. * @param string $content source message text; links may be shortened
  290. * per current user's preference
  291. * @param string $source source key ('web', 'api', etc)
  292. * @param array $options Associative array of optional properties:
  293. * string 'created' timestamp of notice; defaults to now
  294. * int 'is_local' source/gateway ID, one of:
  295. * Notice::LOCAL_PUBLIC - Local, ok to appear in public timeline
  296. * Notice::REMOTE - Sent from a remote service;
  297. * hide from public timeline but show in
  298. * local "and friends" timelines
  299. * Notice::LOCAL_NONPUBLIC - Local, but hide from public timeline
  300. * Notice::GATEWAY - From another non-OStatus service;
  301. * will not appear in public views
  302. * float 'lat' decimal latitude for geolocation
  303. * float 'lon' decimal longitude for geolocation
  304. * int 'location_id' geoname identifier
  305. * int 'location_ns' geoname namespace to interpret location_id
  306. * int 'reply_to'; notice ID this is a reply to
  307. * int 'repeat_of'; notice ID this is a repeat of
  308. * string 'uri' unique ID for notice; a unique tag uri (can be url or anything too)
  309. * string 'url' permalink to notice; defaults to local notice URL
  310. * string 'rendered' rendered HTML version of content
  311. * array 'replies' list of profile URIs for reply delivery in
  312. * place of extracting @-replies from content.
  313. * array 'groups' list of group IDs to deliver to, in place of
  314. * extracting ! tags from content
  315. * array 'tags' list of hashtag strings to save with the notice
  316. * in place of extracting # tags from content
  317. * array 'urls' list of attached/referred URLs to save with the
  318. * notice in place of extracting links from content
  319. * boolean 'distribute' whether to distribute the notice, default true
  320. * string 'object_type' URL of the associated object type (default ActivityObject::NOTE)
  321. * string 'verb' URL of the associated verb (default ActivityVerb::POST)
  322. * int 'scope' Scope bitmask; default to SITE_SCOPE on private sites, 0 otherwise
  323. *
  324. * @fixme tag override
  325. *
  326. * @return Notice
  327. * @throws ClientException
  328. */
  329. static function saveNew($profile_id, $content, $source, array $options=null) {
  330. $defaults = array('uri' => null,
  331. 'url' => null,
  332. 'reply_to' => null,
  333. 'repeat_of' => null,
  334. 'scope' => null,
  335. 'distribute' => true,
  336. 'object_type' => null,
  337. 'verb' => null);
  338. if (!empty($options) && is_array($options)) {
  339. $options = array_merge($defaults, $options);
  340. extract($options);
  341. } else {
  342. extract($defaults);
  343. }
  344. if (!isset($is_local)) {
  345. $is_local = Notice::LOCAL_PUBLIC;
  346. }
  347. $profile = Profile::getKV('id', $profile_id);
  348. if (!$profile instanceof Profile) {
  349. // TRANS: Client exception thrown when trying to save a notice for an unknown user.
  350. throw new ClientException(_('Problem saving notice. Unknown user.'));
  351. }
  352. $user = User::getKV('id', $profile_id);
  353. if ($user instanceof User) {
  354. // Use the local user's shortening preferences, if applicable.
  355. $final = $user->shortenLinks($content);
  356. } else {
  357. $final = common_shorten_links($content);
  358. }
  359. if (Notice::contentTooLong($final)) {
  360. // TRANS: Client exception thrown if a notice contains too many characters.
  361. throw new ClientException(_('Problem saving notice. Too long.'));
  362. }
  363. if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) {
  364. common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.');
  365. // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame.
  366. throw new ClientException(_('Too many notices too fast; take a breather '.
  367. 'and post again in a few minutes.'));
  368. }
  369. if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
  370. common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
  371. // TRANS: Client exception thrown when a user tries to post too many duplicate notices in a given time frame.
  372. throw new ClientException(_('Too many duplicate messages too quickly;'.
  373. ' take a breather and post again in a few minutes.'));
  374. }
  375. if (!$profile->hasRight(Right::NEWNOTICE)) {
  376. common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $profile->nickname);
  377. // TRANS: Client exception thrown when a user tries to post while being banned.
  378. throw new ClientException(_('You are banned from posting notices on this site.'), 403);
  379. }
  380. $notice = new Notice();
  381. $notice->profile_id = $profile_id;
  382. $autosource = common_config('public', 'autosource');
  383. // Sandboxed are non-false, but not 1, either
  384. if (!$profile->hasRight(Right::PUBLICNOTICE) ||
  385. ($source && $autosource && in_array($source, $autosource))) {
  386. $notice->is_local = Notice::LOCAL_NONPUBLIC;
  387. } else {
  388. $notice->is_local = $is_local;
  389. }
  390. if (!empty($created)) {
  391. $notice->created = $created;
  392. } else {
  393. $notice->created = common_sql_now();
  394. }
  395. if (!$notice->isLocal()) {
  396. // Only do these checks for non-local notices. Local notices will generate these values later.
  397. if (!common_valid_http_url($url)) {
  398. common_debug('Bad notice URL: ['.$url.'], URI: ['.$uri.']. Cannot link back to original! This is normal for shared notices etc.');
  399. }
  400. if (empty($uri)) {
  401. throw new ServerException('No URI for remote notice. Cannot accept that.');
  402. }
  403. }
  404. $notice->content = $final;
  405. $notice->source = $source;
  406. $notice->uri = $uri;
  407. $notice->url = $url;
  408. // Get the groups here so we can figure out replies and such
  409. if (!isset($groups)) {
  410. $groups = User_group::idsFromText($notice->content, $profile);
  411. }
  412. $reply = null;
  413. // Handle repeat case
  414. if (isset($repeat_of)) {
  415. // Check for a private one
  416. $repeat = Notice::getKV('id', $repeat_of);
  417. if (!($repeat instanceof Notice)) {
  418. // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice.
  419. throw new ClientException(_('Cannot repeat; original notice is missing or deleted.'));
  420. }
  421. if ($profile->id == $repeat->profile_id) {
  422. // TRANS: Client error displayed when trying to repeat an own notice.
  423. throw new ClientException(_('You cannot repeat your own notice.'));
  424. }
  425. if ($repeat->scope != Notice::SITE_SCOPE &&
  426. $repeat->scope != Notice::PUBLIC_SCOPE) {
  427. // TRANS: Client error displayed when trying to repeat a non-public notice.
  428. throw new ClientException(_('Cannot repeat a private notice.'), 403);
  429. }
  430. if (!$repeat->inScope($profile)) {
  431. // The generic checks above should cover this, but let's be sure!
  432. // TRANS: Client error displayed when trying to repeat a notice you cannot access.
  433. throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403);
  434. }
  435. if ($profile->hasRepeated($repeat)) {
  436. // TRANS: Client error displayed when trying to repeat an already repeated notice.
  437. throw new ClientException(_('You already repeated that notice.'));
  438. }
  439. $notice->repeat_of = $repeat_of;
  440. } else {
  441. $reply = self::getReplyTo($reply_to, $profile_id, $source, $final);
  442. if (!empty($reply)) {
  443. if (!$reply->inScope($profile)) {
  444. // TRANS: Client error displayed when trying to reply to a notice a the target has no access to.
  445. // TRANS: %1$s is a user nickname, %2$d is a notice ID (number).
  446. throw new ClientException(sprintf(_('%1$s has no access to notice %2$d.'),
  447. $profile->nickname, $reply->id), 403);
  448. }
  449. $notice->reply_to = $reply->id;
  450. $notice->conversation = $reply->conversation;
  451. // If the original is private to a group, and notice has no group specified,
  452. // make it to the same group(s)
  453. if (empty($groups) && ($reply->scope & Notice::GROUP_SCOPE)) {
  454. $groups = array();
  455. $replyGroups = $reply->getGroups();
  456. foreach ($replyGroups as $group) {
  457. if ($profile->isMember($group)) {
  458. $groups[] = $group->id;
  459. }
  460. }
  461. }
  462. // Scope set below
  463. }
  464. }
  465. if (!empty($lat) && !empty($lon)) {
  466. $notice->lat = $lat;
  467. $notice->lon = $lon;
  468. }
  469. if (!empty($location_ns) && !empty($location_id)) {
  470. $notice->location_id = $location_id;
  471. $notice->location_ns = $location_ns;
  472. }
  473. if (!empty($rendered)) {
  474. $notice->rendered = $rendered;
  475. } else {
  476. $notice->rendered = common_render_content($final, $notice);
  477. }
  478. if (empty($verb)) {
  479. if (!empty($notice->repeat_of)) {
  480. $notice->verb = ActivityVerb::SHARE;
  481. $notice->object_type = ActivityObject::ACTIVITY;
  482. } else {
  483. $notice->verb = ActivityVerb::POST;
  484. }
  485. } else {
  486. $notice->verb = $verb;
  487. }
  488. if (empty($object_type)) {
  489. $notice->object_type = (empty($notice->reply_to)) ? ActivityObject::NOTE : ActivityObject::COMMENT;
  490. } else {
  491. $notice->object_type = $object_type;
  492. }
  493. if (is_null($scope)) { // 0 is a valid value
  494. if (!empty($reply)) {
  495. $notice->scope = $reply->scope;
  496. } else {
  497. $notice->scope = self::defaultScope();
  498. }
  499. } else {
  500. $notice->scope = $scope;
  501. }
  502. // For private streams
  503. try {
  504. $user = $profile->getUser();
  505. if ($user->private_stream &&
  506. ($notice->scope == Notice::PUBLIC_SCOPE ||
  507. $notice->scope == Notice::SITE_SCOPE)) {
  508. $notice->scope |= Notice::FOLLOWER_SCOPE;
  509. }
  510. } catch (NoSuchUserException $e) {
  511. // Cannot handle private streams for remote profiles
  512. }
  513. // Force the scope for private groups
  514. foreach ($groups as $groupId) {
  515. $group = User_group::getKV('id', $groupId);
  516. if ($group instanceof User_group) {
  517. if ($group->force_scope) {
  518. $notice->scope |= Notice::GROUP_SCOPE;
  519. break;
  520. }
  521. }
  522. }
  523. if (Event::handle('StartNoticeSave', array(&$notice))) {
  524. // XXX: some of these functions write to the DB
  525. $id = $notice->insert();
  526. if (!$id) {
  527. common_log_db_error($notice, 'INSERT', __FILE__);
  528. // TRANS: Server exception thrown when a notice cannot be saved.
  529. throw new ServerException(_('Problem saving notice.'));
  530. }
  531. // Update ID-dependent columns: URI, conversation
  532. $orig = clone($notice);
  533. $changed = false;
  534. // We can only get here if it's a local notice, since remote notices
  535. // should've bailed out earlier due to lacking a URI.
  536. if (empty($notice->uri)) {
  537. $notice->uri = sprintf('%s%s=%d:%s=%s',
  538. TagURI::mint(),
  539. 'noticeId', $notice->id,
  540. 'objectType', $notice->get_object_type(true));
  541. $changed = true;
  542. }
  543. // If it's not part of a conversation, it's
  544. // the beginning of a new conversation.
  545. if (empty($notice->conversation)) {
  546. $conv = Conversation::create($notice);
  547. $notice->conversation = $conv->id;
  548. $changed = true;
  549. }
  550. if ($changed) {
  551. if ($notice->update($orig) === false) {
  552. common_log_db_error($notice, 'UPDATE', __FILE__);
  553. // TRANS: Server exception thrown when a notice cannot be updated.
  554. throw new ServerException(_('Problem saving notice.'));
  555. }
  556. }
  557. }
  558. // Clear the cache for subscribed users, so they'll update at next request
  559. // XXX: someone clever could prepend instead of clearing the cache
  560. $notice->blowOnInsert();
  561. // Save per-notice metadata...
  562. if (isset($replies)) {
  563. $notice->saveKnownReplies($replies);
  564. } else {
  565. $notice->saveReplies();
  566. }
  567. if (isset($tags)) {
  568. $notice->saveKnownTags($tags);
  569. } else {
  570. $notice->saveTags();
  571. }
  572. // Note: groups may save tags, so must be run after tags are saved
  573. // to avoid errors on duplicates.
  574. // Note: groups should always be set.
  575. $notice->saveKnownGroups($groups);
  576. if (isset($urls)) {
  577. $notice->saveKnownUrls($urls);
  578. } else {
  579. $notice->saveUrls();
  580. }
  581. if ($distribute) {
  582. // Prepare inbox delivery, may be queued to background.
  583. $notice->distribute();
  584. }
  585. return $notice;
  586. }
  587. function blowOnInsert($conversation = false)
  588. {
  589. $this->blowStream('profile:notice_ids:%d', $this->profile_id);
  590. if ($this->isPublic()) {
  591. $this->blowStream('public');
  592. }
  593. self::blow('notice:list-ids:conversation:%s', $this->conversation);
  594. self::blow('conversation:notice_count:%d', $this->conversation);
  595. if (!empty($this->repeat_of)) {
  596. // XXX: we should probably only use one of these
  597. $this->blowStream('notice:repeats:%d', $this->repeat_of);
  598. self::blow('notice:list-ids:repeat_of:%d', $this->repeat_of);
  599. }
  600. $original = Notice::getKV('id', $this->repeat_of);
  601. if ($original instanceof Notice) {
  602. $originalUser = User::getKV('id', $original->profile_id);
  603. if ($originalUser instanceof User) {
  604. $this->blowStream('user:repeats_of_me:%d', $originalUser->id);
  605. }
  606. }
  607. $profile = Profile::getKV($this->profile_id);
  608. if ($profile instanceof Profile) {
  609. $profile->blowNoticeCount();
  610. }
  611. $ptags = $this->getProfileTags();
  612. foreach ($ptags as $ptag) {
  613. $ptag->blowNoticeStreamCache();
  614. }
  615. }
  616. /**
  617. * Clear cache entries related to this notice at delete time.
  618. * Necessary to avoid breaking paging on public, profile timelines.
  619. */
  620. function blowOnDelete()
  621. {
  622. $this->blowOnInsert();
  623. self::blow('profile:notice_ids:%d;last', $this->profile_id);
  624. if ($this->isPublic()) {
  625. self::blow('public;last');
  626. }
  627. self::blow('fave:by_notice', $this->id);
  628. if ($this->conversation) {
  629. // In case we're the first, will need to calc a new root.
  630. self::blow('notice:conversation_root:%d', $this->conversation);
  631. }
  632. $ptags = $this->getProfileTags();
  633. foreach ($ptags as $ptag) {
  634. $ptag->blowNoticeStreamCache(true);
  635. }
  636. }
  637. function blowStream()
  638. {
  639. $c = self::memcache();
  640. if (empty($c)) {
  641. return false;
  642. }
  643. $args = func_get_args();
  644. $format = array_shift($args);
  645. $keyPart = vsprintf($format, $args);
  646. $cacheKey = Cache::key($keyPart);
  647. $c->delete($cacheKey);
  648. // delete the "last" stream, too, if this notice is
  649. // older than the top of that stream
  650. $lastKey = $cacheKey.';last';
  651. $lastStr = $c->get($lastKey);
  652. if ($lastStr !== false) {
  653. $window = explode(',', $lastStr);
  654. $lastID = $window[0];
  655. $lastNotice = Notice::getKV('id', $lastID);
  656. if (!$lastNotice instanceof Notice // just weird
  657. || strtotime($lastNotice->created) >= strtotime($this->created)) {
  658. $c->delete($lastKey);
  659. }
  660. }
  661. }
  662. /** save all urls in the notice to the db
  663. *
  664. * follow redirects and save all available file information
  665. * (mimetype, date, size, oembed, etc.)
  666. *
  667. * @return void
  668. */
  669. function saveUrls() {
  670. if (common_config('attachments', 'process_links')) {
  671. common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id);
  672. }
  673. }
  674. /**
  675. * Save the given URLs as related links/attachments to the db
  676. *
  677. * follow redirects and save all available file information
  678. * (mimetype, date, size, oembed, etc.)
  679. *
  680. * @return void
  681. */
  682. function saveKnownUrls($urls)
  683. {
  684. if (common_config('attachments', 'process_links')) {
  685. // @fixme validation?
  686. foreach (array_unique($urls) as $url) {
  687. File::processNew($url, $this->id);
  688. }
  689. }
  690. }
  691. /**
  692. * @private callback
  693. */
  694. function saveUrl($url, $notice_id) {
  695. File::processNew($url, $notice_id);
  696. }
  697. static function checkDupes($profile_id, $content) {
  698. $profile = Profile::getKV($profile_id);
  699. if (!$profile instanceof Profile) {
  700. return false;
  701. }
  702. $notice = $profile->getNotices(0, CachingNoticeStream::CACHE_WINDOW);
  703. if (!empty($notice)) {
  704. $last = 0;
  705. while ($notice->fetch()) {
  706. if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
  707. return true;
  708. } else if ($notice->content == $content) {
  709. return false;
  710. }
  711. }
  712. }
  713. // If we get here, oldest item in cache window is not
  714. // old enough for dupe limit; do direct check against DB
  715. $notice = new Notice();
  716. $notice->profile_id = $profile_id;
  717. $notice->content = $content;
  718. $threshold = common_sql_date(time() - common_config('site', 'dupelimit'));
  719. $notice->whereAdd(sprintf("created > '%s'", $notice->escape($threshold)));
  720. $cnt = $notice->count();
  721. return ($cnt == 0);
  722. }
  723. static function checkEditThrottle($profile_id) {
  724. $profile = Profile::getKV($profile_id);
  725. if (!$profile instanceof Profile) {
  726. return false;
  727. }
  728. // Get the Nth notice
  729. $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1);
  730. if ($notice && $notice->fetch()) {
  731. // If the Nth notice was posted less than timespan seconds ago
  732. if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) {
  733. // Then we throttle
  734. return false;
  735. }
  736. }
  737. // Either not N notices in the stream, OR the Nth was not posted within timespan seconds
  738. return true;
  739. }
  740. protected $_attachments = -1;
  741. function attachments() {
  742. if ($this->_attachments != -1) {
  743. return $this->_attachments;
  744. }
  745. $f2ps = File_to_post::listGet('post_id', array($this->id));
  746. $ids = array();
  747. foreach ($f2ps[$this->id] as $f2p) {
  748. $ids[] = $f2p->file_id;
  749. }
  750. $files = File::multiGet('id', $ids);
  751. $this->_attachments = $files->fetchAll();
  752. return $this->_attachments;
  753. }
  754. function _setAttachments($attachments)
  755. {
  756. $this->_attachments = $attachments;
  757. }
  758. function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0)
  759. {
  760. $stream = new PublicNoticeStream();
  761. return $stream->getNotices($offset, $limit, $since_id, $max_id);
  762. }
  763. function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
  764. {
  765. $stream = new ConversationNoticeStream($id);
  766. return $stream->getNotices($offset, $limit, $since_id, $max_id);
  767. }
  768. /**
  769. * Is this notice part of an active conversation?
  770. *
  771. * @return boolean true if other messages exist in the same
  772. * conversation, false if this is the only one
  773. */
  774. function hasConversation()
  775. {
  776. if (!empty($this->conversation)) {
  777. $conversation = Notice::conversationStream(
  778. $this->conversation,
  779. 1,
  780. 1
  781. );
  782. if ($conversation->N > 0) {
  783. return true;
  784. }
  785. }
  786. return false;
  787. }
  788. /**
  789. * Grab the earliest notice from this conversation.
  790. *
  791. * @return Notice or null
  792. */
  793. function conversationRoot($profile=-1)
  794. {
  795. // XXX: can this happen?
  796. if (empty($this->conversation)) {
  797. return null;
  798. }
  799. // Get the current profile if not specified
  800. if (is_int($profile) && $profile == -1) {
  801. $profile = Profile::current();
  802. }
  803. // If this notice is out of scope, no root for you!
  804. if (!$this->inScope($profile)) {
  805. return null;
  806. }
  807. // If this isn't a reply to anything, then it's its own
  808. // root.
  809. if (empty($this->reply_to)) {
  810. return $this;
  811. }
  812. if (is_null($profile)) {
  813. $keypart = sprintf('notice:conversation_root:%d:null', $this->id);
  814. } else {
  815. $keypart = sprintf('notice:conversation_root:%d:%d',
  816. $this->id,
  817. $profile->id);
  818. }
  819. $root = self::cacheGet($keypart);
  820. if ($root !== false && $root->inScope($profile)) {
  821. return $root;
  822. }
  823. $last = $this;
  824. while (true) {
  825. try {
  826. $parent = $last->getParent();
  827. if ($parent->inScope($profile)) {
  828. $last = $parent;
  829. continue;
  830. }
  831. } catch (Exception $e) {
  832. // Latest notice has no parent
  833. }
  834. // No parent, or parent out of scope
  835. $root = $last;
  836. break;
  837. }
  838. self::cacheSet($keypart, $root);
  839. return $root;
  840. }
  841. /**
  842. * Pull up a full list of local recipients who will be getting
  843. * this notice in their inbox. Results will be cached, so don't
  844. * change the input data wily-nilly!
  845. *
  846. * @param array $groups optional list of Group objects;
  847. * if left empty, will be loaded from group_inbox records
  848. * @param array $recipient optional list of reply profile ids
  849. * if left empty, will be loaded from reply records
  850. * @return array associating recipient user IDs with an inbox source constant
  851. */
  852. function whoGets(array $groups=null, array $recipients=null)
  853. {
  854. $c = self::memcache();
  855. if (!empty($c)) {
  856. $ni = $c->get(Cache::key('notice:who_gets:'.$this->id));
  857. if ($ni !== false) {
  858. return $ni;
  859. }
  860. }
  861. if (is_null($recipients)) {
  862. $recipients = $this->getReplies();
  863. }
  864. $ni = array();
  865. // Give plugins a chance to add folks in at start...
  866. if (Event::handle('StartNoticeWhoGets', array($this, &$ni))) {
  867. $users = $this->getSubscribedUsers();
  868. foreach ($users as $id) {
  869. $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
  870. }
  871. if (is_null($groups)) {
  872. $groups = $this->getGroups();
  873. }
  874. foreach ($groups as $group) {
  875. $users = $group->getUserMembers();
  876. foreach ($users as $id) {
  877. if (!array_key_exists($id, $ni)) {
  878. $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
  879. }
  880. }
  881. }
  882. $ptAtts = $this->getAttentionsFromProfileTags();
  883. foreach ($ptAtts as $key=>$val) {
  884. if (!array_key_exists($key, $ni)) {
  885. $ni[$key] = $val;
  886. }
  887. }
  888. foreach ($recipients as $recipient) {
  889. if (!array_key_exists($recipient, $ni)) {
  890. $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
  891. }
  892. }
  893. // Exclude any deleted, non-local, or blocking recipients.
  894. $profile = $this->getProfile();
  895. $originalProfile = null;
  896. if ($this->repeat_of) {
  897. // Check blocks against the original notice's poster as well.
  898. $original = Notice::getKV('id', $this->repeat_of);
  899. if ($original instanceof Notice) {
  900. $originalProfile = $original->getProfile();
  901. }
  902. }
  903. foreach ($ni as $id => $source) {
  904. try {
  905. $user = User::getKV('id', $id);
  906. if (!$user instanceof User ||
  907. $user->hasBlocked($profile) ||
  908. ($originalProfile && $user->hasBlocked($originalProfile))) {
  909. unset($ni[$id]);
  910. }
  911. } catch (UserNoProfileException $e) {
  912. // User doesn't have a profile; invalid; skip them.
  913. unset($ni[$id]);
  914. }
  915. }
  916. // Give plugins a chance to filter out...
  917. Event::handle('EndNoticeWhoGets', array($this, &$ni));
  918. }
  919. if (!empty($c)) {
  920. // XXX: pack this data better
  921. $c->set(Cache::key('notice:who_gets:'.$this->id), $ni);
  922. }
  923. return $ni;
  924. }
  925. function getSubscribedUsers()
  926. {
  927. $user = new User();
  928. if(common_config('db','quote_identifiers'))
  929. $user_table = '"user"';
  930. else $user_table = 'user';
  931. $qry =
  932. 'SELECT id ' .
  933. 'FROM '. $user_table .' JOIN subscription '.
  934. 'ON '. $user_table .'.id = subscription.subscriber ' .
  935. 'WHERE subscription.subscribed = %d ';
  936. $user->query(sprintf($qry, $this->profile_id));
  937. $ids = array();
  938. while ($user->fetch()) {
  939. $ids[] = $user->id;
  940. }
  941. $user->free();
  942. return $ids;
  943. }
  944. function getProfileTags()
  945. {
  946. $profile = $this->getProfile();
  947. $list = $profile->getOtherTags($profile);
  948. $ptags = array();
  949. while($list->fetch()) {
  950. $ptags[] = clone($list);
  951. }
  952. return $ptags;
  953. }
  954. public function getAttentionsFromProfileTags()
  955. {
  956. $ni = array();
  957. $ptags = $this->getProfileTags();
  958. foreach ($ptags as $ptag) {
  959. $users = $ptag->getUserSubscribers();
  960. foreach ($users as $id) {
  961. $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG;
  962. }
  963. }
  964. return $ni;
  965. }
  966. /**
  967. * Record this notice to the given group inboxes for delivery.
  968. * Overrides the regular parsing of !group markup.
  969. *
  970. * @param string $group_ids
  971. * @fixme might prefer URIs as identifiers, as for replies?
  972. * best with generalizations on user_group to support
  973. * remote groups better.
  974. */
  975. function saveKnownGroups($group_ids)
  976. {
  977. if (!is_array($group_ids)) {
  978. // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups().
  979. throw new ServerException(_('Bad type provided to saveKnownGroups.'));
  980. }
  981. $groups = array();
  982. foreach (array_unique($group_ids) as $id) {
  983. $group = User_group::getKV('id', $id);
  984. if ($group instanceof User_group) {
  985. common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname");
  986. $result = $this->addToGroupInbox($group);
  987. if (!$result) {
  988. common_log_db_error($gi, 'INSERT', __FILE__);
  989. }
  990. if (common_config('group', 'addtag')) {
  991. // we automatically add a tag for every group name, too
  992. $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($group->nickname),
  993. 'notice_id' => $this->id));
  994. if (is_null($tag)) {
  995. $this->saveTag($group->nickname);
  996. }
  997. }
  998. $groups[] = clone($group);
  999. } else {
  1000. common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist");
  1001. }
  1002. }
  1003. return $groups;
  1004. }
  1005. function addToGroupInbox(User_group $group)
  1006. {
  1007. $gi = Group_inbox::pkeyGet(array('group_id' => $group->id,
  1008. 'notice_id' => $this->id));
  1009. if (!$gi instanceof Group_inbox) {
  1010. $gi = new Group_inbox();
  1011. $gi->group_id = $group->id;
  1012. $gi->notice_id = $this->id;
  1013. $gi->created = $this->created;
  1014. $result = $gi->insert();
  1015. if (!$result) {
  1016. common_log_db_error($gi, 'INSERT', __FILE__);
  1017. // TRANS: Server exception thrown when an update for a group inbox fails.
  1018. throw new ServerException(_('Problem saving group inbox.'));
  1019. }
  1020. self::blow('user_group:notice_ids:%d', $gi->group_id);
  1021. }
  1022. return true;
  1023. }
  1024. /**
  1025. * Save reply records indicating that this notice needs to be
  1026. * delivered to the local users with the given URIs.
  1027. *
  1028. * Since this is expected to be used when saving foreign-sourced
  1029. * messages, we won't deliver to any remote targets as that's the
  1030. * source service's responsibility.
  1031. *
  1032. * Mail notifications etc will be handled later.
  1033. *
  1034. * @param array $uris Array of unique identifier URIs for recipients
  1035. */
  1036. function saveKnownReplies(array $uris)
  1037. {
  1038. if (empty($uris)) {
  1039. return;
  1040. }
  1041. $sender = Profile::getKV($this->profile_id);
  1042. foreach (array_unique($uris) as $uri) {
  1043. try {
  1044. $profile = Profile::fromUri($uri);
  1045. } catch (UnknownUriException $e) {
  1046. common_log(LOG_WARNING, "Unable to determine profile for URI '$uri'");
  1047. continue;
  1048. }
  1049. if ($profile->hasBlocked($sender)) {
  1050. common_log(LOG_INFO, "Not saving reply to profile {$profile->id} ($uri) from sender {$sender->id} because of a block.");
  1051. continue;
  1052. }
  1053. $this->saveReply($profile->id);
  1054. self::blow('reply:stream:%d', $profile->id);
  1055. }
  1056. return;
  1057. }
  1058. /**
  1059. * Pull @-replies from this message's content in StatusNet markup format
  1060. * and save reply records indicating that this message needs to be
  1061. * delivered to those users.
  1062. *
  1063. * Mail notifications to local profiles will be sent later.
  1064. *
  1065. * @return array of integer profile IDs
  1066. */
  1067. function saveReplies()
  1068. {
  1069. // Don't save reply data for repeats
  1070. if (!empty($this->repeat_of)) {
  1071. return array();
  1072. }
  1073. $sender = Profile::getKV($this->profile_id);
  1074. $replied = array();
  1075. // If it's a reply, save for the replied-to author
  1076. try {
  1077. $parent = $this->getParent();
  1078. $author = $parent->getProfile();
  1079. if ($author instanceof Profile) {
  1080. $this->saveReply($author->id);
  1081. $replied[$author->id] = 1;
  1082. self::blow('reply:stream:%d', $author->id);
  1083. }
  1084. } catch (Exception $e) {
  1085. // Not a reply, since it has no parent!
  1086. }
  1087. // @todo ideally this parser information would only
  1088. // be calculated once.
  1089. $mentions = common_find_mentions($this->content, $this);
  1090. // store replied only for first @ (what user/notice what the reply directed,
  1091. // we assume first @ is it)
  1092. foreach ($mentions as $mention) {
  1093. foreach ($mention['mentioned'] as $mentioned) {
  1094. // skip if they're already covered
  1095. if (!empty($replied[$mentioned->id])) {
  1096. continue;
  1097. }
  1098. // Don't save replies from blocked profile to local user
  1099. $mentioned_user = User::getKV('id', $mentioned->id);
  1100. if ($mentioned_user instanceof User && $mentioned_user->hasBlocked($sender)) {
  1101. continue;
  1102. }
  1103. $this->saveReply($mentioned->id);
  1104. $replied[$mentioned->id] = 1;
  1105. self::blow('reply:stream:%d', $mentioned->id);
  1106. }
  1107. }
  1108. $recipientIds = array_keys($replied);
  1109. return $recipientIds;
  1110. }
  1111. function saveReply($profileId)
  1112. {
  1113. $reply = new Reply();
  1114. $reply->notice_id = $this->id;
  1115. $reply->profile_id = $profileId;
  1116. $reply->modified = $this->created;
  1117. $reply->insert();
  1118. return $reply;
  1119. }
  1120. protected $_replies = -1;
  1121. /**
  1122. * Pull the complete list of @-reply targets for this notice.
  1123. *
  1124. * @return array of integer profile ids
  1125. */
  1126. function getReplies()
  1127. {
  1128. if ($this->_replies != -1) {
  1129. return $this->_replies;
  1130. }
  1131. $replyMap = Reply::listGet('notice_id', array($this->id));
  1132. $ids = array();
  1133. foreach ($replyMap[$this->id] as $reply) {
  1134. $ids[] = $reply->profile_id;
  1135. }
  1136. $this->_replies = $ids;
  1137. return $ids;
  1138. }
  1139. function _setReplies($replies)
  1140. {
  1141. $this->_replies = $replies;
  1142. }
  1143. /**
  1144. * Pull the complete list of @-reply targets for this notice.
  1145. *
  1146. * @return array of Profiles
  1147. */
  1148. function getReplyProfiles()
  1149. {
  1150. $ids = $this->getReplies();
  1151. $profiles = Profile::multiGet('id', $ids);
  1152. return $profiles->fetchAll();
  1153. }
  1154. /**
  1155. * Send e-mail notifications to local @-reply targets.
  1156. *
  1157. * Replies must already have been saved; this is expected to be run
  1158. * from the distrib queue handler.
  1159. */
  1160. function sendReplyNotifications()
  1161. {
  1162. // Don't send reply notifications for repeats
  1163. if (!empty($this->repeat_of)) {
  1164. return array();
  1165. }
  1166. $recipientIds = $this->getReplies();
  1167. foreach ($recipientIds as $recipientId) {
  1168. $user = User::getKV('id', $recipientId);
  1169. if ($user instanceof User) {
  1170. mail_notify_attn($user, $this);
  1171. }
  1172. }
  1173. }
  1174. /**
  1175. * Pull list of groups this notice needs to be delivered to,
  1176. * as previously recorded by saveKnownGroups().
  1177. *
  1178. * @return array of Group objects
  1179. */
  1180. protecte

Large files files are truncated, but you can click here to view the full file