We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

Notice.php 74.3 KB
Newer Older
Evan Prodromou's avatar
Evan Prodromou committed
1
<?php
Brenda Wallace's avatar
Brenda Wallace committed
2
/**
3
 * StatusNet - the distributed open-source microblogging tool
4
 * Copyright (C) 2008, 2009, StatusNet, Inc.
Evan Prodromou's avatar
Evan Prodromou committed
5
 *
Evan Prodromou's avatar
Evan Prodromou committed
6 7 8 9
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
Evan Prodromou's avatar
Evan Prodromou committed
10
 *
Evan Prodromou's avatar
Evan Prodromou committed
11 12
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
Evan Prodromou's avatar
Evan Prodromou committed
14
 * GNU Affero General Public License for more details.
Evan Prodromou's avatar
Evan Prodromou committed
15
 *
Evan Prodromou's avatar
Evan Prodromou committed
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/>.
Evan Prodromou's avatar
Evan Prodromou committed
18
 *
Brenda Wallace's avatar
Brenda Wallace committed
19 20 21 22 23 24 25 26 27 28 29 30 31
 * @category Notices
 * @package  StatusNet
 * @author   Brenda Wallace <shiny@cpan.org>
 * @author   Christopher Vollick <psycotica0@gmail.com>
 * @author   CiaranG <ciaran@ciarang.com>
 * @author   Craig Andrews <candrews@integralblue.com>
 * @author   Evan Prodromou <evan@controlezvous.ca>
 * @author   Gina Haeussge <osd@foosel.net>
 * @author   Jeffery To <jeffery.to@gmail.com>
 * @author   Mike Cochrane <mikec@mikenz.geek.nz>
 * @author   Robin Millette <millette@controlyourself.ca>
 * @author   Sarven Capadisli <csarven@controlyourself.ca>
 * @author   Tom Adams <tom@holizz.com>
32
 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
Brenda Wallace's avatar
Brenda Wallace committed
33
 * @license  GNU Affero General Public License http://www.gnu.org/licenses/
Evan Prodromou's avatar
Evan Prodromou committed
34
 */
Evan Prodromou's avatar
Evan Prodromou committed
35

Evan Prodromou's avatar
Evan Prodromou committed
36 37
if (!defined('STATUSNET') && !defined('LACONICA')) {
    exit(1);
Brenda Wallace's avatar
Brenda Wallace committed
38
}
Evan Prodromou's avatar
Evan Prodromou committed
39

Evan Prodromou's avatar
Evan Prodromou committed
40
/**
Evan Prodromou's avatar
Evan Prodromou committed
41 42
 * Table Definition for notice
 */
43
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
Evan Prodromou's avatar
Evan Prodromou committed
44

45
/* We keep 200 notices, the max number of notices available per API request,
46 47
 * in the memcached cache. */

48
define('NOTICE_CACHE_WINDOW', CachingNoticeStream::CACHE_WINDOW);
49

Evan Prodromou's avatar
Evan Prodromou committed
50 51
define('MAX_BOXCARS', 128);

52
class Notice extends Memcached_DataObject
Evan Prodromou's avatar
Evan Prodromou committed
53
{
54 55
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */
Evan Prodromou's avatar
Evan Prodromou committed
56

57 58
    public $__table = 'notice';                          // table name
    public $id;                              // int(4)  primary_key not_null
59
    public $profile_id;                      // int(4)  multiple_key not_null
60
    public $uri;                             // varchar(255)  unique_key
61 62
    public $content;                         // text
    public $rendered;                        // text
63
    public $url;                             // varchar(255)
64 65
    public $created;                         // datetime  multiple_key not_null default_0000-00-00%2000%3A00%3A00
    public $modified;                        // timestamp   not_null default_CURRENT_TIMESTAMP
66
    public $reply_to;                        // int(4)
67
    public $is_local;                        // int(4)
68
    public $source;                          // varchar(32)
69
    public $conversation;                    // int(4)
70 71 72 73
    public $lat;                             // decimal(10,7)
    public $lon;                             // decimal(10,7)
    public $location_id;                     // int(4)
    public $location_ns;                     // int(4)
74
    public $repeat_of;                       // int(4)
Evan Prodromou's avatar
Evan Prodromou committed
75
    public $object_type;                     // varchar(255)
Evan Prodromou's avatar
Evan Prodromou committed
76
    public $scope;                           // int(4)
Evan Prodromou's avatar
Evan Prodromou committed
77

78
    /* Static get */
79 80
    function staticGet($k,$v=NULL)
    {
81 82
        return Memcached_DataObject::staticGet('Notice',$k,$v);
    }
Evan Prodromou's avatar
Evan Prodromou committed
83

84 85
    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE
Evan Prodromou's avatar
Evan Prodromou committed
86

87
    /* Notice types */
88 89 90 91
    const LOCAL_PUBLIC    =  1;
    const REMOTE_OMB      =  0;
    const LOCAL_NONPUBLIC = -1;
    const GATEWAY         = -2;
Evan Prodromou's avatar
Evan Prodromou committed
92

93
    const PUBLIC_SCOPE    = 0; // Useful fake constant
Evan Prodromou's avatar
Evan Prodromou committed
94 95
    const SITE_SCOPE      = 1;
    const ADDRESSEE_SCOPE = 2;
96 97
    const GROUP_SCOPE     = 4;
    const FOLLOWER_SCOPE  = 8;
Evan Prodromou's avatar
Evan Prodromou committed
98

99 100
    protected $_profile = -1;

101 102
    function getProfile()
    {
103
        if (is_int($this->_profile) && $this->_profile == -1) {
104
            $this->_profile = Profile::staticGet('id', $this->profile_id);
105

106 107 108 109 110
            if (empty($this->_profile)) {
                // TRANS: Server exception thrown when a user profile for a notice cannot be found.
                // TRANS: %1$d is a profile ID (number), %2$d is a notice ID (number).
                throw new ServerException(sprintf(_('No such profile (%1$d) for notice (%2$d).'), $this->profile_id, $this->id));
            }
111 112
        }

113
        return $this->_profile;
114
    }
Evan Prodromou's avatar
Evan Prodromou committed
115

116 117
    function delete()
    {
118 119
        // For auditing purposes, save a record that the notice
        // was deleted.
Evan Prodromou's avatar
Evan Prodromou committed
120

121 122 123
        // @fixme we have some cases where things get re-run and so the
        // insert fails.
        $deleted = Deleted_notice::staticGet('id', $this->id);
124 125 126 127 128

        if (!$deleted) {
            $deleted = Deleted_notice::staticGet('uri', $this->uri);
        }

129 130
        if (!$deleted) {
            $deleted = new Deleted_notice();
Evan Prodromou's avatar
Evan Prodromou committed
131

132 133 134 135 136
            $deleted->id         = $this->id;
            $deleted->profile_id = $this->profile_id;
            $deleted->uri        = $this->uri;
            $deleted->created    = $this->created;
            $deleted->deleted    = common_sql_now();
Evan Prodromou's avatar
Evan Prodromou committed
137

138 139
            $deleted->insert();
        }
Evan Prodromou's avatar
Evan Prodromou committed
140

Evan Prodromou's avatar
Evan Prodromou committed
141
        if (Event::handle('NoticeDeleteRelated', array($this))) {
142

Evan Prodromou's avatar
Evan Prodromou committed
143
            // Clear related records
144

Evan Prodromou's avatar
Evan Prodromou committed
145 146 147 148 149
            $this->clearReplies();
            $this->clearRepeats();
            $this->clearFaves();
            $this->clearTags();
            $this->clearGroupInboxes();
150
            $this->clearFiles();
Evan Prodromou's avatar
Evan Prodromou committed
151 152 153 154

            // NOTE: we don't clear inboxes
            // NOTE: we don't clear queue items
        }
Evan Prodromou's avatar
Evan Prodromou committed
155

Evan Prodromou's avatar
Evan Prodromou committed
156
        $result = parent::delete();
157 158 159

        $this->blowOnDelete();
        return $result;
160
    }
Evan Prodromou's avatar
Evan Prodromou committed
161

162 163 164
    /**
     * Extract #hashtags from this notice's content and save them to the database.
     */
165 166
    function saveTags()
    {
167
        /* extract all #hastags */
168
        $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/u', strtolower($this->content), $match);
169 170 171
        if (!$count) {
            return true;
        }
Evan Prodromou's avatar
Evan Prodromou committed
172

173 174 175 176 177 178 179 180 181 182
        /* Add them to the database */
        return $this->saveKnownTags($match[1]);
    }

    /**
     * Record the given set of hash tags in the db for this notice.
     * Given tag strings will be normalized and checked for dupes.
     */
    function saveKnownTags($hashtags)
    {
183 184
        //turn each into their canonical tag
        //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag
185
        for($i=0; $i<count($hashtags); $i++) {
Brion Vibber's avatar
Brion Vibber committed
186
            /* elide characters we don't want in the tag */
187
            $hashtags[$i] = common_canonical_tag($hashtags[$i]);
188
        }
Evan Prodromou's avatar
Evan Prodromou committed
189

190
        foreach(array_unique($hashtags) as $hashtag) {
191
            $this->saveTag($hashtag);
192
            self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $hashtag);
193 194 195
        }
        return true;
    }
Evan Prodromou's avatar
Evan Prodromou committed
196

197 198 199 200
    /**
     * Record a single hash tag as associated with this notice.
     * Tag format and uniqueness must be validated by caller.
     */
201 202 203 204 205 206 207
    function saveTag($hashtag)
    {
        $tag = new Notice_tag();
        $tag->notice_id = $this->id;
        $tag->tag = $hashtag;
        $tag->created = $this->created;
        $id = $tag->insert();
Evan Prodromou's avatar
Evan Prodromou committed
208

209
        if (!$id) {
210
            // TRANS: Server exception. %s are the error details.
211
            throw new ServerException(sprintf(_('Database error inserting hashtag: %s.'),
212 213 214
                                              $last_error->message));
            return;
        }
215 216 217

        // if it's saved, blow its cache
        $tag->blowCache(false);
218
    }
Evan Prodromou's avatar
Evan Prodromou committed
219

220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    /**
     * Save a new notice and push it out to subscribers' inboxes.
     * Poster's permissions are checked before sending.
     *
     * @param int $profile_id Profile ID of the poster
     * @param string $content source message text; links may be shortened
     *                        per current user's preference
     * @param string $source source key ('web', 'api', etc)
     * @param array $options Associative array of optional properties:
     *              string 'created' timestamp of notice; defaults to now
     *              int 'is_local' source/gateway ID, one of:
     *                  Notice::LOCAL_PUBLIC    - Local, ok to appear in public timeline
     *                  Notice::REMOTE_OMB      - Sent from a remote OMB service;
     *                                            hide from public timeline but show in
     *                                            local "and friends" timelines
     *                  Notice::LOCAL_NONPUBLIC - Local, but hide from public timeline
     *                  Notice::GATEWAY         - From another non-OMB service;
     *                                            will not appear in public views
     *              float 'lat' decimal latitude for geolocation
     *              float 'lon' decimal longitude for geolocation
     *              int 'location_id' geoname identifier
     *              int 'location_ns' geoname namespace to interpret location_id
     *              int 'reply_to'; notice ID this is a reply to
     *              int 'repeat_of'; notice ID this is a repeat of
244 245 246 247 248 249 250
     *              string 'uri' unique ID for notice; defaults to local notice URL
     *              string 'url' permalink to notice; defaults to local notice URL
     *              string 'rendered' rendered HTML version of content
     *              array 'replies' list of profile URIs for reply delivery in
     *                              place of extracting @-replies from content.
     *              array 'groups' list of group IDs to deliver to, in place of
     *                              extracting ! tags from content
251 252
     *              array 'tags' list of hashtag strings to save with the notice
     *                           in place of extracting # tags from content
253 254
     *              array 'urls' list of attached/referred URLs to save with the
     *                           notice in place of extracting links from content
255
     *              boolean 'distribute' whether to distribute the notice, default true
Evan Prodromou's avatar
Evan Prodromou committed
256
     *              string 'object_type' URL of the associated object type (default ActivityObject::NOTE)
257
     *              int 'scope' Scope bitmask; default to SITE_SCOPE on private sites, 0 otherwise
258
     *
259
     * @fixme tag override
260 261 262 263
     *
     * @return Notice
     * @throws ClientException
     */
264
    static function saveNew($profile_id, $content, $source, $options=null) {
265
        $defaults = array('uri' => null,
266
                          'url' => null,
267
                          'reply_to' => null,
268
                          'repeat_of' => null,
269
                          'scope' => null,
270
                          'distribute' => true);
271 272

        if (!empty($options)) {
273
            $options = $options + $defaults;
274
            extract($options);
275 276
        } else {
            extract($defaults);
277 278
        }

279
        if (!isset($is_local)) {
280 281
            $is_local = Notice::LOCAL_PUBLIC;
        }
Evan Prodromou's avatar
Evan Prodromou committed
282

283 284 285 286 287 288 289 290
        $profile = Profile::staticGet('id', $profile_id);
        $user = User::staticGet('id', $profile_id);
        if ($user) {
            // Use the local user's shortening preferences, if applicable.
            $final = $user->shortenLinks($content);
        } else {
            $final = common_shorten_links($content);
        }
Evan Prodromou's avatar
Evan Prodromou committed
291 292

        if (Notice::contentTooLong($final)) {
293
            // TRANS: Client exception thrown if a notice contains too many characters.
Evan Prodromou's avatar
Evan Prodromou committed
294 295 296
            throw new ClientException(_('Problem saving notice. Too long.'));
        }

297
        if (empty($profile)) {
298
            // TRANS: Client exception thrown when trying to save a notice for an unknown user.
Evan Prodromou's avatar
Evan Prodromou committed
299 300 301 302 303
            throw new ClientException(_('Problem saving notice. Unknown user.'));
        }

        if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) {
            common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.');
304
            // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame.
Evan Prodromou's avatar
Evan Prodromou committed
305
            throw new ClientException(_('Too many notices too fast; take a breather '.
306
                                        'and post again in a few minutes.'));
Evan Prodromou's avatar
Evan Prodromou committed
307 308 309 310
        }

        if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
            common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
311
            // TRANS: Client exception thrown when a user tries to post too many duplicate notices in a given time frame.
Evan Prodromou's avatar
Evan Prodromou committed
312
            throw new ClientException(_('Too many duplicate messages too quickly;'.
313
                                        ' take a breather and post again in a few minutes.'));
Evan Prodromou's avatar
Evan Prodromou committed
314 315
        }

316 317
        if (!$profile->hasRight(Right::NEWNOTICE)) {
            common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $profile->nickname);
Zach Copley's avatar
Zach Copley committed
318

319
            // TRANS: Client exception thrown when a user tries to post while being banned.
320
            throw new ClientException(_('You are banned from posting notices on this site.'), 403);
Evan Prodromou's avatar
Evan Prodromou committed
321 322 323 324 325 326 327
        }

        $notice = new Notice();
        $notice->profile_id = $profile_id;

        $autosource = common_config('public', 'autosource');

328
        // Sandboxed are non-false, but not 1, either
Evan Prodromou's avatar
Evan Prodromou committed
329

330
        if (!$profile->hasRight(Right::PUBLICNOTICE) ||
Evan Prodromou's avatar
Evan Prodromou committed
331 332 333 334 335 336 337 338 339 340 341 342 343
            ($source && $autosource && in_array($source, $autosource))) {
            $notice->is_local = Notice::LOCAL_NONPUBLIC;
        } else {
            $notice->is_local = $is_local;
        }

        if (!empty($created)) {
            $notice->created = $created;
        } else {
            $notice->created = common_sql_now();
        }

        $notice->content = $final;
344

Evan Prodromou's avatar
Evan Prodromou committed
345 346
        $notice->source = $source;
        $notice->uri = $uri;
347
        $notice->url = $url;
Evan Prodromou's avatar
Evan Prodromou committed
348

349 350 351 352 353 354
        // Get the groups here so we can figure out replies and such

        if (!isset($groups)) {
            $groups = self::groupsFromText($notice->content, $profile);
        }

355 356
        $reply = null;

Evan Prodromou's avatar
Evan Prodromou committed
357 358 359
        // Handle repeat case

        if (isset($repeat_of)) {
360 361 362 363 364

            // Check for a private one

            $repeat = Notice::staticGet('id', $repeat_of);

365
            if (empty($repeat)) {
366
                // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice.
367 368 369 370 371 372 373 374 375
                throw new ClientException(_('Cannot repeat; original notice is missing or deleted.'));
            }

            if ($profile->id == $repeat->profile_id) {
                // TRANS: Client error displayed when trying to repeat an own notice.
                throw new ClientException(_('You cannot repeat your own notice.'));
            }

            if ($repeat->scope != Notice::SITE_SCOPE &&
376
                $repeat->scope != Notice::PUBLIC_SCOPE) {
377
                // TRANS: Client error displayed when trying to repeat a non-public notice.
378 379 380
                throw new ClientException(_('Cannot repeat a private notice.'), 403);
            }

381 382 383 384 385 386 387 388 389 390
            if (!$repeat->inScope($profile)) {
                // The generic checks above should cover this, but let's be sure!
                // TRANS: Client error displayed when trying to repeat a notice you cannot access.
                throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403);
            }

            if ($profile->hasRepeated($repeat->id)) {
                // TRANS: Client error displayed when trying to repeat an already repeated notice.
                throw new ClientException(_('You already repeated that notice.'));
            }
391

Evan Prodromou's avatar
Evan Prodromou committed
392 393
            $notice->repeat_of = $repeat_of;
        } else {
394 395 396 397 398 399 400 401 402 403
            $reply = self::getReplyTo($reply_to, $profile_id, $source, $final);

            if (!empty($reply)) {

                if (!$reply->inScope($profile)) {
                    // TRANS: Client error displayed when trying to reply to a notice a the target has no access to.
                    // TRANS: %1$s is a user nickname, %2$d is a notice ID (number).
                    throw new ClientException(sprintf(_('%1$s has no access to notice %2$d.'),
                                                      $profile->nickname, $reply->id), 403);
                }
Evan Prodromou's avatar
Evan Prodromou committed
404

405 406
                $notice->reply_to     = $reply->id;
                $notice->conversation = $reply->conversation;
Evan Prodromou's avatar
Evan Prodromou committed
407

408 409 410 411 412 413 414 415 416 417 418 419 420
                // If the original is private to a group, and notice has no group specified,
                // make it to the same group(s)

                if (empty($groups) && ($reply->scope | Notice::GROUP_SCOPE)) {
                    $groups = array();
                    $replyGroups = $reply->getGroups();
                    foreach ($replyGroups as $group) {
                        if ($profile->isMember($group)) {
                            $groups[] = $group->id;
                        }
                    }
                }

421
                // Scope set below
422
            }
Evan Prodromou's avatar
Evan Prodromou committed
423 424
        }

425 426 427
        if (!empty($lat) && !empty($lon)) {
            $notice->lat = $lat;
            $notice->lon = $lon;
428 429 430
        }

        if (!empty($location_ns) && !empty($location_id)) {
431 432 433 434
            $notice->location_id = $location_id;
            $notice->location_ns = $location_ns;
        }

435 436 437 438 439 440
        if (!empty($rendered)) {
            $notice->rendered = $rendered;
        } else {
            $notice->rendered = common_render_content($final, $notice);
        }

Evan Prodromou's avatar
Evan Prodromou committed
441
        if (empty($object_type)) {
442
            $notice->object_type = (empty($notice->reply_to)) ? ActivityObject::NOTE : ActivityObject::COMMENT;
Evan Prodromou's avatar
Evan Prodromou committed
443 444 445 446
        } else {
            $notice->object_type = $object_type;
        }

447
        if (is_null($scope)) { // 0 is a valid value
448 449 450 451 452
            if (!empty($reply)) {
                $notice->scope = $reply->scope;
            } else {
                $notice->scope = common_config('notice', 'defaultscope');
            }
453 454 455 456
        } else {
            $notice->scope = $scope;
        }

457 458 459 460 461 462 463 464 465 466 467 468
        // For private streams

        $user = $profile->getUser();

        if (!empty($user)) {
            if ($user->private_stream &&
                ($notice->scope == Notice::PUBLIC_SCOPE ||
                 $notice->scope == Notice::SITE_SCOPE)) {
                $notice->scope |= Notice::FOLLOWER_SCOPE;
            }
        }

469 470 471 472 473 474 475 476 477 478 479 480
        // Force the scope for private groups

        foreach ($groups as $groupId) {
            $group = User_group::staticGet('id', $groupId);
            if (!empty($group)) {
                if ($group->force_scope) {
                    $notice->scope |= Notice::GROUP_SCOPE;
                    break;
                }
            }
        }

Evan Prodromou's avatar
Evan Prodromou committed
481 482
        if (Event::handle('StartNoticeSave', array(&$notice))) {

483
            // XXX: some of these functions write to the DB
Evan Prodromou's avatar
Evan Prodromou committed
484 485 486 487 488

            $id = $notice->insert();

            if (!$id) {
                common_log_db_error($notice, 'INSERT', __FILE__);
489
                // TRANS: Server exception thrown when a notice cannot be saved.
Evan Prodromou's avatar
Evan Prodromou committed
490 491 492
                throw new ServerException(_('Problem saving notice.'));
            }

493
            // Update ID-dependent columns: URI, conversation
Evan Prodromou's avatar
Evan Prodromou committed
494 495 496 497 498 499 500 501 502 503

            $orig = clone($notice);

            $changed = false;

            if (empty($uri)) {
                $notice->uri = common_notice_uri($notice);
                $changed = true;
            }

504 505
            // If it's not part of a conversation, it's
            // the beginning of a new conversation.
Evan Prodromou's avatar
Evan Prodromou committed
506 507

            if (empty($notice->conversation)) {
508 509
                $conv = Conversation::create();
                $notice->conversation = $conv->id;
Evan Prodromou's avatar
Evan Prodromou committed
510 511 512 513 514 515
                $changed = true;
            }

            if ($changed) {
                if (!$notice->update($orig)) {
                    common_log_db_error($notice, 'UPDATE', __FILE__);
516
                    // TRANS: Server exception thrown when a notice cannot be updated.
Evan Prodromou's avatar
Evan Prodromou committed
517 518 519 520
                    throw new ServerException(_('Problem saving notice.'));
                }
            }

521 522
        }

523 524
        // Clear the cache for subscribed users, so they'll update at next request
        // XXX: someone clever could prepend instead of clearing the cache
525

526
        $notice->blowOnInsert();
Evan Prodromou's avatar
Evan Prodromou committed
527

528 529
        // Save per-notice metadata...

530 531 532 533 534 535
        if (isset($replies)) {
            $notice->saveKnownReplies($replies);
        } else {
            $notice->saveReplies();
        }

536 537 538 539 540 541
        if (isset($tags)) {
            $notice->saveKnownTags($tags);
        } else {
            $notice->saveTags();
        }

542 543
        // Note: groups may save tags, so must be run after tags are saved
        // to avoid errors on duplicates.
544 545 546
        // Note: groups should always be set.

        $notice->saveKnownGroups($groups);
547

548 549 550 551 552
        if (isset($urls)) {
            $notice->saveKnownUrls($urls);
        } else {
            $notice->saveUrls();
        }
553

554 555 556 557
        if ($distribute) {
            // Prepare inbox delivery, may be queued to background.
            $notice->distribute();
        }
558

559 560
        return $notice;
    }
561

562
    function blowOnInsert($conversation = false)
563 564
    {
        self::blow('profile:notice_ids:%d', $this->profile_id);
565 566 567 568

        if ($this->isPublic()) {
            self::blow('public');
        }
Evan Prodromou's avatar
Evan Prodromou committed
569

570 571 572 573
        // XXX: Before we were blowing the casche only if the notice id
        // was not the root of the conversation.  What to do now?

        self::blow('notice:conversation_ids:%d', $this->conversation);
574
        self::blow('conversation::notice_count:%d', $this->conversation);
Evan Prodromou's avatar
Evan Prodromou committed
575

576 577
        if (!empty($this->repeat_of)) {
            self::blow('notice:repeats:%d', $this->repeat_of);
Evan Prodromou's avatar
Evan Prodromou committed
578 579
        }

580
        $original = Notice::staticGet('id', $this->repeat_of);
Evan Prodromou's avatar
Evan Prodromou committed
581

582 583 584 585 586 587
        if (!empty($original)) {
            $originalUser = User::staticGet('id', $original->profile_id);
            if (!empty($originalUser)) {
                self::blow('user:repeats_of_me:%d', $originalUser->id);
            }
        }
Evan Prodromou's avatar
Evan Prodromou committed
588

589
        $profile = Profile::staticGet($this->profile_id);
590 591 592
        if (!empty($profile)) {
            $profile->blowNoticeCount();
        }
Shashi Gowda's avatar
Shashi Gowda committed
593 594 595 596 597

        $ptags = $this->getProfileTags();
        foreach ($ptags as $ptag) {
            $ptag->blowNoticeStreamCache();
        }
Evan Prodromou's avatar
Evan Prodromou committed
598 599
    }

600 601 602 603 604 605 606 607 608
    /**
     * Clear cache entries related to this notice at delete time.
     * Necessary to avoid breaking paging on public, profile timelines.
     */
    function blowOnDelete()
    {
        $this->blowOnInsert();

        self::blow('profile:notice_ids:%d;last', $this->profile_id);
609 610 611 612

        if ($this->isPublic()) {
            self::blow('public;last');
        }
613 614

        self::blow('fave:by_notice', $this->id);
615 616 617 618 619

        if ($this->conversation) {
            // In case we're the first, will need to calc a new root.
            self::blow('notice:conversation_root:%d', $this->conversation);
        }
Shashi Gowda's avatar
Shashi Gowda committed
620 621 622 623 624

        $ptags = $this->getProfileTags();
        foreach ($ptags as $ptag) {
            $ptag->blowNoticeStreamCache(true);
        }
625 626
    }

627
    /** save all urls in the notice to the db
Evan Prodromou's avatar
Evan Prodromou committed
628 629 630 631 632 633 634
     *
     * follow redirects and save all available file information
     * (mimetype, date, size, oembed, etc.)
     *
     * @return void
     */
    function saveUrls() {
635 636 637
        if (common_config('attachments', 'process_links')) {
            common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id);
        }
Evan Prodromou's avatar
Evan Prodromou committed
638 639
    }

640 641 642 643 644 645 646 647 648 649
    /**
     * Save the given URLs as related links/attachments to the db
     *
     * follow redirects and save all available file information
     * (mimetype, date, size, oembed, etc.)
     *
     * @return void
     */
    function saveKnownUrls($urls)
    {
650 651 652 653 654
        if (common_config('attachments', 'process_links')) {
            // @fixme validation?
            foreach (array_unique($urls) as $url) {
                File::processNew($url, $this->id);
            }
655 656 657 658 659 660
        }
    }

    /**
     * @private callback
     */
661
    function saveUrl($url, $notice_id) {
662 663
        File::processNew($url, $notice_id);
    }
Evan Prodromou's avatar
Evan Prodromou committed
664

665 666
    static function checkDupes($profile_id, $content) {
        $profile = Profile::staticGet($profile_id);
667
        if (empty($profile)) {
668 669
            return false;
        }
670
        $notice = $profile->getNotices(0, CachingNoticeStream::CACHE_WINDOW);
671
        if (!empty($notice)) {
672 673 674 675 676 677 678 679 680
            $last = 0;
            while ($notice->fetch()) {
                if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
                    return true;
                } else if ($notice->content == $content) {
                    return false;
                }
            }
        }
681 682
        // If we get here, oldest item in cache window is not
        // old enough for dupe limit; do direct check against DB
683 684 685
        $notice = new Notice();
        $notice->profile_id = $profile_id;
        $notice->content = $content;
686 687
        $threshold = common_sql_date(time() - common_config('site', 'dupelimit'));
        $notice->whereAdd(sprintf("created > '%s'", $notice->escape($threshold)));
Evan Prodromou's avatar
Evan Prodromou committed
688

689
        $cnt = $notice->count();
690
        return ($cnt == 0);
691
    }
Evan Prodromou's avatar
Evan Prodromou committed
692

Evan Prodromou's avatar
Evan Prodromou committed
693 694
    static function checkEditThrottle($profile_id) {
        $profile = Profile::staticGet($profile_id);
695
        if (empty($profile)) {
Evan Prodromou's avatar
Evan Prodromou committed
696 697
            return false;
        }
698
        // Get the Nth notice
Evan Prodromou's avatar
Evan Prodromou committed
699 700
        $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1);
        if ($notice && $notice->fetch()) {
701
            // If the Nth notice was posted less than timespan seconds ago
Evan Prodromou's avatar
Evan Prodromou committed
702
            if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) {
703
                // Then we throttle
Evan Prodromou's avatar
Evan Prodromou committed
704 705 706
                return false;
            }
        }
707
        // Either not N notices in the stream, OR the Nth was not posted within timespan seconds
Evan Prodromou's avatar
Evan Prodromou committed
708 709
        return true;
    }
Evan Prodromou's avatar
Evan Prodromou committed
710

711 712
    function getUploadedAttachment() {
        $post = clone $this;
713
        $query = 'select file.url as up, file.id as i from file join file_to_post on file.id = file_id where post_id=' . $post->escape($post->id) . ' and url like "%/notice/%/file"';
714 715
        $post->query($query);
        $post->fetch();
716 717 718 719 720
        if (empty($post->up) || empty($post->i)) {
            $ret = false;
        } else {
            $ret = array($post->up, $post->i);
        }
721 722 723
        $post->free();
        return $ret;
    }
Evan Prodromou's avatar
Evan Prodromou committed
724

725
    function hasAttachments() {
726 727
        $post = clone $this;
        $query = "select count(file_id) as n_attachments from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $post->escape($post->id);
728 729 730 731 732 733
        $post->query($query);
        $post->fetch();
        $n_attachments = intval($post->n_attachments);
        $post->free();
        return $n_attachments;
    }
Evan Prodromou's avatar
Evan Prodromou committed
734

Evan Prodromou's avatar
Evan Prodromou committed
735
    function attachments() {
Evan Prodromou's avatar
Evan Prodromou committed
736 737 738 739 740 741 742 743

        $keypart = sprintf('notice:file_ids:%d', $this->id);

        $idstr = self::cacheGet($keypart);

        if ($idstr !== false) {
            $ids = explode(',', $idstr);
        } else {
744
            $ids = array();
Evan Prodromou's avatar
Evan Prodromou committed
745 746 747 748 749
            $f2p = new File_to_post;
            $f2p->post_id = $this->id;
            if ($f2p->find()) {
                while ($f2p->fetch()) {
                    $ids[] = $f2p->file_id;
750
                }
Evan Prodromou's avatar
Evan Prodromou committed
751
            }
Evan Prodromou's avatar
Evan Prodromou committed
752
            self::cacheSet($keypart, implode(',', $ids));
Evan Prodromou's avatar
Evan Prodromou committed
753
        }
Evan Prodromou's avatar
Evan Prodromou committed
754 755 756 757 758 759 760 761 762 763

        $att = array();

        foreach ($ids as $id) {
            $f = File::staticGet('id', $id);
            if (!empty($f)) {
                $att[] = clone($f);
            }
        }

Evan Prodromou's avatar
Evan Prodromou committed
764 765
        return $att;
    }
Evan Prodromou's avatar
Evan Prodromou committed
766 767


768
    function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0)
769
    {
770
        $stream = new PublicNoticeStream();
771
        return $stream->getNotices($offset, $limit, $since_id, $max_id);
Evan Prodromou's avatar
Evan Prodromou committed
772
    }
Evan Prodromou's avatar
Evan Prodromou committed
773 774


775
    function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
776
    {
777
        $stream = new ConversationNoticeStream($id);
Evan Prodromou's avatar
Evan Prodromou committed
778

779
        return $stream->getNotices($offset, $limit, $since_id, $max_id);
780
    }
Evan Prodromou's avatar
Evan Prodromou committed
781

782 783
    /**
     * Is this notice part of an active conversation?
784
     *
785 786 787 788 789 790 791 792 793 794 795
     * @return boolean true if other messages exist in the same
     *                 conversation, false if this is the only one
     */
    function hasConversation()
    {
        if (!empty($this->conversation)) {
            $conversation = Notice::conversationStream(
                $this->conversation,
                1,
                1
            );
796

797 798 799 800 801 802 803
            if ($conversation->N > 0) {
                return true;
            }
        }
        return false;
    }

804 805 806 807 808
    /**
     * Grab the earliest notice from this conversation.
     *
     * @return Notice or null
     */
809
    function conversationRoot($profile=-1)
810
    {
811
        // XXX: can this happen?
812

813 814 815
        if (empty($this->conversation)) {
            return null;
        }
816

817
        // Get the current profile if not specified
818

819 820
        if (is_int($profile) && $profile == -1) {
            $profile = Profile::current();
821
        }
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865

        // If this notice is out of scope, no root for you!

        if (!$this->inScope($profile)) {
            return null;
        }

        // If this isn't a reply to anything, then it's its own
        // root.

        if (empty($this->reply_to)) {
            return $this;
        }
        
        if (is_null($profile)) {
            $keypart = sprintf('notice:conversation_root:%d:null', $this->id);
        } else {
            $keypart = sprintf('notice:conversation_root:%d:%d',
                               $this->id,
                               $profile->id);
        }
            
        $root = self::cacheGet($keypart);

        if ($root !== false && $root->inScope($profile)) {
            return $root;
        } else {
            $last = $this;

            do {
                $parent = $last->getOriginal();
                if (!empty($parent) && $parent->inScope($profile)) {
                    $last = $parent;
                    continue;
                } else {
                    $root = $last;
                    break;
                }
            } while (!empty($parent));

            self::cacheSet($keypart, $root);
        }

        return $root;
866
    }
867

868
    /**
869 870 871 872 873 874 875 876 877
     * Pull up a full list of local recipients who will be getting
     * this notice in their inbox. Results will be cached, so don't
     * change the input data wily-nilly!
     *
     * @param array $groups optional list of Group objects;
     *              if left empty, will be loaded from group_inbox records
     * @param array $recipient optional list of reply profile ids
     *              if left empty, will be loaded from reply records
     * @return array associating recipient user IDs with an inbox source constant
878
     */
879
    function whoGets($groups=null, $recipients=null)
880
    {
Evan Prodromou's avatar
Evan Prodromou committed
881 882 883
        $c = self::memcache();

        if (!empty($c)) {
884
            $ni = $c->get(Cache::key('notice:who_gets:'.$this->id));
Evan Prodromou's avatar
Evan Prodromou committed
885 886 887 888 889
            if ($ni !== false) {
                return $ni;
            }
        }

890 891 892 893 894 895 896 897
        if (is_null($groups)) {
            $groups = $this->getGroups();
        }

        if (is_null($recipients)) {
            $recipients = $this->getReplies();
        }

Evan Prodromou's avatar
Evan Prodromou committed
898
        $users = $this->getSubscribedUsers();
Shashi Gowda's avatar
Shashi Gowda committed
899
        $ptags = $this->getProfileTags();
Evan Prodromou's avatar
Evan Prodromou committed
900

Evan Prodromou's avatar
Evan Prodromou committed
901 902 903
        // FIXME: kind of ignoring 'transitional'...
        // we'll probably stop supporting inboxless mode
        // in 0.9.x
Evan Prodromou's avatar
Evan Prodromou committed
904

Evan Prodromou's avatar
Evan Prodromou committed
905
        $ni = array();
Evan Prodromou's avatar
Evan Prodromou committed
906

907 908
        // Give plugins a chance to add folks in at start...
        if (Event::handle('StartNoticeWhoGets', array($this, &$ni))) {
Evan Prodromou's avatar
Evan Prodromou committed
909

910
            foreach ($users as $id) {
911 912 913 914 915 916 917 918 919
                $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
            }

            foreach ($groups as $group) {
                $users = $group->getUserMembers();
                foreach ($users as $id) {
                    if (!array_key_exists($id, $ni)) {
                        $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
                    }
Evan Prodromou's avatar
Evan Prodromou committed
920 921
                }
            }
Evan Prodromou's avatar
Evan Prodromou committed
922

923 924 925 926 927 928 929 930
            foreach ($ptags as $ptag) {
                $users = $ptag->getUserSubscribers();
                foreach ($users as $id) {
                    if (!array_key_exists($id, $ni)) {
                        $user = User::staticGet('id', $id);
                        if (!$user->hasBlocked($profile)) {
                            $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG;
                        }
Shashi Gowda's avatar
Shashi Gowda committed
931 932 933 934
                    }
                }
            }

935 936 937