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

Notice.php 61 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', 200);
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

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

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

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

91 92
    function getProfile()
    {
93 94 95
        $profile = Profile::staticGet('id', $this->profile_id);

        if (empty($profile)) {
96 97 98
            // 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));
99 100 101
        }

        return $profile;
102
    }
Evan Prodromou's avatar
Evan Prodromou committed
103

104 105
    function delete()
    {
106 107
        // For auditing purposes, save a record that the notice
        // was deleted.
Evan Prodromou's avatar
Evan Prodromou committed
108

109 110 111 112 113
        // @fixme we have some cases where things get re-run and so the
        // insert fails.
        $deleted = Deleted_notice::staticGet('id', $this->id);
        if (!$deleted) {
            $deleted = new Deleted_notice();
Evan Prodromou's avatar
Evan Prodromou committed
114

115 116 117 118 119
            $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
120

121 122
            $deleted->insert();
        }
Evan Prodromou's avatar
Evan Prodromou committed
123

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

Evan Prodromou's avatar
Evan Prodromou committed
126
            // Clear related records
127

Evan Prodromou's avatar
Evan Prodromou committed
128 129 130 131 132 133 134 135 136
            $this->clearReplies();
            $this->clearRepeats();
            $this->clearFaves();
            $this->clearTags();
            $this->clearGroupInboxes();

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

Evan Prodromou's avatar
Evan Prodromou committed
138
        $result = parent::delete();
139 140 141

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

144 145 146
    /**
     * Extract #hashtags from this notice's content and save them to the database.
     */
147 148
    function saveTags()
    {
149
        /* extract all #hastags */
150
        $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/', strtolower($this->content), $match);
151 152 153
        if (!$count) {
            return true;
        }
Evan Prodromou's avatar
Evan Prodromou committed
154

155 156 157 158 159 160 161 162 163 164
        /* 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)
    {
165 166
        //turn each into their canonical tag
        //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag
167
        for($i=0; $i<count($hashtags); $i++) {
Brion Vibber's avatar
Brion Vibber committed
168
            /* elide characters we don't want in the tag */
169
            $hashtags[$i] = common_canonical_tag($hashtags[$i]);
170
        }
Evan Prodromou's avatar
Evan Prodromou committed
171

172
        foreach(array_unique($hashtags) as $hashtag) {
173
            $this->saveTag($hashtag);
174
            self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $hashtag);
175 176 177
        }
        return true;
    }
Evan Prodromou's avatar
Evan Prodromou committed
178

179 180 181 182
    /**
     * Record a single hash tag as associated with this notice.
     * Tag format and uniqueness must be validated by caller.
     */
183 184 185 186 187 188 189
    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
190

191
        if (!$id) {
192 193
            // TRANS: Server exception. %s are the error details.
            throw new ServerException(sprintf(_('Database error inserting hashtag: %s'),
194 195 196
                                              $last_error->message));
            return;
        }
197 198 199

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

202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    /**
     * 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
226 227 228 229 230 231 232
     *              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
233 234
     *              array 'tags' list of hashtag strings to save with the notice
     *                           in place of extracting # tags from content
235 236
     *              array 'urls' list of attached/referred URLs to save with the
     *                           notice in place of extracting links from content
237 238
     *              boolean 'distribute' whether to distribute the notice, default true
     *                    
239
     * @fixme tag override
240 241 242 243
     *
     * @return Notice
     * @throws ClientException
     */
244
    static function saveNew($profile_id, $content, $source, $options=null) {
245
        $defaults = array('uri' => null,
246
                          'url' => null,
247
                          'reply_to' => null,
248 249
                          'repeat_of' => null,
                          'distribute' => true);
250 251

        if (!empty($options)) {
252
            $options = $options + $defaults;
253
            extract($options);
254 255
        } else {
            extract($defaults);
256 257
        }

258
        if (!isset($is_local)) {
259 260
            $is_local = Notice::LOCAL_PUBLIC;
        }
Evan Prodromou's avatar
Evan Prodromou committed
261

262 263 264 265 266 267 268 269
        $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
270 271

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

276
        if (empty($profile)) {
277
            // TRANS: Client exception thrown when trying to save a notice for an unknown user.
Evan Prodromou's avatar
Evan Prodromou committed
278 279 280 281 282
            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.');
283
            // 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
284
            throw new ClientException(_('Too many notices too fast; take a breather '.
285
                                        'and post again in a few minutes.'));
Evan Prodromou's avatar
Evan Prodromou committed
286 287 288 289
        }

        if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
            common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
290
            // 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
291
            throw new ClientException(_('Too many duplicate messages too quickly;'.
292
                                        ' take a breather and post again in a few minutes.'));
Evan Prodromou's avatar
Evan Prodromou committed
293 294
        }

295 296
        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
297

298
            // TRANS: Client exception thrown when a user tries to post while being banned.
299
            throw new ClientException(_('You are banned from posting notices on this site.'), 403);
Evan Prodromou's avatar
Evan Prodromou committed
300 301 302 303 304 305 306
        }

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

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

307
        # Sandboxed are non-false, but not 1, either
Evan Prodromou's avatar
Evan Prodromou committed
308

309
        if (!$profile->hasRight(Right::PUBLICNOTICE) ||
Evan Prodromou's avatar
Evan Prodromou committed
310 311 312 313 314 315 316 317 318 319 320 321 322
            ($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;
323

Evan Prodromou's avatar
Evan Prodromou committed
324 325
        $notice->source = $source;
        $notice->uri = $uri;
326
        $notice->url = $url;
Evan Prodromou's avatar
Evan Prodromou committed
327

Evan Prodromou's avatar
Evan Prodromou committed
328 329 330 331 332 333 334
        // Handle repeat case

        if (isset($repeat_of)) {
            $notice->repeat_of = $repeat_of;
        } else {
            $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final);
        }
Evan Prodromou's avatar
Evan Prodromou committed
335 336 337 338 339 340

        if (!empty($notice->reply_to)) {
            $reply = Notice::staticGet('id', $notice->reply_to);
            $notice->conversation = $reply->conversation;
        }

341 342 343
        if (!empty($lat) && !empty($lon)) {
            $notice->lat = $lat;
            $notice->lon = $lon;
344 345 346
        }

        if (!empty($location_ns) && !empty($location_id)) {
347 348 349 350
            $notice->location_id = $location_id;
            $notice->location_ns = $location_ns;
        }

351 352 353 354 355 356
        if (!empty($rendered)) {
            $notice->rendered = $rendered;
        } else {
            $notice->rendered = common_render_content($final, $notice);
        }

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

359
            // XXX: some of these functions write to the DB
Evan Prodromou's avatar
Evan Prodromou committed
360 361 362 363 364

            $id = $notice->insert();

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

369
            // Update ID-dependent columns: URI, conversation
Evan Prodromou's avatar
Evan Prodromou committed
370 371 372 373 374 375 376 377 378 379

            $orig = clone($notice);

            $changed = false;

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

380 381
            // If it's not part of a conversation, it's
            // the beginning of a new conversation.
Evan Prodromou's avatar
Evan Prodromou committed
382 383

            if (empty($notice->conversation)) {
384 385
                $conv = Conversation::create();
                $notice->conversation = $conv->id;
Evan Prodromou's avatar
Evan Prodromou committed
386 387 388 389 390 391
                $changed = true;
            }

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

397 398 399 400
        }

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

402
        $notice->blowOnInsert();
Evan Prodromou's avatar
Evan Prodromou committed
403

404 405
        // Save per-notice metadata...

406 407 408 409 410 411
        if (isset($replies)) {
            $notice->saveKnownReplies($replies);
        } else {
            $notice->saveReplies();
        }

412 413 414 415 416 417
        if (isset($tags)) {
            $notice->saveKnownTags($tags);
        } else {
            $notice->saveTags();
        }

418 419 420 421 422 423 424 425
        // Note: groups may save tags, so must be run after tags are saved
        // to avoid errors on duplicates.
        if (isset($groups)) {
            $notice->saveKnownGroups($groups);
        } else {
            $notice->saveGroups();
        }

426 427 428 429 430
        if (isset($urls)) {
            $notice->saveKnownUrls($urls);
        } else {
            $notice->saveUrls();
        }
431

432 433 434 435
        if ($distribute) {
            // Prepare inbox delivery, may be queued to background.
            $notice->distribute();
        }
436

437 438
        return $notice;
    }
439

440
    function blowOnInsert($conversation = false)
441 442 443
    {
        self::blow('profile:notice_ids:%d', $this->profile_id);
        self::blow('public');
Evan Prodromou's avatar
Evan Prodromou committed
444

445 446 447 448
        // 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);
Evan Prodromou's avatar
Evan Prodromou committed
449

450 451
        if (!empty($this->repeat_of)) {
            self::blow('notice:repeats:%d', $this->repeat_of);
Evan Prodromou's avatar
Evan Prodromou committed
452 453
        }

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

456 457 458 459 460 461
        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
462

463
        $profile = Profile::staticGet($this->profile_id);
464 465 466
        if (!empty($profile)) {
            $profile->blowNoticeCount();
        }
Evan Prodromou's avatar
Evan Prodromou committed
467 468
    }

469 470 471 472 473 474 475 476 477 478 479 480
    /**
     * 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);
        self::blow('public;last');
    }

481
    /** save all urls in the notice to the db
Evan Prodromou's avatar
Evan Prodromou committed
482 483 484 485 486 487 488
     *
     * follow redirects and save all available file information
     * (mimetype, date, size, oembed, etc.)
     *
     * @return void
     */
    function saveUrls() {
489 490 491
        if (common_config('attachments', 'process_links')) {
            common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id);
        }
Evan Prodromou's avatar
Evan Prodromou committed
492 493
    }

494 495 496 497 498 499 500 501 502 503
    /**
     * 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)
    {
504 505 506 507 508
        if (common_config('attachments', 'process_links')) {
            // @fixme validation?
            foreach (array_unique($urls) as $url) {
                File::processNew($url, $this->id);
            }
509 510 511 512 513 514
        }
    }

    /**
     * @private callback
     */
515
    function saveUrl($url, $notice_id) {
516 517
        File::processNew($url, $notice_id);
    }
Evan Prodromou's avatar
Evan Prodromou committed
518

519 520
    static function checkDupes($profile_id, $content) {
        $profile = Profile::staticGet($profile_id);
521
        if (empty($profile)) {
522 523 524
            return false;
        }
        $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW);
525
        if (!empty($notice)) {
526 527 528 529 530 531 532 533 534 535 536 537 538 539
            $last = 0;
            while ($notice->fetch()) {
                if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
                    return true;
                } else if ($notice->content == $content) {
                    return false;
                }
            }
        }
        # If we get here, oldest item in cache window is not
        # old enough for dupe limit; do direct check against DB
        $notice = new Notice();
        $notice->profile_id = $profile_id;
        $notice->content = $content;
540 541
        $threshold = common_sql_date(time() - common_config('site', 'dupelimit'));
        $notice->whereAdd(sprintf("created > '%s'", $notice->escape($threshold)));
Evan Prodromou's avatar
Evan Prodromou committed
542

543
        $cnt = $notice->count();
544
        return ($cnt == 0);
545
    }
Evan Prodromou's avatar
Evan Prodromou committed
546

Evan Prodromou's avatar
Evan Prodromou committed
547 548
    static function checkEditThrottle($profile_id) {
        $profile = Profile::staticGet($profile_id);
549
        if (empty($profile)) {
Evan Prodromou's avatar
Evan Prodromou committed
550 551 552 553 554 555 556 557 558 559 560 561 562 563
            return false;
        }
        # Get the Nth notice
        $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1);
        if ($notice && $notice->fetch()) {
            # If the Nth notice was posted less than timespan seconds ago
            if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) {
                # Then we throttle
                return false;
            }
        }
        # Either not N notices in the stream, OR the Nth was not posted within timespan seconds
        return true;
    }
Evan Prodromou's avatar
Evan Prodromou committed
564

565 566
    function getUploadedAttachment() {
        $post = clone $this;
567
        $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"';
568 569
        $post->query($query);
        $post->fetch();
570 571 572 573 574
        if (empty($post->up) || empty($post->i)) {
            $ret = false;
        } else {
            $ret = array($post->up, $post->i);
        }
575 576 577
        $post->free();
        return $ret;
    }
Evan Prodromou's avatar
Evan Prodromou committed
578

579
    function hasAttachments() {
580 581
        $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);
582 583 584 585 586 587
        $post->query($query);
        $post->fetch();
        $n_attachments = intval($post->n_attachments);
        $post->free();
        return $n_attachments;
    }
Evan Prodromou's avatar
Evan Prodromou committed
588

Evan Prodromou's avatar
Evan Prodromou committed
589 590 591 592 593 594 595 596
    function attachments() {
        // XXX: cache this
        $att = array();
        $f2p = new File_to_post;
        $f2p->post_id = $this->id;
        if ($f2p->find()) {
            while ($f2p->fetch()) {
                $f = File::staticGet($f2p->file_id);
597 598 599
                if ($f) {
                    $att[] = clone($f);
                }
Evan Prodromou's avatar
Evan Prodromou committed
600 601 602 603
            }
        }
        return $att;
    }
Evan Prodromou's avatar
Evan Prodromou committed
604

605 606 607
    function getStreamByIds($ids)
    {
        $cache = common_memcache();
Evan Prodromou's avatar
Evan Prodromou committed
608

609 610 611
        if (!empty($cache)) {
            $notices = array();
            foreach ($ids as $id) {
612 613 614 615
                $n = Notice::staticGet('id', $id);
                if (!empty($n)) {
                    $notices[] = $n;
                }
616 617 618 619
            }
            return new ArrayWrapper($notices);
        } else {
            $notice = new Notice();
620 621 622 623
            if (empty($ids)) {
                //if no IDs requested, just return the notice object
                return $notice;
            }
624
            $notice->whereAdd('id in (' . implode(', ', $ids) . ')');
Evan Prodromou's avatar
Evan Prodromou committed
625

626
            $notice->find();
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642

            $temp = array();

            while ($notice->fetch()) {
                $temp[$notice->id] = clone($notice);
            }

            $wrapped = array();

            foreach ($ids as $id) {
                if (array_key_exists($id, $temp)) {
                    $wrapped[] = $temp[$id];
                }
            }

            return new ArrayWrapper($wrapped);
643 644
        }
    }
Evan Prodromou's avatar
Evan Prodromou committed
645

646
    function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0)
647
    {
Evan Prodromou's avatar
Evan Prodromou committed
648 649 650
        $ids = Notice::stream(array('Notice', '_publicStreamDirect'),
                              array(),
                              'public',
651
                              $offset, $limit, $since_id, $max_id);
Evan Prodromou's avatar
Evan Prodromou committed
652 653
        return Notice::getStreamByIds($ids);
    }
Evan Prodromou's avatar
Evan Prodromou committed
654

655
    function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0)
Evan Prodromou's avatar
Evan Prodromou committed
656 657
    {
        $notice = new Notice();
Evan Prodromou's avatar
Evan Prodromou committed
658

Evan Prodromou's avatar
Evan Prodromou committed
659 660
        $notice->selectAdd(); // clears it
        $notice->selectAdd('id');
Evan Prodromou's avatar
Evan Prodromou committed
661

Evan Prodromou's avatar
Evan Prodromou committed
662
        $notice->orderBy('id DESC');
Evan Prodromou's avatar
Evan Prodromou committed
663

Evan Prodromou's avatar
Evan Prodromou committed
664 665 666
        if (!is_null($offset)) {
            $notice->limit($offset, $limit);
        }
Evan Prodromou's avatar
Evan Prodromou committed
667

668
        if (common_config('public', 'localonly')) {
669
            $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC);
670
        } else {
671 672 673
            # -1 == blacklisted, -2 == gateway (i.e. Twitter)
            $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC);
            $notice->whereAdd('is_local !='. Notice::GATEWAY);
674
        }
Evan Prodromou's avatar
Evan Prodromou committed
675

676 677 678
        if ($since_id != 0) {
            $notice->whereAdd('id > ' . $since_id);
        }
Evan Prodromou's avatar
Evan Prodromou committed
679

680 681 682
        if ($max_id != 0) {
            $notice->whereAdd('id <= ' . $max_id);
        }
Evan Prodromou's avatar
Evan Prodromou committed
683

684
        $ids = array();
Evan Prodromou's avatar
Evan Prodromou committed
685

686 687 688 689 690
        if ($notice->find()) {
            while ($notice->fetch()) {
                $ids[] = $notice->id;
            }
        }
Evan Prodromou's avatar
Evan Prodromou committed
691

692 693
        $notice->free();
        $notice = NULL;
Evan Prodromou's avatar
Evan Prodromou committed
694

695 696
        return $ids;
    }
Evan Prodromou's avatar
Evan Prodromou committed
697

698
    function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
699 700 701 702
    {
        $ids = Notice::stream(array('Notice', '_conversationStreamDirect'),
                              array($id),
                              'notice:conversation_ids:'.$id,
703
                              $offset, $limit, $since_id, $max_id);
Evan Prodromou's avatar
Evan Prodromou committed
704

705 706
        return Notice::getStreamByIds($ids);
    }
Evan Prodromou's avatar
Evan Prodromou committed
707

708
    function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
709 710
    {
        $notice = new Notice();
Evan Prodromou's avatar
Evan Prodromou committed
711

712 713
        $notice->selectAdd(); // clears it
        $notice->selectAdd('id');
Evan Prodromou's avatar
Evan Prodromou committed
714

Evan Prodromou's avatar
Evan Prodromou committed
715
        $notice->conversation = $id;
Evan Prodromou's avatar
Evan Prodromou committed
716

717
        $notice->orderBy('id DESC');
Evan Prodromou's avatar
Evan Prodromou committed
718

719 720
        if (!is_null($offset)) {
            $notice->limit($offset, $limit);
721
        }
Evan Prodromou's avatar
Evan Prodromou committed
722

Evan Prodromou's avatar
Evan Prodromou committed
723 724
        if ($since_id != 0) {
            $notice->whereAdd('id > ' . $since_id);
725
        }
Evan Prodromou's avatar
Evan Prodromou committed
726

727 728
        if ($max_id != 0) {
            $notice->whereAdd('id <= ' . $max_id);
Evan Prodromou's avatar
Evan Prodromou committed
729
        }
Evan Prodromou's avatar
Evan Prodromou committed
730

Evan Prodromou's avatar
Evan Prodromou committed
731
        $ids = array();
Evan Prodromou's avatar
Evan Prodromou committed
732

Evan Prodromou's avatar
Evan Prodromou committed
733 734 735 736
        if ($notice->find()) {
            while ($notice->fetch()) {
                $ids[] = $notice->id;
            }
737
        }
Evan Prodromou's avatar
Evan Prodromou committed
738

Evan Prodromou's avatar
Evan Prodromou committed
739 740
        $notice->free();
        $notice = NULL;
Evan Prodromou's avatar
Evan Prodromou committed
741

Evan Prodromou's avatar
Evan Prodromou committed
742
        return $ids;
743
    }
Evan Prodromou's avatar
Evan Prodromou committed
744

745 746
    /**
     * Is this notice part of an active conversation?
747
     *
748 749 750 751 752 753 754 755 756 757 758
     * @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
            );
759

760 761 762 763 764 765 766
            if ($conversation->N > 0) {
                return true;
            }
        }
        return false;
    }

767
    /**
768 769 770 771 772 773 774 775 776
     * 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
777
     */
778
    function whoGets($groups=null, $recipients=null)
779
    {
Evan Prodromou's avatar
Evan Prodromou committed
780 781 782 783 784 785 786 787 788
        $c = self::memcache();

        if (!empty($c)) {
            $ni = $c->get(common_cache_key('notice:who_gets:'.$this->id));
            if ($ni !== false) {
                return $ni;
            }
        }

789 790 791 792 793 794 795 796
        if (is_null($groups)) {
            $groups = $this->getGroups();
        }

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

Evan Prodromou's avatar
Evan Prodromou committed
797
        $users = $this->getSubscribedUsers();
Evan Prodromou's avatar
Evan Prodromou committed
798

Evan Prodromou's avatar
Evan Prodromou committed
799 800 801
        // FIXME: kind of ignoring 'transitional'...
        // we'll probably stop supporting inboxless mode
        // in 0.9.x
Evan Prodromou's avatar
Evan Prodromou committed
802

Evan Prodromou's avatar
Evan Prodromou committed
803
        $ni = array();
Evan Prodromou's avatar
Evan Prodromou committed
804

Evan Prodromou's avatar
Evan Prodromou committed
805 806 807
        foreach ($users as $id) {
            $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
        }
Evan Prodromou's avatar
Evan Prodromou committed
808

Evan Prodromou's avatar
Evan Prodromou committed
809 810
        foreach ($groups as $group) {
            $users = $group->getUserMembers();
811
            foreach ($users as $id) {
Evan Prodromou's avatar
Evan Prodromou committed
812
                if (!array_key_exists($id, $ni)) {
813
                    $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
Evan Prodromou's avatar
Evan Prodromou committed
814 815
                }
            }
Evan Prodromou's avatar
Evan Prodromou committed
816
        }
Evan Prodromou's avatar
Evan Prodromou committed
817

Evan Prodromou's avatar
Evan Prodromou committed
818 819
        foreach ($recipients as $recipient) {
            if (!array_key_exists($recipient, $ni)) {
820 821 822 823 824 825 826 827 828 829
                $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
            }
        }

        // Exclude any deleted, non-local, or blocking recipients.
        $profile = $this->getProfile();
        foreach ($ni as $id => $source) {
            $user = User::staticGet('id', $id);
            if (empty($user) || $user->hasBlocked($profile)) {
                unset($ni[$id]);
Evan Prodromou's avatar
Evan Prodromou committed
830 831 832
            }
        }

Evan Prodromou's avatar
Evan Prodromou committed
833 834 835 836 837
        if (!empty($c)) {
            // XXX: pack this data better
            $c->set(common_cache_key('notice:who_gets:'.$this->id), $ni);
        }

Evan Prodromou's avatar
Evan Prodromou committed
838 839 840
        return $ni;
    }

841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856
    /**
     * Adds this notice to the inboxes of each local user who should receive
     * it, based on author subscriptions, group memberships, and @-replies.
     *
     * Warning: running a second time currently will make items appear
     * multiple times in users' inboxes.
     *
     * @fixme make more robust against errors
     * @fixme break up massive deliveries to smaller background tasks
     *
     * @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
     */
    function addToInboxes($groups=null, $recipients=null)
Evan Prodromou's avatar
Evan Prodromou committed
857
    {
858
        $ni = $this->whoGets($groups, $recipients);
Evan Prodromou's avatar
Evan Prodromou committed
859

860 861 862 863 864 865 866 867 868 869 870 871 872 873
        $ids = array_keys($ni);

        // We remove the author (if they're a local user),
        // since we'll have already done this in distribute()

        $i = array_search($this->profile_id, $ids);

        if ($i !== false) {
            unset($ids[$i]);
        }

        // Bulk insert

        Inbox::bulkInsert($this->id, $ids);
Evan Prodromou's avatar
Evan Prodromou committed
874

875 876
        return;
    }
Evan Prodromou's avatar
Evan Prodromou committed
877

878 879 880
    function getSubscribedUsers()
    {
        $user = new User();
Evan Prodromou's avatar
Evan Prodromou committed
881

882
        if(common_config('db','quote_identifiers'))
Evan Prodromou's avatar
Evan Prodromou committed
883
          $user_table = '"user"';
884
        else $user_table = 'user';
Evan Prodromou's avatar
Evan Prodromou committed
885

886
        $qry =
Evan Prodromou's avatar
Evan Prodromou committed
887 888 889 890 891
          'SELECT id ' .
          'FROM '. $user_table .' JOIN subscription '.
          'ON '. $user_table .'.id = subscription.subscriber ' .
          'WHERE subscription.subscribed = %d ';

892
        $user->query(sprintf($qry, $this->profile_id));
Evan Prodromou's avatar
Evan Prodromou committed
893

894
        $ids = array();
Evan Prodromou's avatar
Evan Prodromou committed
895

896 897 898
        while ($user->fetch()) {
            $ids[] = $user->id;
        }
Evan Prodromou's avatar
Evan Prodromou committed
899

900
        $user->free();
Evan Prodromou's avatar
Evan Prodromou committed
901

902 903
        return $ids;
    }
Evan Prodromou's avatar
Evan Prodromou committed
904

905
    /**
906 907 908 909 910 911 912 913 914 915 916
     * Record this notice to the given group inboxes for delivery.
     * Overrides the regular parsing of !group markup.
     *
     * @param string $group_ids
     * @fixme might prefer URIs as identifiers, as for replies?
     *        best with generalizations on user_group to support
     *        remote groups better.
     */
    function saveKnownGroups($group_ids)
    {
        if (!is_array($group_ids)) {
917
            // TRANS: Server exception thrown when no array is provided to the method saveKnownGroups().
Siebrand Mazeland's avatar
Siebrand Mazeland committed
918
            throw new ServerException(_('Bad type provided to saveKnownGroups.'));
919 920 921
        }

        $groups = array();
922
        foreach (array_unique($group_ids) as $id) {
923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942
            $group = User_group::staticGet('id', $id);
            if ($group) {
                common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname");
                $result = $this->addToGroupInbox($group);
                if (!$result) {
                    common_log_db_error($gi, 'INSERT', __FILE__);
                }

                // @fixme should we save the tags here or not?
                $groups[] = clone($group);
            } else {
                common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist");
            }
        }

        return $groups;
    }

    /**
     * Parse !group delivery and record targets into group_inbox.
943 944
     * @return array of Group objects
     */
945 946
    function saveGroups()
    {
947 948 949 950 951 952
        // Don't save groups for repeats

        if (!empty($this->repeat_of)) {
            return array();
        }

Evan Prodromou's avatar
Evan Prodromou committed
953
        $groups = array();
Evan Prodromou's avatar
Evan Prodromou committed
954

955 956 957 958 959
        /* extract all !group */
        $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/',
                                strtolower($this->content),
                                $match);
        if (!$count) {
Evan Prodromou's avatar
Evan Prodromou committed
960
            return $groups;
961
        }
Evan Prodromou's avatar
Evan Prodromou committed
962

963
        $profile = $this->getProfile();
Evan Prodromou's avatar
Evan Prodromou committed
964

965
        /* Add them to the database */
Evan Prodromou's avatar
Evan Prodromou committed
966

967 968
        foreach (array_unique($match[1]) as $nickname) {
            /* XXX: remote groups. */
969
            $group = User_group::getForNickname($nickname, $profile);
Evan Prodromou's avatar
Evan Prodromou committed
970

971
            if (empty($group)) {
972 973
                continue;
            }
Evan Prodromou's avatar
Evan Prodromou committed
974

975
            // we automatically add a tag for every group name, too
Evan Prodromou's avatar
Evan Prodromou committed
976

977
            $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname),
978
                                             'notice_id' => $this->id));
Evan Prodromou's avatar
Evan Prodromou committed
979

980 981 982
            if (is_null($tag)) {
                $this->saveTag($nickname);
            }
Evan Prodromou's avatar
Evan Prodromou committed
983

984
            if ($profile->isMember($group)) {
Evan Prodromou's avatar
Evan Prodromou committed
985