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

Notice.php 13.8 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 13 14
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
Evan Prodromou's avatar
Evan Prodromou committed
15
 *
Evan Prodromou's avatar
Evan Prodromou committed
16 17 18
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
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
{
    ###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
40
    public $uri;                             // varchar(255)  unique_key
Evan Prodromou's avatar
Evan Prodromou committed
41 42 43
    public $content;                         // varchar(140)
    public $rendered;                        // text()
    public $url;                             // varchar(255)
Evan Prodromou's avatar
Evan Prodromou committed
44 45
    public $created;                         // datetime()   not_null
    public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
Evan Prodromou's avatar
Evan Prodromou committed
46 47 48
    public $reply_to;                        // int(4)
    public $is_local;                        // tinyint(1)
    public $source;                          // varchar(32)
Evan Prodromou's avatar
Evan Prodromou committed
49 50

    /* Static get */
51
    function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('Notice',$k,$v); }
Evan Prodromou's avatar
Evan Prodromou committed
52 53 54

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

	function getProfile() {
57
		return Profile::staticGet('id', $this->profile_id);
Evan Prodromou's avatar
Evan Prodromou committed
58
	}
Mike Cochrane's avatar
Mike Cochrane committed
59

60
	function delete() {
61 62
		$this->blowCaches(true);
		$this->blowFavesCache(true);
Evan Prodromou's avatar
Evan Prodromou committed
63
		$this->blowInboxes();
64 65
		parent::delete();
	}
66

Mike Cochrane's avatar
Mike Cochrane committed
67 68
	function saveTags() {
		/* extract all #hastags */
Garret Buell's avatar
Garret Buell committed
69
		$count = preg_match_all('/(?:^|\s)#([A-Za-z0-9_\-\.]{1,64})/', strtolower($this->content), $match);
Mike Cochrane's avatar
Mike Cochrane committed
70 71 72
		if (!$count) {
			return true;
		}
73

Garret Buell's avatar
Garret Buell committed
74 75
		/* elide characters we don't want in the tag */
		$match[1] = str_replace(array('-', '_', '.'), '', $match[1]);
Mike Cochrane's avatar
Mike Cochrane committed
76 77 78 79 80 81 82 83 84 85

		/* 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');
Evan Prodromou's avatar
Evan Prodromou committed
86
				common_log(LOG_ERR, 'DB error inserting hashtag: ' . $last_error->message);
Mike Cochrane's avatar
Mike Cochrane committed
87 88 89 90 91 92
				common_server_error(sprintf(_('DB error inserting hashtag: %s'), $last_error->message));
				return;
			}
		}
		return true;
	}
93 94

	static function saveNew($profile_id, $content, $source=NULL, $is_local=1, $reply_to=NULL, $uri=NULL) {
95

Evan Prodromou's avatar
Evan Prodromou committed
96 97 98 99 100
        if (!Notice::checkEditThrottle($profile_id)) {
            common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.');
			return _('Too many notices too fast; take a breather and post again in a few minutes.');
        }

101 102
		$notice = new Notice();
		$notice->profile_id = $profile_id;
103 104 105 106 107 108 109 110 111 112 113

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

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

		if ($blacklist && in_array($profile_id, $blacklist)) {
			$notice->is_local = -1;
		} else {
			$notice->is_local = $is_local;
		}

114
		$notice->reply_to = $reply_to;
115
		$notice->created = common_sql_now();
116 117
		$notice->content = $content;
		$notice->rendered = common_render_content($notice->content, $notice);
Ori Avtalion's avatar
Ori Avtalion committed
118 119
		$notice->source = $source;
		$notice->uri = $uri;
120

121 122 123
		$id = $notice->insert();

		if (!$id) {
124
			common_log_db_error($notice, 'INSERT', __FILE__);
125 126 127
			return _('Problem saving notice.');
		}

Ori Avtalion's avatar
Ori Avtalion committed
128 129 130
		# Update the URI after the notice is in the database
		if (!$uri) {
			$orig = clone($notice);
131
			$notice->uri = common_notice_uri($notice);
132

Ori Avtalion's avatar
Ori Avtalion committed
133
			if (!$notice->update($orig)) {
134
				common_log_db_error($notice, 'UPDATE', __FILE__);
Ori Avtalion's avatar
Ori Avtalion committed
135 136
				return _('Problem saving notice.');
			}
137 138
		}

139
		# XXX: do we need to change this for remote users?
140

141 142
		common_save_replies($notice);
		$notice->saveTags();
143 144 145

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

147
		if (common_config('memcached', 'enabled')) {
148
			$notice->blowCaches();
149
		}
150 151

		$notice->addToInboxes();
152 153
		return $notice;
	}
154

Evan Prodromou's avatar
Evan Prodromou committed
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
    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;
    }

173 174 175 176 177 178
	function blowCaches($blowLast=false) {
		$this->blowSubsCache($blowLast);
		$this->blowNoticeCache($blowLast);
		$this->blowRepliesCache($blowLast);
		$this->blowPublicCache($blowLast);
		$this->blowTagCache($blowLast);
179 180
	}

181
	function blowTagCache($blowLast=false) {
182 183 184 185 186 187
		$cache = common_memcache();
		if ($cache) {
			$tag = new Notice_tag();
			$tag->notice_id = $this->id;
			if ($tag->find()) {
				while ($tag->fetch()) {
Evan Prodromou's avatar
Evan Prodromou committed
188
					$cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag));
189 190 191
					if ($blowLast) {
						$cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag . ';last'));
					}
192 193 194 195 196
				}
			}
			$tag->free();
			unset($tag);
		}
197
	}
198

199
	function blowSubsCache($blowLast=false) {
200 201
		$cache = common_memcache();
		if ($cache) {
202
			$user = new User();
203

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

208 209
			while ($user->fetch()) {
				$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
210 211 212
				if ($blowLast) {
					$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last'));
				}
213 214 215 216 217
			}
			$user->free();
			unset($user);
		}
	}
218

219
	function blowNoticeCache($blowLast=false) {
220 221 222
		if ($this->is_local) {
			$cache = common_memcache();
			if ($cache) {
Evan Prodromou's avatar
Evan Prodromou committed
223
				$cache->delete(common_cache_key('profile:notices:'.$this->profile_id));
224
				if ($blowLast) {
Evan Prodromou's avatar
Evan Prodromou committed
225
					$cache->delete(common_cache_key('profile:notices:'.$this->profile_id.';last'));
226
				}
227 228 229 230
			}
		}
	}

231
	function blowRepliesCache($blowLast=false) {
232 233 234 235 236 237 238
		$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));
239 240 241
					if ($blowLast) {
						$cache->delete(common_cache_key('user:replies:'.$reply->profile_id.';last'));
					}
242 243 244 245 246 247 248
				}
			}
			$reply->free();
			unset($reply);
		}
	}

249
	function blowPublicCache($blowLast=false) {
250
		if ($this->is_local == 1) {
251 252 253
			$cache = common_memcache();
			if ($cache) {
				$cache->delete(common_cache_key('public'));
254 255 256
				if ($blowLast) {
					$cache->delete(common_cache_key('public').';last');
				}
257 258 259 260
			}
		}
	}

261
	function blowFavesCache($blowLast=false) {
262 263 264 265 266 267 268
		$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));
269 270 271
					if ($blowLast) {
						$cache->delete(common_cache_key('user:faves:'.$fave->user_id.';last'));
					}
272 273 274 275 276 277
				}
			}
			$fave->free();
			unset($fave);
		}
	}
278

279 280
	# XXX: too many args; we need to move to named params or even a separate
	# class for notice streams
Evan Prodromou's avatar
Evan Prodromou committed
281

282
	static function getStream($qry, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $order=NULL) {
283

284
		if (common_config('memcached', 'enabled')) {
285 286 287

			# Skip the cache if this is a since_id or before_id qry
			if ($since_id > 0 || $before_id > 0) {
288
				return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order);
289
			} else {
290
				return Notice::getCachedStream($qry, $cachekey, $offset, $limit, $order);
291
			}
292
		}
293

294
		return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order);
295 296
	}

297
	static function getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order) {
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330

		$needAnd = FALSE;
	  	$needWhere = TRUE;

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

		if ($since_id > 0) {

			if ($needWhere) {
		    	$qry .= ' WHERE ';
				$needWhere = FALSE;
			} else {
				$qry .= ' AND ';
			}

		    $qry .= ' notice.id > ' . $since_id;
		}

		if ($before_id > 0) {

			if ($needWhere) {
		    	$qry .= ' WHERE ';
				$needWhere = FALSE;
			} else {
				$qry .= ' AND ';
			}

			$qry .= ' notice.id < ' . $before_id;
		}

331
		# Allow ORDER override
Evan Prodromou's avatar
Evan Prodromou committed
332

333 334 335 336 337
		if ($order) {
			$qry .= $order;
		} else {
			$qry .= ' ORDER BY notice.created DESC, notice.id DESC ';
		}
338 339

		if (common_config('db','type') == 'pgsql') {
340
			$qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
341
		} else {
342
			$qry .= ' LIMIT ' . $offset . ', ' . $limit;
343 344 345 346 347
		}

		$notice = new Notice();

		$notice->query($qry);
348

349 350
		return $notice;
	}
351

352 353
	# XXX: this is pretty long and should probably be broken up into
	# some helper functions
Evan Prodromou's avatar
Evan Prodromou committed
354

355
	static function getCachedStream($qry, $cachekey, $offset, $limit, $order) {
356 357

		# If outside our cache window, just go to the DB
358

359
		if ($offset + $limit > NOTICE_CACHE_WINDOW) {
360
			return Notice::getStreamDirect($qry, $offset, $limit, NULL, NULL, $order);
361 362 363
		}

		# Get the cache; if we can't, just go to the DB
364

365
		$cache = common_memcache();
366

367
		if (!$cache) {
368
			return Notice::getStreamDirect($qry, $offset, $limit, NULL, NULL, $order);
369 370 371
		}

		# Get the notices out of the cache
372

373
		$notices = $cache->get(common_cache_key($cachekey));
374

375
		# On a cache hit, return a DB-object-like wrapper
376

377
		if ($notices !== FALSE) {
378 379 380 381
			$wrapper = new NoticeWrapper(array_slice($notices, $offset, $limit));
			return $wrapper;
		}

382 383 384
		# 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'
Evan Prodromou's avatar
Evan Prodromou committed
385

386 387 388
		# No cache hit. Try to get the *last* cached version

		$last_notices = $cache->get(common_cache_key($cachekey) . ';last');
Evan Prodromou's avatar
Evan Prodromou committed
389

390
		if ($last_notices) {
Evan Prodromou's avatar
Evan Prodromou committed
391

392
			# Reverse-chron order, so last ID is last.
Evan Prodromou's avatar
Evan Prodromou committed
393

394
			$last_id = $last_notices[0]->id;
Evan Prodromou's avatar
Evan Prodromou committed
395

396 397
			# XXX: this assumes monotonically increasing IDs; a fair
			# bet with our DB.
Evan Prodromou's avatar
Evan Prodromou committed
398

399 400
			$new_notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW,
												  $last_id, NULL, $order);
Evan Prodromou's avatar
Evan Prodromou committed
401

402 403 404 405 406 407 408 409
			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);
Evan Prodromou's avatar
Evan Prodromou committed
410

411 412 413 414 415 416 417 418 419 420
				# Store the array in the cache for next time

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

				# return a wrapper of the array for use now

				return new NoticeWrapper(array_slice($notices, $offset, $limit));
			}
		}
Evan Prodromou's avatar
Evan Prodromou committed
421

422 423
		# Otherwise, get the full cache window out of the DB

424
		$notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, NULL, NULL, $order);
425

426
		# If there are no hits, just return the value
427

428 429 430 431 432
		if (!$notice) {
			return $notice;
		}

		# Pack results into an array
433

434 435 436 437 438 439
		$notices = array();

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

440
		$notice->free();
Evan Prodromou's avatar
Evan Prodromou committed
441

442
		# Store the array in the cache for next time
443

444
		$result = $cache->set(common_cache_key($cachekey), $notices);
445
		$result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
446

447
		# return a wrapper of the array for use now
448

449
		$wrapper = new NoticeWrapper(array_slice($notices, $offset, $limit));
450

451 452
		return $wrapper;
	}
453

454
	function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0) {
455

456
		$parts = array();
Evan Prodromou's avatar
Evan Prodromou committed
457

458 459 460
		$qry = 'SELECT * FROM notice ';

		if (common_config('public', 'localonly')) {
461
			$parts[] = 'is_local = 1';
462 463 464
		} else {
			# -1 == blacklisted
			$parts[] = 'is_local != -1';
465 466
		}

467 468 469
		if ($parts) {
			$qry .= ' WHERE ' . implode(' AND ', $parts);
		}
Evan Prodromou's avatar
Evan Prodromou committed
470

471 472
		return Notice::getStream($qry,
								 'public',
473
								 $offset, $limit, $since_id, $before_id);
474
	}
475

476
	function addToInboxes() {
477 478 479 480 481 482 483
		$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 ' .
484 485 486
			  'WHERE subscription.subscribed = ' . $this->profile_id . ' ' .
			  'AND NOT EXISTS (SELECT user_id, notice_id ' .
			  'FROM notice_inbox ' .
Evan Prodromou's avatar
Evan Prodromou committed
487
			  'WHERE user_id = user.id ' .
488
			  'AND notice_id = ' . $this->id . ' )';
489 490 491 492 493
			if ($enabled === 'transitional') {
				$qry .= ' AND user.inboxed = 1';
			}
			$inbox->query($qry);
		}
494 495
		return;
	}
Evan Prodromou's avatar
Evan Prodromou committed
496 497

	# Delete from inboxes if we're deleted.
498

Evan Prodromou's avatar
Evan Prodromou committed
499 500
	function blowInboxes() {

501 502 503 504 505 506 507
		$enabled = common_config('inboxes', 'enabled');

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

Evan Prodromou's avatar
Evan Prodromou committed
509 510
		return;
	}
511

Evan Prodromou's avatar
Evan Prodromou committed
512
}
513