Notice.php 98 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976
  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 = array();
  131. /**
  132. * Will always return a profile, if anything fails it will
  133. * (through _setProfile) throw a NoProfileException.
  134. */
  135. public function getProfile()
  136. {
  137. if (!isset($this->_profile[$this->profile_id])) {
  138. // We could've sent getKV directly to _setProfile, but occasionally we get
  139. // a "false" (instead of null), likely because it indicates a cache miss.
  140. $profile = Profile::getKV('id', $this->profile_id);
  141. $this->_setProfile($profile instanceof Profile ? $profile : null);
  142. }
  143. return $this->_profile[$this->profile_id];
  144. }
  145. public function _setProfile(Profile $profile=null)
  146. {
  147. if (!$profile instanceof Profile) {
  148. throw new NoProfileException($this->profile_id);
  149. }
  150. $this->_profile[$this->profile_id] = $profile;
  151. }
  152. function delete($useWhere=false)
  153. {
  154. // For auditing purposes, save a record that the notice
  155. // was deleted.
  156. // @fixme we have some cases where things get re-run and so the
  157. // insert fails.
  158. $deleted = Deleted_notice::getKV('id', $this->id);
  159. if (!$deleted instanceof Deleted_notice) {
  160. $deleted = Deleted_notice::getKV('uri', $this->uri);
  161. }
  162. if (!$deleted instanceof Deleted_notice) {
  163. $deleted = new Deleted_notice();
  164. $deleted->id = $this->id;
  165. $deleted->profile_id = $this->profile_id;
  166. $deleted->uri = $this->uri;
  167. $deleted->created = $this->created;
  168. $deleted->deleted = common_sql_now();
  169. $deleted->insert();
  170. }
  171. if (Event::handle('NoticeDeleteRelated', array($this))) {
  172. // Clear related records
  173. $this->clearReplies();
  174. $this->clearRepeats();
  175. $this->clearTags();
  176. $this->clearGroupInboxes();
  177. $this->clearFiles();
  178. $this->clearAttentions();
  179. // NOTE: we don't clear queue items
  180. }
  181. $result = parent::delete($useWhere);
  182. $this->blowOnDelete();
  183. return $result;
  184. }
  185. public function getUri()
  186. {
  187. return $this->uri;
  188. }
  189. /*
  190. * Get a Notice object by URI. Will call external plugins for help
  191. * using the event StartGetNoticeFromURI.
  192. *
  193. * @param string $uri A unique identifier for a resource (notice in this case)
  194. */
  195. static function fromUri($uri)
  196. {
  197. $notice = null;
  198. if (Event::handle('StartGetNoticeFromUri', array($uri, &$notice))) {
  199. $notice = Notice::getKV('uri', $uri);
  200. Event::handle('EndGetNoticeFromUri', array($uri, $notice));
  201. }
  202. if (!$notice instanceof Notice) {
  203. throw new UnknownUriException($uri);
  204. }
  205. return $notice;
  206. }
  207. /*
  208. * @param $root boolean If true, link to just the conversation root.
  209. *
  210. * @return URL to conversation
  211. */
  212. public function getConversationUrl($anchor=true)
  213. {
  214. return Conversation::getUrlFromNotice($this, $anchor);
  215. }
  216. /*
  217. * Get the local representation URL of this notice.
  218. */
  219. public function getLocalUrl()
  220. {
  221. return common_local_url('shownotice', array('notice' => $this->id), null, null, false);
  222. }
  223. public function getTitle()
  224. {
  225. $title = null;
  226. if (Event::handle('GetNoticeTitle', array($this, &$title))) {
  227. // TRANS: Title of a notice posted without a title value.
  228. // TRANS: %1$s is a user name, %2$s is the notice creation date/time.
  229. $title = sprintf(_('%1$s\'s status on %2$s'),
  230. $this->getProfile()->getFancyName(),
  231. common_exact_date($this->created));
  232. }
  233. return $title;
  234. }
  235. public function getContent()
  236. {
  237. return $this->content;
  238. }
  239. /*
  240. * Get the original representation URL of this notice.
  241. *
  242. * @param boolean $fallback Whether to fall back to generate a local URL or throw InvalidUrlException
  243. */
  244. public function getUrl($fallback=false)
  245. {
  246. // The risk is we start having empty urls and non-http uris...
  247. // and we can't really handle any other protocol right now.
  248. switch (true) {
  249. case common_valid_http_url($this->url): // should we allow non-http/https URLs?
  250. return $this->url;
  251. case !$this->isLocal() && common_valid_http_url($this->uri): // Sometimes we only have the URI for remote posts.
  252. return $this->uri;
  253. case $this->isLocal() || $fallback:
  254. // let's generate a valid link to our locally available notice on demand
  255. return common_local_url('shownotice', array('notice' => $this->id), null, null, false);
  256. default:
  257. common_debug('No URL available for notice: id='.$this->id);
  258. throw new InvalidUrlException($this->url);
  259. }
  260. }
  261. public function get_object_type($canonical=false) {
  262. return $canonical
  263. ? ActivityObject::canonicalType($this->object_type)
  264. : $this->object_type;
  265. }
  266. public static function getByUri($uri)
  267. {
  268. $notice = new Notice();
  269. $notice->uri = $uri;
  270. if (!$notice->find(true)) {
  271. throw new NoResultException($notice);
  272. }
  273. return $notice;
  274. }
  275. /**
  276. * Extract #hashtags from this notice's content and save them to the database.
  277. */
  278. function saveTags()
  279. {
  280. /* extract all #hastags */
  281. $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/u', strtolower($this->content), $match);
  282. if (!$count) {
  283. return true;
  284. }
  285. /* Add them to the database */
  286. return $this->saveKnownTags($match[1]);
  287. }
  288. /**
  289. * Record the given set of hash tags in the db for this notice.
  290. * Given tag strings will be normalized and checked for dupes.
  291. */
  292. function saveKnownTags($hashtags)
  293. {
  294. //turn each into their canonical tag
  295. //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag
  296. for($i=0; $i<count($hashtags); $i++) {
  297. /* elide characters we don't want in the tag */
  298. $hashtags[$i] = common_canonical_tag($hashtags[$i]);
  299. }
  300. foreach(array_unique($hashtags) as $hashtag) {
  301. $this->saveTag($hashtag);
  302. self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $hashtag);
  303. }
  304. return true;
  305. }
  306. /**
  307. * Record a single hash tag as associated with this notice.
  308. * Tag format and uniqueness must be validated by caller.
  309. */
  310. function saveTag($hashtag)
  311. {
  312. $tag = new Notice_tag();
  313. $tag->notice_id = $this->id;
  314. $tag->tag = $hashtag;
  315. $tag->created = $this->created;
  316. $id = $tag->insert();
  317. if (!$id) {
  318. // TRANS: Server exception. %s are the error details.
  319. throw new ServerException(sprintf(_('Database error inserting hashtag: %s.'),
  320. $last_error->message));
  321. return;
  322. }
  323. // if it's saved, blow its cache
  324. $tag->blowCache(false);
  325. }
  326. /**
  327. * Save a new notice and push it out to subscribers' inboxes.
  328. * Poster's permissions are checked before sending.
  329. *
  330. * @param int $profile_id Profile ID of the poster
  331. * @param string $content source message text; links may be shortened
  332. * per current user's preference
  333. * @param string $source source key ('web', 'api', etc)
  334. * @param array $options Associative array of optional properties:
  335. * string 'created' timestamp of notice; defaults to now
  336. * int 'is_local' source/gateway ID, one of:
  337. * Notice::LOCAL_PUBLIC - Local, ok to appear in public timeline
  338. * Notice::REMOTE - Sent from a remote service;
  339. * hide from public timeline but show in
  340. * local "and friends" timelines
  341. * Notice::LOCAL_NONPUBLIC - Local, but hide from public timeline
  342. * Notice::GATEWAY - From another non-OStatus service;
  343. * will not appear in public views
  344. * float 'lat' decimal latitude for geolocation
  345. * float 'lon' decimal longitude for geolocation
  346. * int 'location_id' geoname identifier
  347. * int 'location_ns' geoname namespace to interpret location_id
  348. * int 'reply_to'; notice ID this is a reply to
  349. * int 'repeat_of'; notice ID this is a repeat of
  350. * string 'uri' unique ID for notice; a unique tag uri (can be url or anything too)
  351. * string 'url' permalink to notice; defaults to local notice URL
  352. * string 'rendered' rendered HTML version of content
  353. * array 'replies' list of profile URIs for reply delivery in
  354. * place of extracting @-replies from content.
  355. * array 'groups' list of group IDs to deliver to, in place of
  356. * extracting ! tags from content
  357. * array 'tags' list of hashtag strings to save with the notice
  358. * in place of extracting # tags from content
  359. * array 'urls' list of attached/referred URLs to save with the
  360. * notice in place of extracting links from content
  361. * boolean 'distribute' whether to distribute the notice, default true
  362. * string 'object_type' URL of the associated object type (default ActivityObject::NOTE)
  363. * string 'verb' URL of the associated verb (default ActivityVerb::POST)
  364. * int 'scope' Scope bitmask; default to SITE_SCOPE on private sites, 0 otherwise
  365. *
  366. * @fixme tag override
  367. *
  368. * @return Notice
  369. * @throws ClientException
  370. */
  371. static function saveNew($profile_id, $content, $source, array $options=null) {
  372. $defaults = array('uri' => null,
  373. 'url' => null,
  374. 'conversation' => null, // URI of conversation
  375. 'reply_to' => null, // This will override convo URI if the parent is known
  376. 'repeat_of' => null, // This will override convo URI if the repeated notice is known
  377. 'scope' => null,
  378. 'distribute' => true,
  379. 'object_type' => null,
  380. 'verb' => null);
  381. if (!empty($options) && is_array($options)) {
  382. $options = array_merge($defaults, $options);
  383. extract($options);
  384. } else {
  385. extract($defaults);
  386. }
  387. if (!isset($is_local)) {
  388. $is_local = Notice::LOCAL_PUBLIC;
  389. }
  390. $profile = Profile::getKV('id', $profile_id);
  391. if (!$profile instanceof Profile) {
  392. // TRANS: Client exception thrown when trying to save a notice for an unknown user.
  393. throw new ClientException(_('Problem saving notice. Unknown user.'));
  394. }
  395. $user = User::getKV('id', $profile_id);
  396. if ($user instanceof User) {
  397. // Use the local user's shortening preferences, if applicable.
  398. $final = $user->shortenLinks($content);
  399. } else {
  400. $final = common_shorten_links($content);
  401. }
  402. if (Notice::contentTooLong($final)) {
  403. // TRANS: Client exception thrown if a notice contains too many characters.
  404. throw new ClientException(_('Problem saving notice. Too long.'));
  405. }
  406. if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) {
  407. common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.');
  408. // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame.
  409. throw new ClientException(_('Too many notices too fast; take a breather '.
  410. 'and post again in a few minutes.'));
  411. }
  412. if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
  413. common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
  414. // TRANS: Client exception thrown when a user tries to post too many duplicate notices in a given time frame.
  415. throw new ClientException(_('Too many duplicate messages too quickly;'.
  416. ' take a breather and post again in a few minutes.'));
  417. }
  418. if (!$profile->hasRight(Right::NEWNOTICE)) {
  419. common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $profile->nickname);
  420. // TRANS: Client exception thrown when a user tries to post while being banned.
  421. throw new ClientException(_('You are banned from posting notices on this site.'), 403);
  422. }
  423. $notice = new Notice();
  424. $notice->profile_id = $profile_id;
  425. $autosource = common_config('public', 'autosource');
  426. // Sandboxed are non-false, but not 1, either
  427. if (!$profile->hasRight(Right::PUBLICNOTICE) ||
  428. ($source && $autosource && in_array($source, $autosource))) {
  429. $notice->is_local = Notice::LOCAL_NONPUBLIC;
  430. } else {
  431. $notice->is_local = $is_local;
  432. }
  433. if (!empty($created)) {
  434. $notice->created = $created;
  435. } else {
  436. $notice->created = common_sql_now();
  437. }
  438. if (!$notice->isLocal()) {
  439. // Only do these checks for non-local notices. Local notices will generate these values later.
  440. if (!common_valid_http_url($url)) {
  441. common_debug('Bad notice URL: ['.$url.'], URI: ['.$uri.']. Cannot link back to original! This is normal for shared notices etc.');
  442. }
  443. if (empty($uri)) {
  444. throw new ServerException('No URI for remote notice. Cannot accept that.');
  445. }
  446. }
  447. $notice->content = $final;
  448. $notice->source = $source;
  449. $notice->uri = $uri;
  450. $notice->url = $url;
  451. // Get the groups here so we can figure out replies and such
  452. if (!isset($groups)) {
  453. $groups = User_group::idsFromText($notice->content, $profile);
  454. }
  455. $reply = null;
  456. // Handle repeat case
  457. if (isset($repeat_of)) {
  458. // Check for a private one
  459. $repeat = Notice::getKV('id', $repeat_of);
  460. if (!($repeat instanceof Notice)) {
  461. // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice.
  462. throw new ClientException(_('Cannot repeat; original notice is missing or deleted.'));
  463. }
  464. if ($profile->id == $repeat->profile_id) {
  465. // TRANS: Client error displayed when trying to repeat an own notice.
  466. throw new ClientException(_('You cannot repeat your own notice.'));
  467. }
  468. if ($repeat->scope != Notice::SITE_SCOPE &&
  469. $repeat->scope != Notice::PUBLIC_SCOPE) {
  470. // TRANS: Client error displayed when trying to repeat a non-public notice.
  471. throw new ClientException(_('Cannot repeat a private notice.'), 403);
  472. }
  473. if (!$repeat->inScope($profile)) {
  474. // The generic checks above should cover this, but let's be sure!
  475. // TRANS: Client error displayed when trying to repeat a notice you cannot access.
  476. throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403);
  477. }
  478. if ($profile->hasRepeated($repeat)) {
  479. // TRANS: Client error displayed when trying to repeat an already repeated notice.
  480. throw new ClientException(_('You already repeated that notice.'));
  481. }
  482. $notice->repeat_of = $repeat->id;
  483. $notice->conversation = $repeat->conversation;
  484. } else {
  485. $reply = null;
  486. // If $reply_to is specified, we check that it exists, and then
  487. // return it if it does
  488. if (!empty($reply_to)) {
  489. $reply = Notice::getKV('id', $reply_to);
  490. } elseif (in_array($source, array('xmpp', 'mail', 'sms'))) {
  491. // If the source lacks capability of sending the "reply_to"
  492. // metadata, let's try to find an inline replyto-reference.
  493. $reply = self::getInlineReplyTo($profile, $final);
  494. }
  495. if ($reply instanceof Notice) {
  496. if (!$reply->inScope($profile)) {
  497. // TRANS: Client error displayed when trying to reply to a notice a the target has no access to.
  498. // TRANS: %1$s is a user nickname, %2$d is a notice ID (number).
  499. throw new ClientException(sprintf(_('%1$s has no access to notice %2$d.'),
  500. $profile->nickname, $reply->id), 403);
  501. }
  502. // If it's a repeat, the reply_to should be to the original
  503. if ($reply->isRepeat()) {
  504. $notice->reply_to = $reply->repeat_of;
  505. } else {
  506. $notice->reply_to = $reply->id;
  507. }
  508. // But the conversation ought to be the same :)
  509. $notice->conversation = $reply->conversation;
  510. // If the original is private to a group, and notice has
  511. // no group specified, make it to the same group(s)
  512. if (empty($groups) && ($reply->scope & Notice::GROUP_SCOPE)) {
  513. $groups = array();
  514. $replyGroups = $reply->getGroups();
  515. foreach ($replyGroups as $group) {
  516. if ($profile->isMember($group)) {
  517. $groups[] = $group->id;
  518. }
  519. }
  520. }
  521. // Scope set below
  522. }
  523. // If we don't know the reply, we might know the conversation!
  524. // This will happen if a known remote user replies to an
  525. // unknown remote user - within a known conversation.
  526. if (empty($notice->conversation) and !empty($options['conversation'])) {
  527. $conv = Conversation::getKV('uri', $options['conversation']);
  528. if ($conv instanceof Conversation) {
  529. common_debug('Conversation stitched together from (probably) reply to unknown remote user. Activity creation time ('.$notice->created.') should maybe be compared to conversation creation time ('.$conv->created.').');
  530. $notice->conversation = $conv->id;
  531. } else {
  532. // Conversation URI was not found, so we must create it. But we can't create it
  533. // until we have a Notice ID because of the database layout...
  534. $notice->tmp_conv_uri = $options['conversation'];
  535. }
  536. } else {
  537. // If we're not using the attached conversation URI let's remove it
  538. // so we don't mistake ourselves later, when creating our own Conversation.
  539. // This implies that the notice knows which conversation it belongs to.
  540. $options['conversation'] = null;
  541. }
  542. }
  543. if (!empty($lat) && !empty($lon)) {
  544. $notice->lat = $lat;
  545. $notice->lon = $lon;
  546. }
  547. if (!empty($location_ns) && !empty($location_id)) {
  548. $notice->location_id = $location_id;
  549. $notice->location_ns = $location_ns;
  550. }
  551. if (!empty($rendered)) {
  552. $notice->rendered = $rendered;
  553. } else {
  554. $notice->rendered = common_render_content($final, $notice);
  555. }
  556. if (empty($verb)) {
  557. if ($notice->isRepeat()) {
  558. $notice->verb = ActivityVerb::SHARE;
  559. $notice->object_type = ActivityObject::ACTIVITY;
  560. } else {
  561. $notice->verb = ActivityVerb::POST;
  562. }
  563. } else {
  564. $notice->verb = $verb;
  565. }
  566. if (empty($object_type)) {
  567. $notice->object_type = (empty($notice->reply_to)) ? ActivityObject::NOTE : ActivityObject::COMMENT;
  568. } else {
  569. $notice->object_type = $object_type;
  570. }
  571. if (is_null($scope) && $reply instanceof Notice) {
  572. $notice->scope = $reply->scope;
  573. } else {
  574. $notice->scope = $scope;
  575. }
  576. $notice->scope = self::figureOutScope($profile, $groups, $notice->scope);
  577. if (Event::handle('StartNoticeSave', array(&$notice))) {
  578. // XXX: some of these functions write to the DB
  579. try {
  580. $notice->insert(); // throws exception on failure
  581. // If it's not part of a conversation, it's
  582. // the beginning of a new conversation.
  583. if (empty($notice->conversation)) {
  584. $orig = clone($notice);
  585. // $act->context->conversation will be null if it was not provided
  586. $conv = Conversation::create($notice, $options['conversation']);
  587. $notice->conversation = $conv->id;
  588. $notice->update($orig);
  589. }
  590. } catch (Exception $e) {
  591. // Let's test if we managed initial insert, which would imply
  592. // failing on some update-part (check 'insert()'). Delete if
  593. // something had been stored to the database.
  594. if (!empty($notice->id)) {
  595. $notice->delete();
  596. }
  597. throw $e;
  598. }
  599. }
  600. // Clear the cache for subscribed users, so they'll update at next request
  601. // XXX: someone clever could prepend instead of clearing the cache
  602. // Save per-notice metadata...
  603. if (isset($replies)) {
  604. $notice->saveKnownReplies($replies);
  605. } else {
  606. $notice->saveReplies();
  607. }
  608. if (isset($tags)) {
  609. $notice->saveKnownTags($tags);
  610. } else {
  611. $notice->saveTags();
  612. }
  613. // Note: groups may save tags, so must be run after tags are saved
  614. // to avoid errors on duplicates.
  615. // Note: groups should always be set.
  616. $notice->saveKnownGroups($groups);
  617. if (isset($urls)) {
  618. $notice->saveKnownUrls($urls);
  619. } else {
  620. $notice->saveUrls();
  621. }
  622. if ($distribute) {
  623. // Prepare inbox delivery, may be queued to background.
  624. $notice->distribute();
  625. }
  626. return $notice;
  627. }
  628. static function saveActivity(Activity $act, Profile $actor, array $options=array())
  629. {
  630. // First check if we're going to let this Activity through from the specific actor
  631. if (!$actor->hasRight(Right::NEWNOTICE)) {
  632. common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $actor->getNickname());
  633. // TRANS: Client exception thrown when a user tries to post while being banned.
  634. throw new ClientException(_m('You are banned from posting notices on this site.'), 403);
  635. }
  636. if (common_config('throttle', 'enabled') && !self::checkEditThrottle($actor->id)) {
  637. common_log(LOG_WARNING, 'Excessive posting by profile #' . $actor->id . '; throttled.');
  638. // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame.
  639. throw new ClientException(_m('Too many notices too fast; take a breather '.
  640. 'and post again in a few minutes.'));
  641. }
  642. // Get ActivityObject properties
  643. if (!empty($act->id)) {
  644. // implied object
  645. $options['uri'] = $act->id;
  646. $options['url'] = $act->link;
  647. } else {
  648. $actobj = count($act->objects)==1 ? $act->objects[0] : null;
  649. if (!is_null($actobj) && !empty($actobj->id)) {
  650. $options['uri'] = $actobj->id;
  651. if ($actobj->link) {
  652. $options['url'] = $actobj->link;
  653. } elseif (preg_match('!^https?://!', $actobj->id)) {
  654. $options['url'] = $actobj->id;
  655. }
  656. }
  657. }
  658. $defaults = array(
  659. 'groups' => array(),
  660. 'is_local' => self::LOCAL_PUBLIC,
  661. 'mentions' => array(),
  662. 'reply_to' => null,
  663. 'repeat_of' => null,
  664. 'scope' => null,
  665. 'source' => 'unknown',
  666. 'tags' => array(),
  667. 'uri' => null,
  668. 'url' => null,
  669. 'urls' => array(),
  670. 'distribute' => true);
  671. // options will have default values when nothing has been supplied
  672. $options = array_merge($defaults, $options);
  673. foreach (array_keys($defaults) as $key) {
  674. // Only convert the keynames we specify ourselves from 'defaults' array into variables
  675. $$key = $options[$key];
  676. }
  677. extract($options, EXTR_SKIP);
  678. $stored = new Notice();
  679. if (!empty($uri)) {
  680. $stored->uri = $uri;
  681. if ($stored->find()) {
  682. common_debug('cannot create duplicate Notice URI: '.$stored->uri);
  683. throw new Exception('Notice URI already exists');
  684. }
  685. }
  686. $stored->profile_id = $actor->id;
  687. $stored->source = $source;
  688. $stored->uri = $uri;
  689. $stored->url = $url;
  690. $stored->verb = $act->verb;
  691. // Use the local user's shortening preferences, if applicable.
  692. $stored->rendered = $actor->isLocal()
  693. ? $actor->shortenLinks($act->content)
  694. : $act->content;
  695. $stored->content = common_strip_html($stored->rendered);
  696. $autosource = common_config('public', 'autosource');
  697. // Sandboxed are non-false, but not 1, either
  698. if (!$actor->hasRight(Right::PUBLICNOTICE) ||
  699. ($source && $autosource && in_array($source, $autosource))) {
  700. $stored->is_local = Notice::LOCAL_NONPUBLIC;
  701. }
  702. // Maybe a missing act-time should be fatal if the actor is not local?
  703. if (!empty($act->time)) {
  704. $stored->created = common_sql_date($act->time);
  705. } else {
  706. $stored->created = common_sql_now();
  707. }
  708. $reply = null;
  709. if ($act->context instanceof ActivityContext && !empty($act->context->replyToID)) {
  710. $reply = self::getKV('uri', $act->context->replyToID);
  711. }
  712. if (!$reply instanceof Notice && $act->target instanceof ActivityObject) {
  713. $reply = self::getKV('uri', $act->target->id);
  714. }
  715. if ($reply instanceof Notice) {
  716. if (!$reply->inScope($actor)) {
  717. // TRANS: Client error displayed when trying to reply to a notice a the target has no access to.
  718. // TRANS: %1$s is a user nickname, %2$d is a notice ID (number).
  719. throw new ClientException(sprintf(_m('%1$s has no right to reply to notice %2$d.'), $actor->getNickname(), $reply->id), 403);
  720. }
  721. $stored->reply_to = $reply->id;
  722. $stored->conversation = $reply->conversation;
  723. // If the original is private to a group, and notice has no group specified,
  724. // make it to the same group(s)
  725. if (empty($groups) && ($reply->scope & Notice::GROUP_SCOPE)) {
  726. $groups = array();
  727. $replyGroups = $reply->getGroups();
  728. foreach ($replyGroups as $group) {
  729. if ($actor->isMember($group)) {
  730. $groups[] = $group->id;
  731. }
  732. }
  733. }
  734. if (is_null($scope)) {
  735. $scope = $reply->scope;
  736. }
  737. }
  738. if ($act->context instanceof ActivityContext) {
  739. $location = $act->context->location;
  740. if ($location) {
  741. $stored->lat = $location->lat;
  742. $stored->lon = $location->lon;
  743. if ($location->location_id) {
  744. $stored->location_ns = $location->location_ns;
  745. $stored->location_id = $location->location_id;
  746. }
  747. }
  748. } else {
  749. $act->context = new ActivityContext();
  750. }
  751. $stored->scope = self::figureOutScope($actor, $groups, $scope);
  752. foreach ($act->categories as $cat) {
  753. if ($cat->term) {
  754. $term = common_canonical_tag($cat->term);
  755. if (!empty($term)) {
  756. $tags[] = $term;
  757. }
  758. }
  759. }
  760. foreach ($act->enclosures as $href) {
  761. // @todo FIXME: Save these locally or....?
  762. $urls[] = $href;
  763. }
  764. if (Event::handle('StartNoticeSave', array(&$stored))) {
  765. // XXX: some of these functions write to the DB
  766. try {
  767. $stored->insert(); // throws exception on error
  768. $orig = clone($stored); // for updating later in this try clause
  769. // If it's not part of a conversation, it's
  770. // the beginning of a new conversation.
  771. if (empty($stored->conversation)) {
  772. // $act->context->conversation will be null if it was not provided
  773. $conv = Conversation::create($stored, $act->context->conversation);
  774. $stored->conversation = $conv->id;
  775. }
  776. $object = null;
  777. Event::handle('StoreActivityObject', array($act, $stored, $options, &$object));
  778. if (empty($object)) {
  779. throw new ServerException('No object from StoreActivityObject '.$stored->uri . ': '.$act->asString());
  780. }
  781. $stored->object_type = ActivityUtils::resolveUri($object->getObjectType(), true);
  782. $stored->update($orig);
  783. } catch (Exception $e) {
  784. if (empty($stored->id)) {
  785. common_debug('Failed to save stored object entry in database ('.$e->getMessage().')');
  786. } else {
  787. common_debug('Failed to store activity object in database ('.$e->getMessage().'), deleting notice id '.$stored->id);
  788. $stored->delete();
  789. }
  790. throw $e;
  791. }
  792. }
  793. if (!$stored instanceof Notice) {
  794. throw new ServerException('StartNoticeSave did not give back a Notice');
  795. }
  796. // Save per-notice metadata...
  797. $mentions = array();
  798. $groups = array();
  799. // This event lets plugins filter out non-local recipients (attentions we don't care about)
  800. // Used primarily for OStatus (and if we don't federate, all attentions would be local anyway)
  801. Event::handle('GetLocalAttentions', array($actor, $act->context->attention, &$mentions, &$groups));
  802. if (!empty($mentions)) {
  803. $stored->saveKnownReplies($mentions);
  804. } else {
  805. $stored->saveReplies();
  806. }
  807. if (!empty($tags)) {
  808. $stored->saveKnownTags($tags);
  809. } else {
  810. $stored->saveTags();
  811. }
  812. // Note: groups may save tags, so must be run after tags are saved
  813. // to avoid errors on duplicates.
  814. // Note: groups should always be set.
  815. $stored->saveKnownGroups($groups);
  816. if (!empty($urls)) {
  817. $stored->saveKnownUrls($urls);
  818. } else {
  819. $stored->saveUrls();
  820. }
  821. if ($distribute) {
  822. // Prepare inbox delivery, may be queued to background.
  823. $stored->distribute();
  824. }
  825. return $stored;
  826. }
  827. static public function figureOutScope(Profile $actor, array $groups, $scope=null) {
  828. if (is_null($scope)) {
  829. $scope = self::defaultScope();
  830. }
  831. // For private streams
  832. try {
  833. $user = $actor->getUser();
  834. // FIXME: We can't do bit comparison with == (Legacy StatusNet thing. Let's keep it for now.)
  835. if ($user->private_stream && ($scope == Notice::PUBLIC_SCOPE || $scope == Notice::SITE_SCOPE)) {
  836. $scope |= Notice::FOLLOWER_SCOPE;
  837. }
  838. } catch (NoSuchUserException $e) {
  839. // TODO: Not a local user, so we don't know about scope preferences... yet!
  840. }
  841. // Force the scope for private groups
  842. foreach ($groups as $group_id) {
  843. $group = User_group::staticGet('id', $group_id);
  844. if ($group instanceof User_group) {
  845. if ($group->force_scope) {
  846. $scope |= Notice::GROUP_SCOPE;
  847. break;
  848. }
  849. }
  850. }
  851. return $scope;
  852. }
  853. function blowOnInsert($conversation = false)
  854. {
  855. $this->blowStream('profile:notice_ids:%d', $this->profile_id);
  856. if ($this->isPublic()) {
  857. $this->blowStream('public');
  858. $this->blowStream('networkpublic');
  859. }
  860. self::blow('notice:list-ids:conversation:%s', $this->conversation);
  861. self::blow('conversation:notice_count:%d', $this->conversation);
  862. if ($this->isRepeat()) {
  863. // XXX: we should probably only use one of these
  864. $this->blowStream('notice:repeats:%d', $this->repeat_of);
  865. self::blow('notice:list-ids:repeat_of:%d', $this->repeat_of);
  866. }
  867. $original = Notice::getKV('id', $this->repeat_of);
  868. if ($original instanceof Notice) {
  869. $originalUser = User::getKV('id', $original->profile_id);
  870. if ($originalUser instanceof User) {
  871. $this->blowStream('user:repeats_of_me:%d', $originalUser->id);
  872. }
  873. }
  874. $profile = Profile::getKV($this->profile_id);
  875. if ($profile instanceof Profile) {
  876. $profile->blowNoticeCount();
  877. }
  878. $ptags = $this->getProfileTags();
  879. foreach ($ptags as $ptag) {
  880. $ptag->blowNoticeStreamCache();
  881. }
  882. }
  883. /**
  884. * Clear cache entries related to this notice at delete time.
  885. * Necessary to avoid breaking paging on public, profile timelines.
  886. */
  887. function blowOnDelete()
  888. {
  889. $this->blowOnInsert();
  890. self::blow('profile:notice_ids:%d;last', $this->profile_id);
  891. if ($this->isPublic()) {
  892. self::blow('public;last');
  893. self::blow('networkpublic;last');
  894. }
  895. self::blow('fave:by_notice', $this->id);
  896. if ($this->conversation) {
  897. // In case we're the first, will need to calc a new root.
  898. self::blow('notice:conversation_root:%d', $this->conversation);
  899. }
  900. $ptags = $this->getProfileTags();
  901. foreach ($ptags as $ptag) {
  902. $ptag->blowNoticeStreamCache(true);
  903. }
  904. }
  905. function blowStream()
  906. {
  907. $c = self::memcache();
  908. if (empty($c)) {
  909. return false;
  910. }
  911. $args = func_get_args();
  912. $format = array_shift($args);
  913. $keyPart = vsprintf($format, $args);
  914. $cacheKey = Cache::key($keyPart);
  915. $c->delete($cacheKey);
  916. // delete the "last" stream, too, if this notice is
  917. // older than the top of that stream
  918. $lastKey = $cacheKey.';last';
  919. $lastStr = $c->get($lastKey);
  920. if ($lastStr !== false) {
  921. $window = explode(',', $lastStr);
  922. $lastID = $window[0];
  923. $lastNotice = Notice::getKV('id', $lastID);
  924. if (!$lastNotice instanceof Notice // just weird
  925. || strtotime($lastNotice->created) >= strtotime($this->created)) {
  926. $c->delete($lastKey);
  927. }
  928. }
  929. }
  930. /** save all urls in the notice to the db
  931. *
  932. * follow redirects and save all available file information
  933. * (mimetype, date, size, oembed, etc.)
  934. *
  935. * @return void
  936. */
  937. function saveUrls() {
  938. if (common_config('attachments', 'process_links')) {
  939. common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id);
  940. }
  941. }
  942. /**
  943. * Save the given URLs as related links/attachments to the db
  944. *
  945. * follow redirects and save all available file information
  946. * (mimetype, date, size, oembed, etc.)
  947. *
  948. * @return void
  949. */
  950. function saveKnownUrls($urls)
  951. {
  952. if (common_config('attachments', 'process_links')) {
  953. // @fixme validation?
  954. foreach (array_unique($urls) as $url) {
  955. try {
  956. File::processNew($url, $this->id);
  957. } catch (ServerException $e) {
  958. // Could not save URL. Log it?
  959. }
  960. }
  961. }
  962. }
  963. /**
  964. * @private callback
  965. */
  966. function saveUrl($url, $notice_id) {
  967. try {
  968. File::processNew($url, $notice_id);
  969. } catch (ServerException $e) {
  970. // Could not save URL. Log it?
  971. }
  972. }
  973. static function checkDupes($profile_id, $content) {
  974. $profile = Profile::getKV($profile_id);
  975. if (!$profile instanceof Profile) {
  976. return false;
  977. }
  978. $notice = $profile->getNotices(0, CachingNoticeStream::CACHE_WINDOW);
  979. if (!empty($notice)) {
  980. $last = 0;
  981. while ($notice->fetch()) {
  982. if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
  983. return true;
  984. } else if ($notice->content == $content) {
  985. return false;
  986. }
  987. }
  988. }
  989. // If we get here, oldest item in cache window is not
  990. // old enough for dupe limit; do direct check against DB
  991. $notice = new Notice();
  992. $notice->profile_id = $profile_id;
  993. $notice->content = $content;
  994. $threshold = common_sql_date(time() - common_config('site', 'dupelimit'));
  995. $notice->whereAdd(sprintf("created > '%s'", $notice->escape($threshold)));
  996. $cnt = $notice->count();
  997. return ($cnt == 0);
  998. }
  999. static function checkEditThrottle($profile_id) {
  1000. $profile = Profile::getKV($profile_id);
  1001. if (!$profile instanceof Profile) {
  1002. return false;
  1003. }
  1004. // Get the Nth notice
  1005. $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1);
  1006. if ($notice && $notice->fetch()) {
  1007. // If the Nth notice was posted less than timespan seconds ago
  1008. if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) {
  1009. // Then we throttle
  1010. return false;
  1011. }
  1012. }
  1013. // Either not N notices in the stream, OR the Nth was not posted within timespan seconds
  1014. return true;
  1015. }
  1016. protected $_attachments = array();
  1017. function attachments() {
  1018. if (isset($this->_attachments[$this->id])) {
  1019. return $this->_attachments[$this->id];
  1020. }
  1021. $f2ps = File_to_post::listGet('post_id', array($this->id));
  1022. $ids = array();
  1023. foreach ($f2ps[$this->id] as $f2p) {
  1024. $ids[] = $f2p->file_id;
  1025. }
  1026. $files = File::multiGet('id', $ids);
  1027. $this->_attachments[$this->id] = $files->fetchAll();
  1028. return $this->_attachments[$this->id];
  1029. }
  1030. function _setAttachments($attachments)
  1031. {
  1032. $this->_attachments[$this->id] = $attachments;
  1033. }
  1034. function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0)
  1035. {
  1036. $stream = new PublicNoticeStream();
  1037. return $stream->getNotices($offset, $limit, $since_id, $max_id);
  1038. }
  1039. function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
  1040. {
  1041. $stream = new ConversationNoticeStream($id);
  1042. return $stream->getNotices($offset, $limit, $since_id, $max_id);
  1043. }
  1044. /**
  1045. * Is this notice part of an active conversation?
  1046. *
  1047. * @return boolean true if other messages exist in the same
  1048. * conversation, false if this is the only one
  1049. */
  1050. function hasConversation()
  1051. {
  1052. if (!empty($this->conversation)) {
  1053. $conversation = Notice::conversationStream(
  1054. $this->conversation,
  1055. 1,
  1056. 1
  1057. );
  1058. if ($conversation->N > 0) {
  1059. return true;
  1060. }
  1061. }
  1062. return false;
  1063. }
  1064. /**
  1065. * Grab the earliest notice from this conversation.
  1066. *
  1067. * @return Notice or null
  1068. */
  1069. function conversationRoot($profile=-1)
  1070. {
  1071. // XXX: can this happen?
  1072. if (empty($this->conversation)) {
  1073. return null;
  1074. }
  1075. // Get the current profile if not specified
  1076. if (is_int($profile) && $profile == -1) {
  1077. $profile = Profile::current();
  1078. }
  1079. // If this notice is out of scope, no root for you!
  1080. if (!$this->inScope($profile)) {
  1081. return null;
  1082. }
  1083. // If this isn't a reply to anything, then it's its own
  1084. // root if it's the earliest notice in the conversation:
  1085. if (empty($this->reply_to)) {
  1086. $root = new Notice;
  1087. $root->conversation = $this->conversation;
  1088. $root->orderBy('notice.created ASC');
  1089. $root->find();
  1090. $root->fetch();
  1091. $root->free();
  1092. return $root;
  1093. }
  1094. if (is_null($profile)) {
  1095. $keypart = sprintf('notice:conversation_root:%d:null', $this->id);
  1096. } else {
  1097. $keypart = sprintf('notice:conversation_root:%d:%d',
  1098. $this->id,
  1099. $profile->id);
  1100. }
  1101. $root = self::cacheGet($keypart);
  1102. if ($root !== false && $root->inScope($profile)) {
  1103. return $root;
  1104. }
  1105. $last = $this;
  1106. while (true) {
  1107. try {
  1108. $parent = $last->getParent();
  1109. if ($parent->inScope($profile)) {
  1110. $last = $parent;
  1111. continue;
  1112. }
  1113. } catch (Exception $e) {
  1114. // Latest notice has no parent
  1115. }
  1116. // No parent, or parent out of scope
  1117. $root = $last;
  1118. break;
  1119. }
  1120. self::cacheSet($keypart, $root);
  1121. return $root;
  1122. }
  1123. /**
  1124. * Pull up a full list of local recipients who will be getting
  1125. * this notice in their inbox. Results will be cached, so don't
  1126. * change the input data wily-nilly!
  1127. *
  1128. * @param array $groups optional list of Group objects;
  1129. * if left empty, will be loaded from group_inbox records
  1130. * @param array $recipient optional list of reply profile ids
  1131. * if left empty, will be loaded from reply records
  1132. * @return array associating recipient user IDs with an inbox source constant
  1133. */
  1134. function whoGets(array $groups=null, array $recipients=null)
  1135. {
  1136. $c = self::memcache();
  1137. if (!empty($c)) {
  1138. $ni = $c->get(Cache::key('notice:who_gets:'.$this->id));
  1139. if ($ni !== false) {
  1140. return $ni;
  1141. }
  1142. }
  1143. if (is_null($recipients)) {
  1144. $recipients = $this->getReplies();
  1145. }
  1146. $ni = array();
  1147. // Give plugins a chance to add folks in at start...
  1148. if (Event::handle('StartNoticeWhoGets', array($this, &$ni))) {
  1149. $users = $this->getSubscribedUsers();
  1150. foreach ($users as $id) {
  1151. $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
  1152. }
  1153. if (is_null($groups)) {
  1154. $groups = $this->getGroups();
  1155. }
  1156. foreach ($groups as $group) {
  1157. $users = $group->getUserMembers();
  1158. foreach ($users as $id) {
  1159. if (!array_key_exists($id, $ni)) {
  1160. $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
  1161. }
  1162. }
  1163. }
  1164. $ptAtts = $this->getAttentionsFromProfileTags();
  1165. foreach ($ptAtts as $key=>$val) {
  1166. if (!array_key_exists($key, $ni)) {
  1167. $ni[$key] = $val;
  1168. }
  1169. }
  1170. foreach ($recipients as $recipient) {
  1171. if (!array_key_exists($recipient, $ni)) {
  1172. $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
  1173. }
  1174. }
  1175. // Exclude any deleted, non-local, or blocking recipients.
  1176. $profile = $this->getProfile();
  1177. $originalProfile = null;
  1178. if ($this->isRepeat()) {
  1179. // Check blocks against the original notice's poster as well.
  1180. $original = Notice::getKV('id', $this->repeat_of);
  1181. if ($original instanceof Notice) {
  1182. $originalProfile = $original->getProfile();
  1183. }
  1184. }
  1185. foreach ($ni as $id => $source) {
  1186. try {
  1187. $user = User::getKV('id', $id);
  1188. if (!$user instanceof User ||
  1189. $user->hasBlocked($profile) ||
  1190. ($originalProfile && $user->hasBlocked($originalProfile))) {
  1191. unset($ni[$id]);
  1192. }
  1193. } catch (UserNoProfileException $e) {
  1194. // User doesn't have a profile; invalid; skip them.
  1195. unset($ni[$id]);
  1196. }
  1197. }
  1198. // Give plugins a chance to filter out...
  1199. Event::handle('EndNoticeWhoGets', array($this, &$ni));
  1200. }
  1201. if (!empty($c)) {
  1202. // XXX: pack this data better
  1203. $c->set(Cache::key('notice:who_gets:'.$this->id), $ni);
  1204. }
  1205. return $ni;
  1206. }
  1207. function getSubscribedUsers()
  1208. {
  1209. $user = new User();
  1210. if(common_config('db','quote_identifiers'))
  1211. $user_table = '"user"';
  1212. else $user_table = 'user';
  1213. $qry =
  1214. 'SELECT id ' .
  1215. 'FROM '. $user_table .' JOIN subscription '.
  1216. 'ON '. $user_table .'.id = subscription.subscriber ' .
  1217. 'WHERE subscription.subscribed = %d ';
  1218. $user->query(sprintf($qry, $this->profile_id));
  1219. $ids = array();
  1220. while ($user->fetch()) {
  1221. $ids[] = $user->id;
  1222. }
  1223. $user->free();
  1224. return $ids;
  1225. }
  1226. function getProfileTags()
  1227. {
  1228. $profile = $this->getProfile();
  1229. $list = $profile->getOtherTags($profile);
  1230. $ptags = array();
  1231. while($list->fetch()) {
  1232. $ptags[] = clone($list);
  1233. }
  1234. return $ptags;
  1235. }
  1236. public function getAttentionsFromProfileTags()
  1237. {
  1238. $ni = array();
  1239. $ptags = $this->getProfileTags();
  1240. foreach ($ptags as $ptag) {
  1241. $users = $ptag->getUserSubscribers();
  1242. foreach ($users as $id) {
  1243. $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG;
  1244. }
  1245. }
  1246. return $ni;
  1247. }
  1248. /**
  1249. * Record this notice to the given group inboxes for delivery.
  1250. * Overrides the regular parsing of !group markup.
  1251. *
  1252. * @param string $group_ids
  1253. * @fixme might prefer URIs as identifiers, as for replies?
  1254. * best with generalizations on user_group to support
  1255. * remote groups better.
  1256. */
  1257. function saveKnownGroups($group_ids)
  1258. {
  1259. if (!is_array($group_ids)) {
  1260. // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups().
  1261. throw new ServerException(_('Bad type provided to saveKnownGroups.'));
  1262. }
  1263. $groups = array();
  1264. foreach (array_unique($group_ids) as $id) {
  1265. $group = User_group::getKV('id', $id);
  1266. if ($group instanceof User_group) {
  1267. common_log(LOG_DEBUG, "Local delivery to group id $id, $group->nickname");
  1268. $result = $this->addToGroupInbox($group);
  1269. if (!$result) {
  1270. common_log_db_error($gi, 'INSERT', __FILE__);
  1271. }
  1272. if (common_config('group', 'addtag')) {
  1273. // we automatically add a tag for every group name, too
  1274. $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($group->nickname),
  1275. 'notice_id' => $this->id));
  1276. if (is_null($tag)) {
  1277. $this->saveTag($group->nickname);
  1278. }
  1279. }
  1280. $groups[] = clone($group);
  1281. } else {
  1282. common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist");
  1283. }
  1284. }
  1285. return $groups;
  1286. }
  1287. function addToGroupInbox(User_group $group)
  1288. {
  1289. $gi = Group_inbox::pkeyGet(array('group_id' => $group->id,
  1290. 'notice_id' => $this->id));
  1291. if (!$gi instanceof Group_inbox) {
  1292. $gi = new Group_inbox();
  1293. $gi->group_id = $group->id;
  1294. $gi->notice_id = $this->id;
  1295. $gi->created = $this->created;
  1296. $result = $gi->insert();
  1297. if (!$result) {
  1298. common_log_db_error($gi, 'INSERT', __FILE__);
  1299. // TRANS: Server exception thrown when an update for a group inbox fails.
  1300. throw new ServerException(_('Problem saving group inbox.'));
  1301. }
  1302. self::blow('user_group:notice_ids:%d', $gi->group_id);
  1303. }
  1304. return true;
  1305. }
  1306. /**
  1307. * Save reply records indicating that this notice needs to be
  1308. * delivered to the local users with the given URIs.
  1309. *
  1310. * Since this is expected to be used when saving foreign-sourced
  1311. * messages, we won't deliver to any remote targets as that's the
  1312. * source service's responsibility.
  1313. *
  1314. * Mail notifications etc will be handled later.
  1315. *
  1316. * @param array $uris Array of unique identifier URIs for recipients
  1317. */
  1318. function saveKnownReplies(array $uris)
  1319. {
  1320. if (empty($uris)) {
  1321. return;
  1322. }
  1323. $sender = Profile::getKV($this->profile_id);
  1324. foreach (array_unique($uris) as $uri) {
  1325. try {
  1326. $profile = Profile::fromUri($uri);
  1327. } catch (UnknownUriException $e) {
  1328. common_log(LOG_WARNING, "Unable to determine profile for URI '$uri'");
  1329. continue;
  1330. }
  1331. if ($profile->hasBlocked($sender)) {
  1332. common_log(LOG_INFO, "Not saving reply to profile {$profile->id} ($uri) from sender {$sender->id} because of a block.");
  1333. continue;
  1334. }
  1335. $this->saveReply($profile->id);
  1336. self::blow('reply:stream:%d', $profile->id);
  1337. }
  1338. return;
  1339. }
  1340. /**
  1341. * Pull @-replies from this message's content in StatusNet markup format
  1342. * and save reply records indicating that this message needs to be
  1343. * delivered to those users.
  1344. *
  1345. * Mail notifications to local profiles will be sent later.
  1346. *
  1347. * @return array of integer profile IDs
  1348. */
  1349. function saveReplies()
  1350. {
  1351. // Don't save reply data for repeats
  1352. if ($this->isRepeat()) {
  1353. return array();
  1354. }
  1355. $sender = $this->getProfile();
  1356. $replied = array();
  1357. // If it's a reply, save for the replied-to author
  1358. try {
  1359. $parent = $this->getParent();
  1360. $parentauthor = $parent->getProfile();
  1361. $this->saveReply($parentauthor->id);
  1362. $replied[$parentauthor->id] = 1;
  1363. self::blow('reply:stream:%d', $parentauthor->id);
  1364. } catch (Exception $e) {
  1365. // Not a reply, since it has no parent!
  1366. }
  1367. // @todo ideally this parser information would only
  1368. // be calculated once.
  1369. $mentions = common_find_mentions($this->content, $this);
  1370. // store replied only for first @ (what user/notice what the reply directed,
  1371. // we assume first @ is it)
  1372. foreach ($mentions as $mention) {
  1373. foreach ($mention['mentioned'] as $mentioned) {
  1374. // skip if they're already covered
  1375. if (!empty($replied[$mentioned->id])) {
  1376. continue;
  1377. }
  1378. // Don't save replies from blocked profile to local user
  1379. $mentioned_user = User::getKV('id', $mentioned->id);
  1380. if ($mentioned_user instanceof User && $mentioned_user->hasBlocked($sender)) {
  1381. continue;
  1382. }
  1383. $this->saveReply($mentioned->id);
  1384. $replied[$mentioned->id] = 1;
  1385. self::blow('reply:stream:%d', $mentioned->id);
  1386. }
  1387. }
  1388. $recipientIds = array_keys($replied);
  1389. return $recipientIds;
  1390. }
  1391. function saveReply($profileId)
  1392. {
  1393. $reply = new Reply();
  1394. $reply->notice_id = $this->id;
  1395. $reply->profile_id = $profileId;
  1396. $reply->modified = $this->created;
  1397. $reply->insert();
  1398. return $reply;
  1399. }
  1400. protected $_replies = array();
  1401. /**
  1402. * Pull the complete list of @-reply targets for this notice.
  1403. *
  1404. * @return array of integer profile ids
  1405. */
  1406. function getReplies()
  1407. {
  1408. if (isset($this->_replies[$this->id])) {
  1409. return $this->_replies[$this->id];
  1410. }
  1411. $replyMap = Reply::listGet('notice_id', array($this->id));
  1412. $ids = array();
  1413. foreach ($replyMap[$this->id] as $reply) {
  1414. $ids[] = $reply->profile_id;
  1415. }
  1416. $this->_replies[$this->id] = $ids;
  1417. return $ids;
  1418. }
  1419. function _setReplies($replies)
  1420. {
  1421. $this->_replies[$this->id] = $replies;
  1422. }
  1423. /**
  1424. * Pull the complete list of @-reply targets for this notice.
  1425. *
  1426. * @return array of Profiles
  1427. */
  1428. function getReplyProfiles()
  1429. {
  1430. $ids = $this->getReplies();
  1431. $profiles = Profile::multiGet('id', $ids);
  1432. return $profiles->fetchAll();
  1433. }
  1434. /**
  1435. * Send e-mail notifications to local @-reply targets.
  1436. *
  1437. * Replies must already have been saved; this is expected to be run
  1438. * from the distrib queue handler.
  1439. */
  1440. function sendReplyNotifications()
  1441. {
  1442. // Don't send reply notifications for repeats
  1443. if ($this->isRepeat()) {
  1444. return array();
  1445. }
  1446. $recipientIds = $this->getReplies();
  1447. if (Event::handle('StartNotifyMentioned', array($this, &$recipientIds))) {
  1448. require_once INSTALLDIR.'/lib/mail.php';
  1449. foreach ($recipientIds as $recipientId) {
  1450. $user = User::getKV('id', $recipientId);
  1451. if ($user instanceof User) {
  1452. mail_notify_attn($user, $this);
  1453. }
  1454. }
  1455. Event::handle('EndNotifyMentioned', array($this, $recipientIds));
  1456. }
  1457. }
  1458. /**
  1459. * Pull list of groups this notice needs to be delivered to,
  1460. * as previously recorded by saveKnownGroups().
  1461. *
  1462. * @return array of Group objects
  1463. */
  1464. protected $_groups = array();
  1465. function getGroups()
  1466. {
  1467. // Don't save groups for repeats
  1468. if (!empty($this->repeat_of)) {
  1469. return array();
  1470. }
  1471. if (isset($this->_groups[$this->id])) {
  1472. return $this->_groups[$this->id];
  1473. }
  1474. $gis = Group_inbox::listGet('notice_id', array($this->id));
  1475. $ids = array();
  1476. foreach ($gis[$this->id] as $gi)
  1477. {
  1478. $ids[] = $gi->group_id;
  1479. }
  1480. $groups = User_group::multiGet('id', $ids);
  1481. $this->_groups[$this->id] = $groups->fetchAll();
  1482. return $this->_groups[$this->id];
  1483. }
  1484. function _setGroups($groups)
  1485. {
  1486. $this->_groups[$this->id] = $groups;
  1487. }
  1488. /**
  1489. * Convert a notice into an activity for export.
  1490. *
  1491. * @param Profile $scoped The currently logged in/scoped profile
  1492. *
  1493. * @return Activity activity object representing this Notice.
  1494. */
  1495. function asActivity(Profile $scoped=null)
  1496. {
  1497. $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id));
  1498. if ($act instanceof Activity) {
  1499. return $act;
  1500. }
  1501. $act = new Activity();
  1502. if (Event::handle('StartNoticeAsActivity', array($this, $act, $scoped))) {
  1503. $act->id = $this->uri;
  1504. $act->time = strtotime($this->created);
  1505. try {
  1506. $act->link = $this->getUrl();
  1507. } catch (InvalidUrlException $e) {
  1508. // The notice is probably a share or similar, which don't
  1509. // have a representational URL of their own.
  1510. }
  1511. $act->content = common_xml_safe_str($this->rendered);
  1512. $profile = $this->getProfile();
  1513. $act->actor = $profile->asActivityObject();
  1514. $act->actor->extra[] = $profile->profileInfo($scoped);
  1515. $act->verb = $this->verb;
  1516. if ($this->repeat_of) {
  1517. $repeated = Notice::getKV('id', $this->repeat_of);
  1518. if ($repeated instanceof Notice) {
  1519. // TRANS: A repeat activity's title. %1$s is repeater's nickname
  1520. // and %2$s is the repeated user's nickname.
  1521. $act->title = sprintf(_('%1$s repeated a notice by %2$s'),
  1522. $this->getProfile()->getNickname(),
  1523. $repeated->getProfile()->getNickname());
  1524. $act->objects[] = $repeated->asActivity($scoped);
  1525. }
  1526. } else {
  1527. $act->objects[] = $this->asActivityObject();
  1528. }
  1529. // XXX: should this be handled by default processing for object entry?
  1530. // Categories
  1531. $tags = $this->getTags();
  1532. foreach ($tags as $tag) {
  1533. $cat = new AtomCategory();
  1534. $cat->term = $tag;
  1535. $act->categories[] = $cat;
  1536. }
  1537. // Enclosures
  1538. // XXX: use Atom Media and/or File activity objects instead
  1539. $attachments = $this->attachments();
  1540. foreach ($attachments as $attachment) {
  1541. // Save local attachments
  1542. if (!empty($attachment->filename)) {
  1543. $act->attachments[] = ActivityObject::fromFile($attachment);
  1544. }
  1545. }
  1546. $ctx = new ActivityContext();
  1547. try {
  1548. $reply = $this->getParent();
  1549. $ctx->replyToID = $reply->getUri();
  1550. $ctx->replyToUrl = $reply->getUrl();
  1551. } catch (Exception $e) {
  1552. // This is not a reply to something
  1553. }
  1554. $ctx->location = $this->getLocation();
  1555. $conv = null;
  1556. if (!empty($this->conversation)) {
  1557. $conv = Conversation::getKV('id', $this->conversation);
  1558. if ($conv instanceof Conversation) {
  1559. $ctx->conversation = $conv->uri;
  1560. }
  1561. }
  1562. $reply_ids = $this->getReplies();
  1563. foreach ($reply_ids as $id) {
  1564. $rprofile = Profile::getKV('id', $id);
  1565. if ($rprofile instanceof Profile) {
  1566. $ctx->attention[$rprofile->getUri()] = ActivityObject::PERSON;
  1567. }
  1568. }
  1569. $groups = $this->getGroups();
  1570. foreach ($groups as $group) {
  1571. $ctx->attention[$group->getUri()] = ActivityObject::GROUP;
  1572. }
  1573. switch ($this->scope) {
  1574. case Notice::PUBLIC_SCOPE:
  1575. $ctx->attention[ActivityContext::ATTN_PUBLIC] = ActivityObject::COLLECTION;
  1576. break;
  1577. case Notice::FOLLOWER_SCOPE:
  1578. $surl = common_local_url("subscribers", array('nickname' => $profile->nickname));
  1579. $ctx->attention[$surl] = ActivityObject::COLLECTION;
  1580. break;
  1581. }
  1582. $act->context = $ctx;
  1583. $source = $this->getSource();
  1584. if ($source instanceof Notice_source) {
  1585. $act->generator = ActivityObject::fromNoticeSource($source);
  1586. }
  1587. // Source
  1588. $atom_feed = $profile->getAtomFeed();
  1589. if (!empty($atom_feed)) {
  1590. $act->source = new ActivitySource();
  1591. // XXX: we should store the actual feed ID
  1592. $act->source->id = $atom_feed;
  1593. // XXX: we should store the actual feed title
  1594. $act->source->title = $profile->getBestName();
  1595. $act->source->links['alternate'] = $profile->profileurl;
  1596. $act->source->links['self'] = $atom_feed;
  1597. $act->source->icon = $profile->avatarUrl(AVATAR_PROFILE_SIZE);
  1598. $notice = $profile->getCurrentNotice();
  1599. if ($notice instanceof Notice) {
  1600. $act->source->updated = self::utcDate($notice->created);
  1601. }
  1602. $user = User::getKV('id', $profile->id);
  1603. if ($user instanceof User) {
  1604. $act->source->links['license'] = common_config('license', 'url');
  1605. }
  1606. }
  1607. if ($this->isLocal()) {
  1608. $act->selfLink = common_local_url('ApiStatusesShow', array('id' => $this->id,
  1609. 'format' => 'atom'));
  1610. $act->editLink = $act->selfLink;
  1611. }
  1612. Event::handle('EndNoticeAsActivity', array($this, $act, $scoped));
  1613. }
  1614. self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act);
  1615. return $act;
  1616. }
  1617. // This has gotten way too long. Needs to be sliced up into functional bits
  1618. // or ideally exported to a utility class.
  1619. function asAtomEntry($namespace=false,
  1620. $source=false,
  1621. $author=true,
  1622. Profile $scoped=null)
  1623. {
  1624. $act = $this->asActivity($scoped);
  1625. $act->extra[] = $this->noticeInfo($scoped);
  1626. return $act->asString($namespace, $author, $source);
  1627. }
  1628. /**
  1629. * Extra notice info for atom entries
  1630. *
  1631. * Clients use some extra notice info in the atom stream.
  1632. * This gives it to them.
  1633. *
  1634. * @param Profile $scoped The currently logged in/scoped profile
  1635. *
  1636. * @return array representation of <statusnet:notice_info> element
  1637. */
  1638. function noticeInfo(Profile $scoped=null)
  1639. {
  1640. // local notice ID (useful to clients for ordering)
  1641. $noticeInfoAttr = array('local_id' => $this->id);
  1642. // notice source
  1643. $ns = $this->getSource();
  1644. if ($ns instanceof Notice_source) {
  1645. $noticeInfoAttr['source'] = $ns->code;
  1646. if (!empty($ns->url)) {
  1647. $noticeInfoAttr['source_link'] = $ns->url;
  1648. if (!empty($ns->name)) {
  1649. $noticeInfoAttr['source'] = '<a href="'
  1650. . htmlspecialchars($ns->url)
  1651. . '" rel="nofollow">'
  1652. . htmlspecialchars($ns->name)
  1653. . '</a>';
  1654. }
  1655. }
  1656. }
  1657. // favorite and repeated
  1658. if ($scoped instanceof Profile) {
  1659. $noticeInfoAttr['repeated'] = ($scoped->hasRepeated($this)) ? "true" : "false";
  1660. }
  1661. if (!empty($this->repeat_of)) {
  1662. $noticeInfoAttr['repeat_of'] = $this->repeat_of;
  1663. }
  1664. Event::handle('StatusNetApiNoticeInfo', array($this, &$noticeInfoAttr, $scoped));
  1665. return array('statusnet:notice_info', $noticeInfoAttr, null);
  1666. }
  1667. /**
  1668. * Returns an XML string fragment with a reference to a notice as an
  1669. * Activity Streams noun object with the given element type.
  1670. *
  1671. * Assumes that 'activity' namespace has been previously defined.
  1672. *
  1673. * @param string $element one of 'subject', 'object', 'target'
  1674. * @return string
  1675. */
  1676. function asActivityNoun($element)
  1677. {
  1678. $noun = $this->asActivityObject();
  1679. return $noun->asString('activity:' . $element);
  1680. }
  1681. public function asActivityObject()
  1682. {
  1683. $object = new ActivityObject();
  1684. if (Event::handle('StartActivityObjectFromNotice', array($this, &$object))) {
  1685. $object->type = $this->object_type ?: ActivityObject::NOTE;
  1686. $object->id = $this->getUri();
  1687. $object->title = sprintf('New %1$s by %2$s', ActivityObject::canonicalType($object->type), $this->getProfile()->getNickname());
  1688. $object->content = $this->rendered;
  1689. $object->link = $this->getUrl();
  1690. $object->extra[] = array('status_net', array('notice_id' => $this->id));
  1691. Event::handle('EndActivityObjectFromNotice', array($this, &$object));
  1692. }
  1693. return $object;
  1694. }
  1695. /**
  1696. * Determine which notice, if any, a new notice is in reply to.
  1697. *
  1698. * For conversation tracking, we try to see where this notice fits
  1699. * in the tree. Beware that this may very well give false positives
  1700. * and add replies to wrong threads (if there have been newer posts
  1701. * by the same user as we're replying to).
  1702. *
  1703. * @param Profile $sender Author profile
  1704. * @param string $content Final notice content
  1705. *
  1706. * @return integer ID of replied-to notice, or null for not a reply.
  1707. */
  1708. static function getInlineReplyTo(Profile $sender, $content)
  1709. {
  1710. // Is there an initial @ or T?
  1711. if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match)
  1712. || preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) {
  1713. $nickname = common_canonical_nickname($match[1]);
  1714. } else {
  1715. return null;
  1716. }
  1717. // Figure out who that is.
  1718. $recipient = common_relative_profile($sender, $nickname, common_sql_now());
  1719. if ($recipient instanceof Profile) {
  1720. // Get their last notice
  1721. $last = $recipient->getCurrentNotice();
  1722. if ($last instanceof Notice) {
  1723. return $last;
  1724. }
  1725. // Maybe in the future we want to handle something else below
  1726. // so don't return getCurrentNotice() immediately.
  1727. }
  1728. return null;
  1729. }
  1730. static function maxContent()
  1731. {
  1732. $contentlimit = common_config('notice', 'contentlimit');
  1733. // null => use global limit (distinct from 0!)
  1734. if (is_null($contentlimit)) {
  1735. $contentlimit = common_config('site', 'textlimit');
  1736. }
  1737. return $contentlimit;
  1738. }
  1739. static function contentTooLong($content)
  1740. {
  1741. $contentlimit = self::maxContent();
  1742. return ($contentlimit > 0 && !empty($content) && (mb_strlen($content) > $contentlimit));
  1743. }
  1744. function getLocation()
  1745. {
  1746. $location = null;
  1747. if (!empty($this->location_id) && !empty($this->location_ns)) {
  1748. $location = Location::fromId($this->location_id, $this->location_ns);
  1749. }
  1750. if (is_null($location)) { // no ID, or Location::fromId() failed
  1751. if (!empty($this->lat) && !empty($this->lon)) {
  1752. $location = Location::fromLatLon($this->lat, $this->lon);
  1753. }
  1754. }
  1755. return $location;
  1756. }
  1757. /**
  1758. * Convenience function for posting a repeat of an existing message.
  1759. *
  1760. * @param Profile $repeater Profile which is doing the repeat
  1761. * @param string $source: posting source key, eg 'web', 'api', etc
  1762. * @return Notice
  1763. *
  1764. * @throws Exception on failure or permission problems
  1765. */
  1766. function repeat(Profile $repeater, $source)
  1767. {
  1768. $author = $this->getProfile();
  1769. // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'.
  1770. // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice.
  1771. $content = sprintf(_('RT @%1$s %2$s'),
  1772. $author->getNickname(),
  1773. $this->content);
  1774. $maxlen = self::maxContent();
  1775. if ($maxlen > 0 && mb_strlen($content) > $maxlen) {
  1776. // Web interface and current Twitter API clients will
  1777. // pull the original notice's text, but some older
  1778. // clients and RSS/Atom feeds will see this trimmed text.
  1779. //
  1780. // Unfortunately this is likely to lose tags or URLs
  1781. // at the end of long notices.
  1782. $content = mb_substr($content, 0, $maxlen - 4) . ' ...';
  1783. }
  1784. // Scope is same as this one's
  1785. return self::saveNew($repeater->id,
  1786. $content,
  1787. $source,
  1788. array('repeat_of' => $this->id,
  1789. 'scope' => $this->scope));
  1790. }
  1791. // These are supposed to be in chron order!
  1792. function repeatStream($limit=100)
  1793. {
  1794. $cache = Cache::instance();
  1795. if (empty($cache)) {
  1796. $ids = $this->_repeatStreamDirect($limit);
  1797. } else {
  1798. $idstr = $cache->get(Cache::key('notice:repeats:'.$this->id));
  1799. if ($idstr !== false) {
  1800. if (empty($idstr)) {
  1801. $ids = array();
  1802. } else {
  1803. $ids = explode(',', $idstr);
  1804. }
  1805. } else {
  1806. $ids = $this->_repeatStreamDirect(100);
  1807. $cache->set(Cache::key('notice:repeats:'.$this->id), implode(',', $ids));
  1808. }
  1809. if ($limit < 100) {
  1810. // We do a max of 100, so slice down to limit
  1811. $ids = array_slice($ids, 0, $limit);
  1812. }
  1813. }
  1814. return NoticeStream::getStreamByIds($ids);
  1815. }
  1816. function _repeatStreamDirect($limit)
  1817. {
  1818. $notice = new Notice();
  1819. $notice->selectAdd(); // clears it
  1820. $notice->selectAdd('id');
  1821. $notice->repeat_of = $this->id;
  1822. $notice->orderBy('created, id'); // NB: asc!
  1823. if (!is_null($limit)) {
  1824. $notice->limit(0, $limit);
  1825. }
  1826. return $notice->fetchAll('id');
  1827. }
  1828. function locationOptions($lat, $lon, $location_id, $location_ns, $profile = null)
  1829. {
  1830. $options = array();
  1831. if (!empty($location_id) && !empty($location_ns)) {
  1832. $options['location_id'] = $location_id;
  1833. $options['location_ns'] = $location_ns;
  1834. $location = Location::fromId($location_id, $location_ns);
  1835. if ($location instanceof Location) {
  1836. $options['lat'] = $location->lat;
  1837. $options['lon'] = $location->lon;
  1838. }
  1839. } else if (!empty($lat) && !empty($lon)) {
  1840. $options['lat'] = $lat;
  1841. $options['lon'] = $lon;
  1842. $location = Location::fromLatLon($lat, $lon);
  1843. if ($location instanceof Location) {
  1844. $options['location_id'] = $location->location_id;
  1845. $options['location_ns'] = $location->location_ns;
  1846. }
  1847. } else if (!empty($profile)) {
  1848. if (isset($profile->lat) && isset($profile->lon)) {
  1849. $options['lat'] = $profile->lat;
  1850. $options['lon'] = $profile->lon;
  1851. }
  1852. if (isset($profile->location_id) && isset($profile->location_ns)) {
  1853. $options['location_id'] = $profile->location_id;
  1854. $options['location_ns'] = $profile->location_ns;
  1855. }
  1856. }
  1857. return $options;
  1858. }
  1859. function clearAttentions()
  1860. {
  1861. $att = new Attention();
  1862. $att->notice_id = $this->getID();
  1863. if ($att->find()) {
  1864. while ($att->fetch()) {
  1865. // Can't do delete() on the object directly since it won't remove all of it
  1866. $other = clone($att);
  1867. $other->delete();
  1868. }
  1869. }
  1870. }
  1871. function clearReplies()
  1872. {
  1873. $replyNotice = new Notice();
  1874. $replyNotice->reply_to = $this->id;
  1875. //Null any notices that are replies to this notice
  1876. if ($replyNotice->find()) {
  1877. while ($replyNotice->fetch()) {
  1878. $orig = clone($replyNotice);
  1879. $replyNotice->reply_to = null;
  1880. $replyNotice->update($orig);
  1881. }
  1882. }
  1883. // Reply records
  1884. $reply = new Reply();
  1885. $reply->notice_id = $this->id;
  1886. if ($reply->find()) {
  1887. while($reply->fetch()) {
  1888. self::blow('reply:stream:%d', $reply->profile_id);
  1889. $reply->delete();
  1890. }
  1891. }
  1892. $reply->free();
  1893. }
  1894. function clearFiles()
  1895. {
  1896. $f2p = new File_to_post();
  1897. $f2p->post_id = $this->id;
  1898. if ($f2p->find()) {
  1899. while ($f2p->fetch()) {
  1900. $f2p->delete();
  1901. }
  1902. }
  1903. // FIXME: decide whether to delete File objects
  1904. // ...and related (actual) files
  1905. }
  1906. function clearRepeats()
  1907. {
  1908. $repeatNotice = new Notice();
  1909. $repeatNotice->repeat_of = $this->id;
  1910. //Null any notices that are repeats of this notice
  1911. if ($repeatNotice->find()) {
  1912. while ($repeatNotice->fetch()) {
  1913. $orig = clone($repeatNotice);
  1914. $repeatNotice->repeat_of = null;
  1915. $repeatNotice->update($orig);
  1916. }
  1917. }
  1918. }
  1919. function clearTags()
  1920. {
  1921. $tag = new Notice_tag();
  1922. $tag->notice_id = $this->id;
  1923. if ($tag->find()) {
  1924. while ($tag->fetch()) {
  1925. self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, Cache::keyize($tag->tag));
  1926. self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, Cache::keyize($tag->tag));
  1927. self::blow('notice_tag:notice_ids:%s', Cache::keyize($tag->tag));
  1928. self::blow('notice_tag:notice_ids:%s;last', Cache::keyize($tag->tag));
  1929. $tag->delete();
  1930. }
  1931. }
  1932. $tag->free();
  1933. }
  1934. function clearGroupInboxes()
  1935. {
  1936. $gi = new Group_inbox();
  1937. $gi->notice_id = $this->id;
  1938. if ($gi->find()) {
  1939. while ($gi->fetch()) {
  1940. self::blow('user_group:notice_ids:%d', $gi->group_id);
  1941. $gi->delete();
  1942. }
  1943. }
  1944. $gi->free();
  1945. }
  1946. function distribute()
  1947. {
  1948. // We always insert for the author so they don't
  1949. // have to wait
  1950. Event::handle('StartNoticeDistribute', array($this));
  1951. // If there's a failure, we want to _force_
  1952. // distribution at this point.
  1953. try {
  1954. $qm = QueueManager::get();
  1955. $qm->enqueue($this, 'distrib');
  1956. } catch (Exception $e) {
  1957. // If the exception isn't transient, this
  1958. // may throw more exceptions as DQH does
  1959. // its own enqueueing. So, we ignore them!
  1960. try {
  1961. $handler = new DistribQueueHandler();
  1962. $handler->handle($this);
  1963. } catch (Exception $e) {
  1964. common_log(LOG_ERR, "emergency redistribution resulted in " . $e->getMessage());
  1965. }
  1966. // Re-throw so somebody smarter can handle it.
  1967. throw $e;
  1968. }
  1969. }
  1970. function insert()
  1971. {
  1972. $result = parent::insert();
  1973. if ($result === false) {
  1974. common_log_db_error($this, 'INSERT', __FILE__);
  1975. // TRANS: Server exception thrown when a stored object entry cannot be saved.
  1976. throw new ServerException('Could not save Notice');
  1977. }
  1978. // Profile::hasRepeated() abuses pkeyGet(), so we
  1979. // have to clear manually
  1980. if (!empty($this->repeat_of)) {
  1981. $c = self::memcache();
  1982. if (!empty($c)) {
  1983. $ck = self::multicacheKey('Notice',
  1984. array('profile_id' => $this->profile_id,
  1985. 'repeat_of' => $this->repeat_of));
  1986. $c->delete($ck);
  1987. }
  1988. }
  1989. // Update possibly ID-dependent columns: URI, conversation
  1990. // (now that INSERT has added the notice's local id)
  1991. $orig = clone($this);
  1992. $changed = false;
  1993. // We can only get here if it's a local notice, since remote notices
  1994. // should've bailed out earlier due to lacking a URI.
  1995. if (empty($this->uri)) {
  1996. $this->uri = sprintf('%s%s=%d:%s=%s',
  1997. TagURI::mint(),
  1998. 'noticeId', $this->id,
  1999. 'objectType', $this->get_object_type(true));
  2000. $changed = true;
  2001. }
  2002. if ($changed && $this->update($orig) === false) {
  2003. common_log_db_error($notice, 'UPDATE', __FILE__);
  2004. // TRANS: Server exception thrown when a notice cannot be updated.
  2005. throw new ServerException(_('Problem saving notice.'));
  2006. }
  2007. $this->blowOnInsert();
  2008. return $result;
  2009. }
  2010. /**
  2011. * Get the source of the notice
  2012. *
  2013. * @return Notice_source $ns A notice source object. 'code' is the only attribute
  2014. * guaranteed to be populated.
  2015. */
  2016. function getSource()
  2017. {
  2018. if (empty($this->source)) {
  2019. return false;
  2020. }
  2021. $ns = new Notice_source();
  2022. switch ($this->source) {
  2023. case 'web':
  2024. case 'xmpp':
  2025. case 'mail':
  2026. case 'omb':
  2027. case 'system':
  2028. case 'api':
  2029. $ns->code = $this->source;
  2030. break;
  2031. default:
  2032. $ns = Notice_source::getKV($this->source);
  2033. if (!$ns) {
  2034. $ns = new Notice_source();
  2035. $ns->code = $this->source;
  2036. $app = Oauth_application::getKV('name', $this->source);
  2037. if ($app) {
  2038. $ns->name = $app->name;
  2039. $ns->url = $app->source_url;
  2040. }
  2041. }
  2042. break;
  2043. }
  2044. return $ns;
  2045. }
  2046. /**
  2047. * Determine whether the notice was locally created
  2048. *
  2049. * @return boolean locality
  2050. */
  2051. public function isLocal()
  2052. {
  2053. return ($this->is_local == Notice::LOCAL_PUBLIC ||
  2054. $this->is_local == Notice::LOCAL_NONPUBLIC);
  2055. }
  2056. public function isRepeat()
  2057. {
  2058. return !empty($this->repeat_of);
  2059. }
  2060. /**
  2061. * Get the list of hash tags saved with this notice.
  2062. *
  2063. * @return array of strings
  2064. */
  2065. public function getTags()
  2066. {
  2067. $tags = array();
  2068. $keypart = sprintf('notice:tags:%d', $this->id);
  2069. $tagstr = self::cacheGet($keypart);
  2070. if ($tagstr !== false) {
  2071. $tags = explode(',', $tagstr);
  2072. } else {
  2073. $tag = new Notice_tag();
  2074. $tag->notice_id = $this->id;
  2075. if ($tag->find()) {
  2076. while ($tag->fetch()) {
  2077. $tags[] = $tag->tag;
  2078. }
  2079. }
  2080. self::cacheSet($keypart, implode(',', $tags));
  2081. }
  2082. return $tags;
  2083. }
  2084. static private function utcDate($dt)
  2085. {
  2086. $dateStr = date('d F Y H:i:s', strtotime($dt));
  2087. $d = new DateTime($dateStr, new DateTimeZone('UTC'));
  2088. return $d->format(DATE_W3C);
  2089. }
  2090. /**
  2091. * Look up the creation timestamp for a given notice ID, even
  2092. * if it's been deleted.
  2093. *
  2094. * @param int $id
  2095. * @return mixed string recorded creation timestamp, or false if can't be found
  2096. */
  2097. public static function getAsTimestamp($id)
  2098. {
  2099. if (!$id) {
  2100. return false;
  2101. }
  2102. $notice = Notice::getKV('id', $id);
  2103. if ($notice) {
  2104. return $notice->created;
  2105. }
  2106. $deleted = Deleted_notice::getKV('id', $id);
  2107. if ($deleted) {
  2108. return $deleted->created;
  2109. }
  2110. return false;
  2111. }
  2112. /**
  2113. * Build an SQL 'where' fragment for timestamp-based sorting from a since_id
  2114. * parameter, matching notices posted after the given one (exclusive).
  2115. *
  2116. * If the referenced notice can't be found, will return false.
  2117. *
  2118. * @param int $id
  2119. * @param string $idField
  2120. * @param string $createdField
  2121. * @return mixed string or false if no match
  2122. */
  2123. public static function whereSinceId($id, $idField='id', $createdField='created')
  2124. {
  2125. $since = Notice::getAsTimestamp($id);
  2126. if ($since) {
  2127. return sprintf("($createdField = '%s' and $idField > %d) or ($createdField > '%s')", $since, $id, $since);
  2128. }
  2129. return false;
  2130. }
  2131. /**
  2132. * Build an SQL 'where' fragment for timestamp-based sorting from a since_id
  2133. * parameter, matching notices posted after the given one (exclusive), and
  2134. * if necessary add it to the data object's query.
  2135. *
  2136. * @param DB_DataObject $obj
  2137. * @param int $id
  2138. * @param string $idField
  2139. * @param string $createdField
  2140. * @return mixed string or false if no match
  2141. */
  2142. public static function addWhereSinceId(DB_DataObject $obj, $id, $idField='id', $createdField='created')
  2143. {
  2144. $since = self::whereSinceId($id, $idField, $createdField);
  2145. if ($since) {
  2146. $obj->whereAdd($since);
  2147. }
  2148. }
  2149. /**
  2150. * Build an SQL 'where' fragment for timestamp-based sorting from a max_id
  2151. * parameter, matching notices posted before the given one (inclusive).
  2152. *
  2153. * If the referenced notice can't be found, will return false.
  2154. *
  2155. * @param int $id
  2156. * @param string $idField
  2157. * @param string $createdField
  2158. * @return mixed string or false if no match
  2159. */
  2160. public static function whereMaxId($id, $idField='id', $createdField='created')
  2161. {
  2162. $max = Notice::getAsTimestamp($id);
  2163. if ($max) {
  2164. return sprintf("($createdField < '%s') or ($createdField = '%s' and $idField <= %d)", $max, $max, $id);
  2165. }
  2166. return false;
  2167. }
  2168. /**
  2169. * Build an SQL 'where' fragment for timestamp-based sorting from a max_id
  2170. * parameter, matching notices posted before the given one (inclusive), and
  2171. * if necessary add it to the data object's query.
  2172. *
  2173. * @param DB_DataObject $obj
  2174. * @param int $id
  2175. * @param string $idField
  2176. * @param string $createdField
  2177. * @return mixed string or false if no match
  2178. */
  2179. public static function addWhereMaxId(DB_DataObject $obj, $id, $idField='id', $createdField='created')
  2180. {
  2181. $max = self::whereMaxId($id, $idField, $createdField);
  2182. if ($max) {
  2183. $obj->whereAdd($max);
  2184. }
  2185. }
  2186. function isPublic()
  2187. {
  2188. return (($this->is_local != Notice::LOCAL_NONPUBLIC) &&
  2189. ($this->is_local != Notice::GATEWAY));
  2190. }
  2191. /**
  2192. * Check that the given profile is allowed to read, respond to, or otherwise
  2193. * act on this notice.
  2194. *
  2195. * The $scope member is a bitmask of scopes, representing a logical AND of the
  2196. * scope requirement. So, 0x03 (Notice::ADDRESSEE_SCOPE | Notice::SITE_SCOPE) means
  2197. * "only visible to people who are mentioned in the notice AND are users on this site."
  2198. * Users on the site who are not mentioned in the notice will not be able to see the
  2199. * notice.
  2200. *
  2201. * @param Profile $profile The profile to check; pass null to check for public/unauthenticated users.
  2202. *
  2203. * @return boolean whether the profile is in the notice's scope
  2204. */
  2205. function inScope($profile)
  2206. {
  2207. if (is_null($profile)) {
  2208. $keypart = sprintf('notice:in-scope-for:%d:null', $this->id);
  2209. } else {
  2210. $keypart = sprintf('notice:in-scope-for:%d:%d', $this->id, $profile->id);
  2211. }
  2212. $result = self::cacheGet($keypart);
  2213. if ($result === false) {
  2214. $bResult = false;
  2215. if (Event::handle('StartNoticeInScope', array($this, $profile, &$bResult))) {
  2216. $bResult = $this->_inScope($profile);
  2217. Event::handle('EndNoticeInScope', array($this, $profile, &$bResult));
  2218. }
  2219. $result = ($bResult) ? 1 : 0;
  2220. self::cacheSet($keypart, $result, 0, 300);
  2221. }
  2222. return ($result == 1) ? true : false;
  2223. }
  2224. protected function _inScope($profile)
  2225. {
  2226. if (!is_null($this->scope)) {
  2227. $scope = $this->scope;
  2228. } else {
  2229. $scope = self::defaultScope();
  2230. }
  2231. // If there's no scope, anyone (even anon) is in scope.
  2232. if ($scope == 0) { // Not private
  2233. return !$this->isHiddenSpam($profile);
  2234. } else { // Private, somehow
  2235. // If there's scope, anon cannot be in scope
  2236. if (empty($profile)) {
  2237. return false;
  2238. }
  2239. // Author is always in scope
  2240. if ($this->profile_id == $profile->id) {
  2241. return true;
  2242. }
  2243. // Only for users on this site
  2244. if (($scope & Notice::SITE_SCOPE) && !$profile->isLocal()) {
  2245. return false;
  2246. }
  2247. // Only for users mentioned in the notice
  2248. if ($scope & Notice::ADDRESSEE_SCOPE) {
  2249. $reply = Reply::pkeyGet(array('notice_id' => $this->id,
  2250. 'profile_id' => $profile->id));
  2251. if (!$reply instanceof Reply) {
  2252. return false;
  2253. }
  2254. }
  2255. // Only for members of the given group
  2256. if ($scope & Notice::GROUP_SCOPE) {
  2257. // XXX: just query for the single membership
  2258. $groups = $this->getGroups();
  2259. $foundOne = false;
  2260. foreach ($groups as $group) {
  2261. if ($profile->isMember($group)) {
  2262. $foundOne = true;
  2263. break;
  2264. }
  2265. }
  2266. if (!$foundOne) {
  2267. return false;
  2268. }
  2269. }
  2270. // Only for followers of the author
  2271. $author = null;
  2272. if ($scope & Notice::FOLLOWER_SCOPE) {
  2273. try {
  2274. $author = $this->getProfile();
  2275. } catch (Exception $e) {
  2276. return false;
  2277. }
  2278. if (!Subscription::exists($profile, $author)) {
  2279. return false;
  2280. }
  2281. }
  2282. return !$this->isHiddenSpam($profile);
  2283. }
  2284. }
  2285. function isHiddenSpam($profile) {
  2286. // Hide posts by silenced users from everyone but moderators.
  2287. if (common_config('notice', 'hidespam')) {
  2288. try {
  2289. $author = $this->getProfile();
  2290. } catch(Exception $e) {
  2291. // If we can't get an author, keep it hidden.
  2292. // XXX: technically not spam, but, whatever.
  2293. return true;
  2294. }
  2295. if ($author->hasRole(Profile_role::SILENCED)) {
  2296. if (!$profile instanceof Profile || (($profile->id !== $author->id) && (!$profile->hasRight(Right::REVIEWSPAM)))) {
  2297. return true;
  2298. }
  2299. }
  2300. }
  2301. return false;
  2302. }
  2303. public function getParent()
  2304. {
  2305. $parent = Notice::getKV('id', $this->reply_to);
  2306. if (!$parent instanceof Notice) {
  2307. throw new ServerException('Notice has no parent');
  2308. }
  2309. return $parent;
  2310. }
  2311. /**
  2312. * Magic function called at serialize() time.
  2313. *
  2314. * We use this to drop a couple process-specific references
  2315. * from DB_DataObject which can cause trouble in future
  2316. * processes.
  2317. *
  2318. * @return array of variable names to include in serialization.
  2319. */
  2320. function __sleep()
  2321. {
  2322. $vars = parent::__sleep();
  2323. $skip = array('_profile', '_groups', '_attachments', '_faves', '_replies', '_repeats');
  2324. return array_diff($vars, $skip);
  2325. }
  2326. static function defaultScope()
  2327. {
  2328. $scope = common_config('notice', 'defaultscope');
  2329. if (is_null($scope)) {
  2330. if (common_config('site', 'private')) {
  2331. $scope = 1;
  2332. } else {
  2333. $scope = 0;
  2334. }
  2335. }
  2336. return $scope;
  2337. }
  2338. static function fillProfiles($notices)
  2339. {
  2340. $map = self::getProfiles($notices);
  2341. foreach ($notices as $entry=>$notice) {
  2342. try {
  2343. if (array_key_exists($notice->profile_id, $map)) {
  2344. $notice->_setProfile($map[$notice->profile_id]);
  2345. }
  2346. } catch (NoProfileException $e) {
  2347. common_log(LOG_WARNING, "Failed to fill profile in Notice with non-existing entry for profile_id: {$e->profile_id}");
  2348. unset($notices[$entry]);
  2349. }
  2350. }
  2351. return array_values($map);
  2352. }
  2353. static function getProfiles(&$notices)
  2354. {
  2355. $ids = array();
  2356. foreach ($notices as $notice) {
  2357. $ids[] = $notice->profile_id;
  2358. }
  2359. $ids = array_unique($ids);
  2360. return Profile::pivotGet('id', $ids);
  2361. }
  2362. static function fillGroups(&$notices)
  2363. {
  2364. $ids = self::_idsOf($notices);
  2365. $gis = Group_inbox::listGet('notice_id', $ids);
  2366. $gids = array();
  2367. foreach ($gis as $id => $gi)
  2368. {
  2369. foreach ($gi as $g)
  2370. {
  2371. $gids[] = $g->group_id;
  2372. }
  2373. }
  2374. $gids = array_unique($gids);
  2375. $group = User_group::pivotGet('id', $gids);
  2376. foreach ($notices as $notice)
  2377. {
  2378. $grps = array();
  2379. $gi = $gis[$notice->id];
  2380. foreach ($gi as $g) {
  2381. $grps[] = $group[$g->group_id];
  2382. }
  2383. $notice->_setGroups($grps);
  2384. }
  2385. }
  2386. static function _idsOf(array &$notices)
  2387. {
  2388. $ids = array();
  2389. foreach ($notices as $notice) {
  2390. $ids[$notice->id] = true;
  2391. }
  2392. return array_keys($ids);
  2393. }
  2394. static function fillAttachments(&$notices)
  2395. {
  2396. $ids = self::_idsOf($notices);
  2397. $f2pMap = File_to_post::listGet('post_id', $ids);
  2398. $fileIds = array();
  2399. foreach ($f2pMap as $noticeId => $f2ps) {
  2400. foreach ($f2ps as $f2p) {
  2401. $fileIds[] = $f2p->file_id;
  2402. }
  2403. }
  2404. $fileIds = array_unique($fileIds);
  2405. $fileMap = File::pivotGet('id', $fileIds);
  2406. foreach ($notices as $notice)
  2407. {
  2408. $files = array();
  2409. $f2ps = $f2pMap[$notice->id];
  2410. foreach ($f2ps as $f2p) {
  2411. $files[] = $fileMap[$f2p->file_id];
  2412. }
  2413. $notice->_setAttachments($files);
  2414. }
  2415. }
  2416. static function fillReplies(&$notices)
  2417. {
  2418. $ids = self::_idsOf($notices);
  2419. $replyMap = Reply::listGet('notice_id', $ids);
  2420. foreach ($notices as $notice) {
  2421. $replies = $replyMap[$notice->id];
  2422. $ids = array();
  2423. foreach ($replies as $reply) {
  2424. $ids[] = $reply->profile_id;
  2425. }
  2426. $notice->_setReplies($ids);
  2427. }
  2428. }
  2429. protected $_repeats = array();
  2430. function getRepeats()
  2431. {
  2432. if (isset($this->_repeats[$this->id])) {
  2433. return $this->_repeats[$this->id];
  2434. }
  2435. $repeatMap = Notice::listGet('repeat_of', array($this->id));
  2436. $this->_repeats[$this->id] = $repeatMap[$this->id];
  2437. return $this->_repeats[$this->id];
  2438. }
  2439. function _setRepeats($repeats)
  2440. {
  2441. $this->_repeats[$this->id] = $repeats;
  2442. }
  2443. static function fillRepeats(&$notices)
  2444. {
  2445. $ids = self::_idsOf($notices);
  2446. $repeatMap = Notice::listGet('repeat_of', $ids);
  2447. foreach ($notices as $notice) {
  2448. $repeats = $repeatMap[$notice->id];
  2449. $notice->_setRepeats($repeats);
  2450. }
  2451. }
  2452. }