Notice.php 17.5 KB
Newer Older
Evan Prodromou's avatar
Evan Prodromou committed
1
<?php
Evan Prodromou's avatar
Evan Prodromou committed
2
/*
Evan Prodromou's avatar
Evan Prodromou committed
3 4
 * Laconica - a distributed open-source microblogging tool
 * Copyright (C) 2008, Controlez-Vous, 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
 */
Evan Prodromou's avatar
Evan Prodromou committed
19

Evan Prodromou's avatar
Evan Prodromou committed
20
if (!defined('LACONICA')) { exit(1); }
Evan Prodromou's avatar
Evan Prodromou committed
21

Evan Prodromou's avatar
Evan Prodromou committed
22 23 24
/**
 * Table Definition for notice
 */
25
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
Evan Prodromou's avatar
Evan Prodromou committed
26

27 28 29 30 31
/* We keep the first three 20-notice pages, plus one for pagination check,
 * in the memcached cache. */

define('NOTICE_CACHE_WINDOW', 61);

32
class Notice extends Memcached_DataObject
Evan Prodromou's avatar
Evan Prodromou committed
33
{
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'notice';                             // table name
    public $id;                                 // int(4)    primary_key not_null
    public $profile_id;                         // int(4)     not_null
    public $uri;                             // varchar(255)  unique_key
    public $content;                         // varchar(140)
    public $rendered;                         // text()
    public $url;                             // varchar(255)
    public $created;                         // datetime()     not_null
    public $modified;                         // timestamp()      not_null default_CURRENT_TIMESTAMP
    public $reply_to;                         // int(4)
    public $is_local;                         // tinyint(1)
    public $source;                             // varchar(32)

    /* Static get */
Evan Prodromou's avatar
Evan Prodromou committed
51
    function staticGet($k,$v=null) { return Memcached_DataObject::staticGet('Notice',$k,$v); }
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    function getProfile() {
        return Profile::staticGet('id', $this->profile_id);
    }

    function delete() {
        $this->blowCaches(true);
        $this->blowFavesCache(true);
        $this->blowInboxes();
        return parent::delete();
    }

    function saveTags() {
        /* extract all #hastags */
        $count = preg_match_all('/(?:^|\s)#([A-Za-z0-9_\-\.]{1,64})/', strtolower($this->content), $match);
        if (!$count) {
            return true;
        }

        /* elide characters we don't want in the tag */
        $match[1] = str_replace(array('-', '_', '.'), '', $match[1]);

        /* Add them to the database */
        foreach(array_unique($match[1]) as $hashtag) {
            $tag = DB_DataObject::factory('Notice_tag');
            $tag->notice_id = $this->id;
            $tag->tag = $hashtag;
            $tag->created = $this->created;
            $id = $tag->insert();
            if (!$id) {
                $last_error = PEAR::getStaticProperty('DB_DataObject','lastError');
                common_log(LOG_ERR, 'DB error inserting hashtag: ' . $last_error->message);
                common_server_error(sprintf(_('DB error inserting hashtag: %s'), $last_error->message));
                return;
            }
        }
        return true;
    }

Evan Prodromou's avatar
Evan Prodromou committed
94
    static function saveNew($profile_id, $content, $source=null, $is_local=1, $reply_to=null, $uri=null) {
95 96

        $profile = Profile::staticGet($profile_id);
97 98

        if (!$profile) {
99 100 101
            common_log(LOG_ERR, 'Problem saving notice. Unknown user.');
            return _('Problem saving notice. Unknown user.');
        }
102

103
        if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) {
Evan Prodromou's avatar
Evan Prodromou committed
104
            common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.');
105
            return _('Too many notices too fast; take a breather and post again in a few minutes.');
Evan Prodromou's avatar
Evan Prodromou committed
106 107
        }

108
        $banned = common_config('profile', 'banned');
109

110 111
        if ( in_array($profile_id, $banned) || in_array($profile->nickname, $banned)) {
            common_log(LOG_WARNING, "Attempted post from banned user: $profile->nickname (user id = $profile_id).");
112
            return _('You are banned from posting notices on this site.');
113
        }
114

115 116
        $notice = new Notice();
        $notice->profile_id = $profile_id;
117

118
        $blacklist = common_config('public', 'blacklist');
119

120
        # Blacklisted are non-false, but not 1, either
121

122 123 124 125 126
        if ($blacklist && in_array($profile_id, $blacklist)) {
            $notice->is_local = -1;
        } else {
            $notice->is_local = $is_local;
        }
127

128 129 130 131 132 133
        $notice->reply_to = $reply_to;
        $notice->created = common_sql_now();
        $notice->content = common_shorten_links($content);
        $notice->rendered = common_render_content($notice->content, $notice);
        $notice->source = $source;
        $notice->uri = $uri;
134

135
        $id = $notice->insert();
136

137 138 139 140
        if (!$id) {
            common_log_db_error($notice, 'INSERT', __FILE__);
            return _('Problem saving notice.');
        }
141

142 143 144 145
        # Update the URI after the notice is in the database
        if (!$uri) {
            $orig = clone($notice);
            $notice->uri = common_notice_uri($notice);
146

147 148 149 150 151
            if (!$notice->update($orig)) {
                common_log_db_error($notice, 'UPDATE', __FILE__);
                return _('Problem saving notice.');
            }
        }
152

153
        # XXX: do we need to change this for remote users?
154

155 156
        common_save_replies($notice);
        $notice->saveTags();
157

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

161 162 163
        if (common_config('memcached', 'enabled')) {
            $notice->blowCaches();
        }
164

165 166 167
        $notice->addToInboxes();
        return $notice;
    }
168

Evan Prodromou's avatar
Evan Prodromou committed
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
    static function checkEditThrottle($profile_id) {
        $profile = Profile::staticGet($profile_id);
        if (!$profile) {
            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;
    }

187 188 189 190 191 192 193 194 195 196 197 198 199 200 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 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
    function blowCaches($blowLast=false) {
        $this->blowSubsCache($blowLast);
        $this->blowNoticeCache($blowLast);
        $this->blowRepliesCache($blowLast);
        $this->blowPublicCache($blowLast);
        $this->blowTagCache($blowLast);
    }

    function blowTagCache($blowLast=false) {
        $cache = common_memcache();
        if ($cache) {
            $tag = new Notice_tag();
            $tag->notice_id = $this->id;
            if ($tag->find()) {
                while ($tag->fetch()) {
                    $cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag));
                    if ($blowLast) {
                        $cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag . ';last'));
                    }
                }
            }
            $tag->free();
            unset($tag);
        }
    }

    function blowSubsCache($blowLast=false) {
        $cache = common_memcache();
        if ($cache) {
            $user = new User();

            $user->query('SELECT id ' .
                         'FROM user JOIN subscription ON user.id = subscription.subscriber ' .
                         'WHERE subscription.subscribed = ' . $this->profile_id);

            while ($user->fetch()) {
                $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
                if ($blowLast) {
                    $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last'));
                }
            }
            $user->free();
            unset($user);
        }
    }

    function blowNoticeCache($blowLast=false) {
        if ($this->is_local) {
            $cache = common_memcache();
            if ($cache) {
                $cache->delete(common_cache_key('profile:notices:'.$this->profile_id));
                if ($blowLast) {
                    $cache->delete(common_cache_key('profile:notices:'.$this->profile_id.';last'));
                }
            }
        }
    }

    function blowRepliesCache($blowLast=false) {
        $cache = common_memcache();
        if ($cache) {
            $reply = new Reply();
            $reply->notice_id = $this->id;
            if ($reply->find()) {
                while ($reply->fetch()) {
                    $cache->delete(common_cache_key('user:replies:'.$reply->profile_id));
                    if ($blowLast) {
                        $cache->delete(common_cache_key('user:replies:'.$reply->profile_id.';last'));
                    }
                }
            }
            $reply->free();
            unset($reply);
        }
    }

    function blowPublicCache($blowLast=false) {
        if ($this->is_local == 1) {
            $cache = common_memcache();
            if ($cache) {
                $cache->delete(common_cache_key('public'));
                if ($blowLast) {
                    $cache->delete(common_cache_key('public').';last');
                }
            }
        }
    }

    function blowFavesCache($blowLast=false) {
        $cache = common_memcache();
        if ($cache) {
            $fave = new Fave();
            $fave->notice_id = $this->id;
            if ($fave->find()) {
                while ($fave->fetch()) {
                    $cache->delete(common_cache_key('user:faves:'.$fave->user_id));
                    if ($blowLast) {
                        $cache->delete(common_cache_key('user:faves:'.$fave->user_id.';last'));
                    }
                }
            }
            $fave->free();
            unset($fave);
        }
    }

    # XXX: too many args; we need to move to named params or even a separate
    # class for notice streams
295

Evan Prodromou's avatar
Evan Prodromou committed
296
    static function getStream($qry, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $order=null, $since=null) {
297

298
        if (common_config('memcached', 'enabled')) {
299

300 301 302 303 304 305 306
            # Skip the cache if this is a since, since_id or before_id qry
            if ($since_id > 0 || $before_id > 0 || $since) {
                return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order, $since);
            } else {
                return Notice::getCachedStream($qry, $cachekey, $offset, $limit, $order);
            }
        }
Evan Prodromou's avatar
Evan Prodromou committed
307

308 309
        return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order, $since);
    }
310

311
    static function getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order, $since) {
312

313 314
        $needAnd = FALSE;
        $needWhere = TRUE;
315

316 317 318 319
        if (preg_match('/\bWHERE\b/i', $qry)) {
            $needWhere = FALSE;
            $needAnd = TRUE;
        }
320

321
        if ($since_id > 0) {
322

323 324 325 326 327 328
            if ($needWhere) {
                $qry .= ' WHERE ';
                $needWhere = FALSE;
            } else {
                $qry .= ' AND ';
            }
Evan Prodromou's avatar
Evan Prodromou committed
329

330 331
            $qry .= ' notice.id > ' . $since_id;
        }
332

333
        if ($before_id > 0) {
334

335 336 337 338 339 340
            if ($needWhere) {
                $qry .= ' WHERE ';
                $needWhere = FALSE;
            } else {
                $qry .= ' AND ';
            }
341

342 343
            $qry .= ' notice.id < ' . $before_id;
        }
344

345
        if ($since) {
346

347 348 349 350 351 352
            if ($needWhere) {
                $qry .= ' WHERE ';
                $needWhere = FALSE;
            } else {
                $qry .= ' AND ';
            }
353

354 355
            $qry .= ' notice.created > \'' . date('Y-m-d H:i:s', $since) . '\'';
        }
356

357
        # Allow ORDER override
358

359 360 361 362 363
        if ($order) {
            $qry .= $order;
        } else {
            $qry .= ' ORDER BY notice.created DESC, notice.id DESC ';
        }
364

365 366 367 368 369
        if (common_config('db','type') == 'pgsql') {
            $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
        } else {
            $qry .= ' LIMIT ' . $offset . ', ' . $limit;
        }
370

371
        $notice = new Notice();
Evan Prodromou's avatar
Evan Prodromou committed
372

373
        $notice->query($qry);
374

375 376
        return $notice;
    }
Evan Prodromou's avatar
Evan Prodromou committed
377

378 379
    # XXX: this is pretty long and should probably be broken up into
    # some helper functions
Evan Prodromou's avatar
Evan Prodromou committed
380

381
    static function getCachedStream($qry, $cachekey, $offset, $limit, $order) {
Evan Prodromou's avatar
Evan Prodromou committed
382

383
        # If outside our cache window, just go to the DB
Evan Prodromou's avatar
Evan Prodromou committed
384

385
        if ($offset + $limit > NOTICE_CACHE_WINDOW) {
Evan Prodromou's avatar
Evan Prodromou committed
386
            return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
387
        }
Evan Prodromou's avatar
Evan Prodromou committed
388

389
        # Get the cache; if we can't, just go to the DB
Evan Prodromou's avatar
Evan Prodromou committed
390

391
        $cache = common_memcache();
Evan Prodromou's avatar
Evan Prodromou committed
392

393
        if (!$cache) {
Evan Prodromou's avatar
Evan Prodromou committed
394
            return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
395
        }
396

397
        # Get the notices out of the cache
398

399
        $notices = $cache->get(common_cache_key($cachekey));
400

401
        # On a cache hit, return a DB-object-like wrapper
Evan Prodromou's avatar
Evan Prodromou committed
402

403 404 405 406
        if ($notices !== FALSE) {
            $wrapper = new NoticeWrapper(array_slice($notices, $offset, $limit));
            return $wrapper;
        }
407

408 409 410
        # If the cache was invalidated because of new data being
        # added, we can try and just get the new stuff. We keep an additional
        # copy of the data at the key + ';last'
411

412
        # No cache hit. Try to get the *last* cached version
413

414
        $last_notices = $cache->get(common_cache_key($cachekey) . ';last');
415

416
        if ($last_notices) {
417

418
            # Reverse-chron order, so last ID is last.
419

420
            $last_id = $last_notices[0]->id;
421

422 423
            # XXX: this assumes monotonically increasing IDs; a fair
            # bet with our DB.
Evan Prodromou's avatar
Evan Prodromou committed
424

425
            $new_notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW,
Evan Prodromou's avatar
Evan Prodromou committed
426
                                                  $last_id, null, $order, null);
427

428 429 430 431 432 433 434 435
            if ($new_notice) {
                $new_notices = array();
                while ($new_notice->fetch()) {
                    $new_notices[] = clone($new_notice);
                }
                $new_notice->free();
                $notices = array_slice(array_merge($new_notices, $last_notices),
                                       0, NOTICE_CACHE_WINDOW);
436

437
                # Store the array in the cache for next time
438

439 440
                $result = $cache->set(common_cache_key($cachekey), $notices);
                $result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
441

442
                # return a wrapper of the array for use now
443

444 445 446
                return new NoticeWrapper(array_slice($notices, $offset, $limit));
            }
        }
447

448
        # Otherwise, get the full cache window out of the DB
Evan Prodromou's avatar
Evan Prodromou committed
449

Evan Prodromou's avatar
Evan Prodromou committed
450
        $notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, null, null, $order, null);
451

452
        # If there are no hits, just return the value
453

454 455 456
        if (!$notice) {
            return $notice;
        }
Evan Prodromou's avatar
Evan Prodromou committed
457

458
        # Pack results into an array
459

460
        $notices = array();
461

462 463 464
        while ($notice->fetch()) {
            $notices[] = clone($notice);
        }
Evan Prodromou's avatar
Evan Prodromou committed
465

466
        $notice->free();
467

468
        # Store the array in the cache for next time
Evan Prodromou's avatar
Evan Prodromou committed
469

470 471
        $result = $cache->set(common_cache_key($cachekey), $notices);
        $result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
472

473
        # return a wrapper of the array for use now
474

475 476 477 478 479
        $wrapper = new NoticeWrapper(array_slice($notices, $offset, $limit));

        return $wrapper;
    }

Evan Prodromou's avatar
Evan Prodromou committed
480
    function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) {
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498

        $parts = array();

        $qry = 'SELECT * FROM notice ';

        if (common_config('public', 'localonly')) {
            $parts[] = 'is_local = 1';
        } else {
            # -1 == blacklisted
            $parts[] = 'is_local != -1';
        }

        if ($parts) {
            $qry .= ' WHERE ' . implode(' AND ', $parts);
        }

        return Notice::getStream($qry,
                                 'public',
Evan Prodromou's avatar
Evan Prodromou committed
499
                                 $offset, $limit, $since_id, $before_id, null, $since);
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536
    }

    function addToInboxes() {
        $enabled = common_config('inboxes', 'enabled');

        if ($enabled === true || $enabled === 'transitional') {
            $inbox = new Notice_inbox();
            $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' .
              'SELECT user.id, ' . $this->id . ', "' . $this->created . '" ' .
              'FROM user JOIN subscription ON user.id = subscription.subscriber ' .
              'WHERE subscription.subscribed = ' . $this->profile_id . ' ' .
              'AND NOT EXISTS (SELECT user_id, notice_id ' .
              'FROM notice_inbox ' .
              'WHERE user_id = user.id ' .
              'AND notice_id = ' . $this->id . ' )';
            if ($enabled === 'transitional') {
                $qry .= ' AND user.inboxed = 1';
            }
            $inbox->query($qry);
        }
        return;
    }

    # Delete from inboxes if we're deleted.

    function blowInboxes() {

        $enabled = common_config('inboxes', 'enabled');

        if ($enabled === true || $enabled === 'transitional') {
            $inbox = new Notice_inbox();
            $inbox->notice_id = $this->id;
            $inbox->delete();
        }

        return;
    }
537

Evan Prodromou's avatar
Evan Prodromou committed
538
}
539