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

Notice.php 14 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 96 97 98
        if (!Profile::staticGet($profile_id)) {
            common_log(LOG_ERR, 'Problem saving notice. Unknown user.');
            return _('Problem saving notice. Unknown user.');
        }
99

Evan Prodromou's avatar
Evan Prodromou committed
100 101 102 103 104
        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.');
        }

105 106
		$notice = new Notice();
		$notice->profile_id = $profile_id;
107 108 109 110 111 112 113 114 115 116 117

		$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;
		}

118
		$notice->reply_to = $reply_to;
119
		$notice->created = common_sql_now();
120 121
		$notice->content = $content;
		$notice->rendered = common_render_content($notice->content, $notice);
Ori Avtalion's avatar
Ori Avtalion committed
122 123
		$notice->source = $source;
		$notice->uri = $uri;
124

125 126 127
		$id = $notice->insert();

		if (!$id) {
128
			common_log_db_error($notice, 'INSERT', __FILE__);
129 130 131
			return _('Problem saving notice.');
		}

Ori Avtalion's avatar
Ori Avtalion committed
132 133 134
		# Update the URI after the notice is in the database
		if (!$uri) {
			$orig = clone($notice);
135
			$notice->uri = common_notice_uri($notice);
136

Ori Avtalion's avatar
Ori Avtalion committed
137
			if (!$notice->update($orig)) {
138
				common_log_db_error($notice, 'UPDATE', __FILE__);
Ori Avtalion's avatar
Ori Avtalion committed
139 140
				return _('Problem saving notice.');
			}
141 142
		}

143
		# XXX: do we need to change this for remote users?
144

145 146
		common_save_replies($notice);
		$notice->saveTags();
147 148 149

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

151
		if (common_config('memcached', 'enabled')) {
152
			$notice->blowCaches();
153
		}
154 155

		$notice->addToInboxes();
156 157
		return $notice;
	}
158

Evan Prodromou's avatar
Evan Prodromou committed
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
    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;
    }

177 178 179 180 181 182
	function blowCaches($blowLast=false) {
		$this->blowSubsCache($blowLast);
		$this->blowNoticeCache($blowLast);
		$this->blowRepliesCache($blowLast);
		$this->blowPublicCache($blowLast);
		$this->blowTagCache($blowLast);
183 184
	}

185
	function blowTagCache($blowLast=false) {
186 187 188 189 190 191
		$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
192
					$cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag));
193 194 195
					if ($blowLast) {
						$cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag . ';last'));
					}
196 197 198 199 200
				}
			}
			$tag->free();
			unset($tag);
		}
201
	}
202

203
	function blowSubsCache($blowLast=false) {
204 205
		$cache = common_memcache();
		if ($cache) {
206
			$user = new User();
207

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

212 213
			while ($user->fetch()) {
				$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
214 215 216
				if ($blowLast) {
					$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last'));
				}
217 218 219 220 221
			}
			$user->free();
			unset($user);
		}
	}
222

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

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

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

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

283 284
	# 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
285

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

288
		if (common_config('memcached', 'enabled')) {
289 290 291

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

298
		return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order);
299 300
	}

301
	static function getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order) {
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 331 332 333 334

		$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;
		}

335
		# Allow ORDER override
Evan Prodromou's avatar
Evan Prodromou committed
336

337 338 339 340 341
		if ($order) {
			$qry .= $order;
		} else {
			$qry .= ' ORDER BY notice.created DESC, notice.id DESC ';
		}
342 343

		if (common_config('db','type') == 'pgsql') {
344
			$qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
345
		} else {
346
			$qry .= ' LIMIT ' . $offset . ', ' . $limit;
347 348 349 350 351
		}

		$notice = new Notice();

		$notice->query($qry);
352

353 354
		return $notice;
	}
355

356 357
	# XXX: this is pretty long and should probably be broken up into
	# some helper functions
Evan Prodromou's avatar
Evan Prodromou committed
358

359
	static function getCachedStream($qry, $cachekey, $offset, $limit, $order) {
360 361

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

363
		if ($offset + $limit > NOTICE_CACHE_WINDOW) {
364
			return Notice::getStreamDirect($qry, $offset, $limit, NULL, NULL, $order);
365 366 367
		}

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

369
		$cache = common_memcache();
370

371
		if (!$cache) {
372
			return Notice::getStreamDirect($qry, $offset, $limit, NULL, NULL, $order);
373 374 375
		}

		# Get the notices out of the cache
376

377
		$notices = $cache->get(common_cache_key($cachekey));
378

379
		# On a cache hit, return a DB-object-like wrapper
380

381
		if ($notices !== FALSE) {
382 383 384 385
			$wrapper = new NoticeWrapper(array_slice($notices, $offset, $limit));
			return $wrapper;
		}

386 387 388
		# 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
389

390 391 392
		# 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
393

394
		if ($last_notices) {
Evan Prodromou's avatar
Evan Prodromou committed
395

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

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

400 401
			# XXX: this assumes monotonically increasing IDs; a fair
			# bet with our DB.
Evan Prodromou's avatar
Evan Prodromou committed
402

403 404
			$new_notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW,
												  $last_id, NULL, $order);
Evan Prodromou's avatar
Evan Prodromou committed
405

406 407 408 409 410 411 412 413
			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
414

415 416 417 418 419 420 421 422 423 424
				# 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
425

426 427
		# Otherwise, get the full cache window out of the DB

428
		$notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, NULL, NULL, $order);
429

430
		# If there are no hits, just return the value
431

432 433 434 435 436
		if (!$notice) {
			return $notice;
		}

		# Pack results into an array
437

438 439 440 441 442 443
		$notices = array();

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

444
		$notice->free();
Evan Prodromou's avatar
Evan Prodromou committed
445

446
		# Store the array in the cache for next time
447

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

451
		# return a wrapper of the array for use now
452

453
		$wrapper = new NoticeWrapper(array_slice($notices, $offset, $limit));
454

455 456
		return $wrapper;
	}
457

458
	function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0) {
459

460
		$parts = array();
Evan Prodromou's avatar
Evan Prodromou committed
461

462 463 464
		$qry = 'SELECT * FROM notice ';

		if (common_config('public', 'localonly')) {
465
			$parts[] = 'is_local = 1';
466 467 468
		} else {
			# -1 == blacklisted
			$parts[] = 'is_local != -1';
469 470
		}

471 472 473
		if ($parts) {
			$qry .= ' WHERE ' . implode(' AND ', $parts);
		}
Evan Prodromou's avatar
Evan Prodromou committed
474

475 476
		return Notice::getStream($qry,
								 'public',
477
								 $offset, $limit, $since_id, $before_id);
478
	}
479

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

	# Delete from inboxes if we're deleted.
502

Evan Prodromou's avatar
Evan Prodromou committed
503 504
	function blowInboxes() {

505 506 507 508 509 510 511
		$enabled = common_config('inboxes', 'enabled');

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

Evan Prodromou's avatar
Evan Prodromou committed
513 514
		return;
	}
515

Evan Prodromou's avatar
Evan Prodromou committed
516
}
517