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

Commit 0853ed06 authored by Eric Helgeson's avatar Eric Helgeson

Merge commit 'origin/0.8.x' into 0.9.x

parents 9e611b40 ff6e976d
......@@ -23,4 +23,4 @@ config-*.php
good-config.php
lac08.log
php.log
config.php.*
......@@ -964,9 +964,6 @@ sslserver: use an alternate server name for SSL URLs, like
shorturllength: Length of URL at which URLs in a message exceeding 140
characters will be sent to the user's chosen
shortening service.
design: a default design (colors and background) for the site.
Sub-items are: backgroundcolor, contentcolor, sidebarcolor,
textcolor, linkcolor, backgroundimage, disposition.
dupelimit: minimum time allowed for one person to say the same thing
twice. Default 60s. Anything lower is considered a user
or UI error.
......@@ -1432,6 +1429,20 @@ notify third-party servers of updates.
notify: an array of URLs for ping endpoints. Default is the empty
array (no notification).
design
------
Default design (colors and background) for the site. Actual appearance
depends on the theme. Null values mean to use the theme defaults.
backgroundcolor: Hex color of the site background.
contentcolor: Hex color of the content area background.
sidebarcolor: Hex color of the sidebar background.
textcolor: Hex color of all non-link text.
linkcolor: Hex color of all links.
backgroundimage: Image to use for the background.
disposition: Flags for whether or not to tile the background image.
Plugins
=======
......
......@@ -130,6 +130,7 @@ class ApiAction extends Action
'laconica/wadl',
'tags/timeline',
'oembed/oembed',
'groups/show',
'groups/timeline');
static $bareauth = array('statuses/user_timeline',
......
......@@ -167,6 +167,8 @@ class ConversationTree extends NoticeList
function _buildTree()
{
$cnt = 0;
$this->tree = array();
$this->table = array();
......
......@@ -229,7 +229,7 @@ class PublicAction extends Action
// $top->show();
$pop = new PopularNoticeSection($this);
$pop->show();
$gbp = new GroupsByPostsSection($this);
$gbp = new GroupsByMembersSection($this);
$gbp->show();
$feat = new FeaturedUsersSection($this);
$feat->show();
......
......@@ -207,32 +207,10 @@ class TwitapifavoritesAction extends TwitterapiAction
$other = User::staticGet('id', $notice->profile_id);
if ($other && $other->id != $user->id) {
if ($other->email && $other->emailnotifyfav) {
$this->notify_mail($other, $user, $notice);
mail_notify_fave($other, $user, $notice);
}
# XXX: notify by IM
# XXX: notify by SMS
}
}
function notify_mail($other, $user, $notice)
{
$profile = $user->getProfile();
$bestname = $profile->getBestName();
$subject = sprintf(_('%s added your notice as a favorite'), $bestname);
$body = sprintf(_("%1\$s just added your notice from %2\$s as one of their favorites.\n\n" .
"In case you forgot, you can see the text of your notice here:\n\n" .
"%3\$s\n\n" .
"You can see the list of %1\$s's favorites here:\n\n" .
"%4\$s\n\n" .
"Faithfully yours,\n" .
"%5\$s\n"),
$bestname,
common_exact_date($notice->created),
common_local_url('shownotice', array('notice' => $notice->id)),
common_local_url('showfavorites', array('nickname' => $user->nickname)),
common_config('site', 'name'));
mail_to_user($other, $subject, $body);
}
}
\ No newline at end of file
}
......@@ -51,6 +51,32 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
class TwitapigroupsAction extends TwitterapiAction
{
function show($args, $apidata)
{
parent::handle($args);
common_debug("in groups api action");
$this->auth_user = $apidata['user'];
$group = $this->get_group($apidata['api_arg'], $apidata);
if (empty($group)) {
$this->clientError('Not Found', 404, $apidata['content-type']);
return;
}
switch($apidata['content-type']) {
case 'xml':
$this->show_single_xml_group($group);
break;
case 'json':
$this->show_single_json_group($group);
break;
default:
$this->clientError(_('API method not found!'), $code = 404);
}
}
function timeline($args, $apidata)
{
parent::handle($args);
......@@ -88,8 +114,7 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
$this->show_xml_timeline($notice);
break;
case 'rss':
$this->show_rss_timeline($notice, $title, $link,
$subtitle, $suplink);
$this->show_rss_timeline($notice, $title, $link, $subtitle);
break;
case 'atom':
if (isset($apidata['api_arg'])) {
......@@ -101,7 +126,7 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
'api/laconica/groups/timeline.atom';
}
$this->show_atom_timeline($notice, $title, $id, $link,
$subtitle, $suplink, $selfuri);
$subtitle, null, $selfuri);
break;
case 'json':
$this->show_json_timeline($notice);
......
......@@ -88,8 +88,7 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
$this->show_xml_timeline($notice);
break;
case 'rss':
$this->show_rss_timeline($notice, $title, $link,
$subtitle, $suplink);
$this->show_rss_timeline($notice, $title, $link, $subtitle);
break;
case 'atom':
if (isset($apidata['api_arg'])) {
......@@ -101,7 +100,7 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
'api/laconica/tags/timeline.atom';
}
$this->show_atom_timeline($notice, $title, $id, $link,
$subtitle, $suplink, $selfuri);
$subtitle, null, $selfuri);
break;
case 'json':
$this->show_json_timeline($notice);
......
......@@ -55,26 +55,38 @@ class Design extends Memcached_DataObject
function showCSS($out)
{
try {
$css = '';
$bgcolor = new WebColor($this->backgroundcolor);
$ccolor = new WebColor($this->contentcolor);
$sbcolor = new WebColor($this->sidebarcolor);
$tcolor = new WebColor($this->textcolor);
$lcolor = new WebColor($this->linkcolor);
$bgcolor = Design::toWebColor($this->backgroundcolor);
} catch (WebColorException $e) {
// This shouldn't happen
common_log(LOG_ERR, "Unable to create color for design $id.",
__FILE__);
if (!empty($bgcolor)) {
$css .= 'body { background-color: #' . $bgcolor->hexValue() . ' }' . "\n";
}
$ccolor = Design::toWebColor($this->contentcolor);
if (!empty($ccolor)) {
$css .= '#content, #site_nav_local_views .current a { background-color: #';
$css .= $ccolor->hexValue() . '} '."\n";
}
$sbcolor = Design::toWebColor($this->sidebarcolor);
if (!empty($sbcolor)) {
$css .= '#aside_primary { background-color: #'. $sbcolor->hexValue() . ' }' . "\n";
}
$tcolor = Design::toWebColor($this->textcolor);
if (!empty($tcolor)) {
$css .= 'html body { color: #'. $tcolor->hexValue() . ' }'. "\n";
}
$css = 'body { background-color: #' . $bgcolor->hexValue() . ' }' . "\n";
$css .= '#content, #site_nav_local_views .current a { background-color: #';
$css .= $ccolor->hexValue() . '} '."\n";
$css .= '#aside_primary { background-color: #'. $sbcolor->hexValue() . ' }' . "\n";
$css .= 'html body { color: #'. $tcolor->hexValue() . ' }'. "\n";
$css .= 'a { color: #' . $lcolor->hexValue() . ' }' . "\n";
$lcolor = Design::toWebColor($this->linkcolor);
if (!empty($lcolor)) {
$css .= 'a { color: #' . $lcolor->hexValue() . ' }' . "\n";
}
if (!empty($this->backgroundimage) &&
$this->disposition & BACKGROUND_ON) {
......@@ -88,8 +100,25 @@ class Design extends Memcached_DataObject
'); ' . $repeat . ' background-attachment:fixed; }' . "\n";
}
$out->element('style', array('type' => 'text/css'), $css);
if (0 != mb_strlen($css)) {
$out->element('style', array('type' => 'text/css'), $css);
}
}
static function toWebColor($color)
{
if (is_null($color)) {
return null;
}
try {
return new WebColor($color);
} catch (WebColorException $e) {
// This shouldn't happen
common_log(LOG_ERR, "Unable to create color for design $id.",
__FILE__);
return null;
}
}
static function filename($id, $extension, $extra=null)
......@@ -152,4 +181,33 @@ class Design extends Memcached_DataObject
}
}
/**
* Return a design object based on the configured site design.
*
* @return Design a singleton design object for the site.
*/
static function siteDesign()
{
static $siteDesign = null;
if (empty($siteDesign)) {
$siteDesign = new Design();
$attrs = array('backgroundcolor',
'contentcolor',
'sidebarcolor',
'textcolor',
'linkcolor',
'backgroundimage',
'disposition');
foreach ($attrs as $attr) {
$siteDesign->$attr = common_config('design', $attr);
}
}
return $siteDesign;
}
}
......@@ -93,7 +93,6 @@ class File extends Memcached_DataObject
if (empty($file)) {
$file_redir = File_redirection::staticGet('url', $given_url);
if (empty($file_redir)) {
common_debug("processNew() '$given_url' not a known redirect.\n");
$redir_data = File_redirection::where($given_url);
$redir_url = $redir_data['url'];
if ($redir_url === $given_url) {
......@@ -114,7 +113,9 @@ class File extends Memcached_DataObject
if (empty($x)) {
$x = File::staticGet($file_id);
if (empty($x)) die('Impossible!');
if (empty($x)) {
throw new ServerException("Robin thinks something is impossible.");
}
}
File_to_post::processNew($file_id, $notice_id);
......
......@@ -116,15 +116,14 @@ class Notice extends Memcached_DataObject
if (!$count) {
return true;
}
//turn each into their canonical tag
//this is needed to remove dupes before saving e.g. #hash.tag = #hashtag
$hashtags = array();
for($i=0; $i<count($match[1]); $i++) {
$hashtags[] = common_canonical_tag($match[1][$i]);
$hashtags[] = common_canonical_tag($match[1][$i]);
}
/* Add them to the database */
foreach(array_unique($hashtags) as $hashtag) {
/* elide characters we don't want in the tag */
......@@ -197,29 +196,30 @@ class Notice extends Memcached_DataObject
$notice->is_local = $is_local;
}
$notice->query('BEGIN');
$notice->reply_to = $reply_to;
if (!empty($created)) {
$notice->created = $created;
} else {
$notice->created = common_sql_now();
}
$notice->content = $final;
$notice->rendered = common_render_content($final, $notice);
$notice->source = $source;
$notice->uri = $uri;
if (!empty($reply_to)) {
$reply_notice = Notice::staticGet('id', $reply_to);
if (!empty($reply_notice)) {
$notice->reply_to = $reply_to;
$notice->conversation = $reply_notice->conversation;
}
$notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final);
if (!empty($notice->reply_to)) {
$reply = Notice::staticGet('id', $notice->reply_to);
$notice->conversation = $reply->conversation;
}
if (Event::handle('StartNoticeSave', array(&$notice))) {
// XXX: some of these functions write to the DB
$notice->query('BEGIN');
$id = $notice->insert();
if (!$id) {
......@@ -227,18 +227,33 @@ class Notice extends Memcached_DataObject
return _('Problem saving notice.');
}
# Update the URI after the notice is in the database
if (!$uri) {
$orig = clone($notice);
// Update ID-dependent columns: URI, conversation
$orig = clone($notice);
$changed = false;
if (empty($uri)) {
$notice->uri = common_notice_uri($notice);
$changed = true;
}
// If it's not part of a conversation, it's
// the beginning of a new conversation.
if (empty($notice->conversation)) {
$notice->conversation = $notice->id;
$changed = true;
}
if ($changed) {
if (!$notice->update($orig)) {
common_log_db_error($notice, 'UPDATE', __FILE__);
return _('Problem saving notice.');
}
}
# XXX: do we need to change this for remote users?
// XXX: do we need to change this for remote users?
$notice->saveReplies();
$notice->saveTags();
......@@ -246,8 +261,13 @@ class Notice extends Memcached_DataObject
$notice->addToInboxes();
$notice->saveUrls();
// FIXME: why do we have to re-render the content?
// Remove this if it's not necessary.
$orig2 = clone($notice);
$notice->rendered = common_render_content($final, $notice);
$notice->rendered = common_render_content($final, $notice);
if (!$notice->update($orig2)) {
common_log_db_error($notice, 'UPDATE', __FILE__);
return _('Problem saving notice.');
......@@ -304,9 +324,9 @@ class Notice extends Memcached_DataObject
$notice->profile_id = $profile_id;
$notice->content = $content;
if (common_config('db','type') == 'pgsql')
$notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit'));
$notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit'));
else
$notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit'));
$notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit'));
$cnt = $notice->count();
return ($cnt == 0);
......@@ -920,14 +940,14 @@ class Notice extends Memcached_DataObject
{
$user = new User();
if(common_config('db','quote_identifiers'))
$user_table = '"user"';
else $user_table = 'user';
if(common_config('db','quote_identifiers'))
$user_table = '"user"';
else $user_table = 'user';
$qry =
'SELECT id ' .
'FROM '. $user_table .' JOIN subscription '.
'ON '. $user_table .'.id = subscription.subscriber ' .
'FROM '. $user_table .' JOIN subscription '.
'ON '. $user_table .'.id = subscription.subscriber ' .
'WHERE subscription.subscribed = %d ';
$user->query(sprintf($qry, $this->profile_id));
......@@ -1045,16 +1065,6 @@ class Notice extends Memcached_DataObject
if (!$recipient) {
continue;
}
if ($i == 0 && ($recipient->id != $sender->id) && !$this->reply_to) { // Don't save reply to self
$reply_for = $recipient;
$recipient_notice = $reply_for->getCurrentNotice();
if ($recipient_notice) {
$orig = clone($this);
$this->reply_to = $recipient_notice->id;
$this->conversation = $recipient_notice->conversation;
$this->update($orig);
}
}
// Don't save replies from blocked profile to local user
$recipient_user = User::staticGet('id', $recipient->id);
if ($recipient_user && $recipient_user->hasBlocked($sender)) {
......@@ -1101,14 +1111,6 @@ class Notice extends Memcached_DataObject
}
}
// If it's not a reply, make it the root of a new conversation
if (empty($this->conversation)) {
$orig = clone($this);
$this->conversation = $this->id;
$this->update($orig);
}
foreach (array_keys($replied) as $recipient) {
$user = User::staticGet('id', $recipient);
if ($user) {
......@@ -1280,4 +1282,76 @@ class Notice extends Memcached_DataObject
return $ids;
}
/**
* Determine which notice, if any, a new notice is in reply to.
*
* For conversation tracking, we try to see where this notice fits
* in the tree. Rough algorithm is:
*
* if (reply_to is set and valid) {
* return reply_to;
* } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) {
* return ID of last notice by initial @name in content;
* }
*
* Note that all @nickname instances will still be used to save "reply" records,
* so the notice shows up in the mentioned users' "replies" tab.
*
* @param integer $reply_to ID passed in by Web or API
* @param integer $profile_id ID of author
* @param string $source Source tag, like 'web' or 'gwibber'
* @param string $content Final notice content
*
* @return integer ID of replied-to notice, or null for not a reply.
*/
static function getReplyTo($reply_to, $profile_id, $source, $content)
{
static $lb = array('xmpp', 'mail', 'sms', 'omb');
// If $reply_to is specified, we check that it exists, and then
// return it if it does
if (!empty($reply_to)) {
$reply_notice = Notice::staticGet('id', $reply_to);
if (!empty($reply_notice)) {
return $reply_to;
}
}
// If it's not a "low bandwidth" source (one where you can't set
// a reply_to argument), we return. This is mostly web and API
// clients.
if (!in_array($source, $lb)) {
return null;
}
// Is there an initial @ or T?
if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match) ||
preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) {
$nickname = common_canonical_nickname($match[1]);
} else {
return null;
}
// Figure out who that is.
$sender = Profile::staticGet('id', $profile_id);
$recipient = common_relative_profile($sender, $nickname, common_sql_now());
if (empty($recipient)) {
return null;
}
// Get their last notice
$last = $recipient->getCurrentNotice();
if (!empty($last)) {
return $last->id;
}
}
}
......@@ -108,11 +108,24 @@ class Session extends Memcached_DataObject
$epoch = common_sql_date(time() - $maxlifetime);
$ids = array();
$session = new Session();
$session->whereAdd('modified < "'.$epoch.'"');
$result = $session->delete(DB_DATAOBJECT_WHEREADD_ONLY);
$session->selectAdd();
$session->selectAdd('id');
$session->find();
while ($session->fetch()) {
$ids[] = $session->id;
}
$session->free();
self::logdeb("garbage collection result = $result");
foreach ($ids as $id) {
self::destroy($id);
}
}
static function setSaveHandler()
......
......@@ -18,14 +18,14 @@ $config['site']['server'] = 'localhost';
$config['site']['path'] = 'laconica';
// $config['site']['fancy'] = false;
// $config['site']['theme'] = 'default';
// Sets the site's default design values (match it with the values in the theme)
// $config['site']['design']['backgroundcolor'] = '#F0F2F5';
// $config['site']['design']['contentcolor'] = '#FFFFFF';
// $config['site']['design']['sidebarcolor'] = '#CEE1E9';
// $config['site']['design']['textcolor'] = '#000000';
// $config['site']['design']['linkcolor'] = '#002E6E';
// $config['site']['design']['backgroundimage'] = null;
// $config['site']['design']['disposition'] = 1;
// Sets the site's default design values
// $config['design']['backgroundcolor'] = '#F0F2F5';
// $config['design']['contentcolor'] = '#FFFFFF';
// $config['design']['sidebarcolor'] = '#CEE1E9';
// $config['design']['textcolor'] = '#000000';
// $config['design']['linkcolor'] = '#002E6E';
// $config['design']['backgroundimage'] = null;
// $config['design']['disposition'] = 1;
// To enable the built-in mobile style sheet, defaults to false.
// $config['site']['mobile'] = true;
// For contact email, defaults to $_SERVER["SERVER_ADMIN"]
......
BEGIN;
create sequence design_seq;
create table design (
id bigint default nextval('design_seq') /* comment 'design ID'*/,
backgroundcolor integer /* comment 'main background color'*/ ,
contentcolor integer /*comment 'content area background color'*/ ,
sidebarcolor integer /*comment 'sidebar background color'*/ ,