diff --git a/.gitignore b/.gitignore index f4c2bba5f7..5394f5eac5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ config-*.php good-config.php lac08.log php.log -config.php.* + diff --git a/README b/README index 0bf1319c6d..ef5a139346 100644 --- a/README +++ b/README @@ -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 ======= diff --git a/actions/api.php b/actions/api.php index 8b92889f8a..99ab262ad7 100644 --- a/actions/api.php +++ b/actions/api.php @@ -130,6 +130,7 @@ class ApiAction extends Action 'laconica/wadl', 'tags/timeline', 'oembed/oembed', + 'groups/show', 'groups/timeline'); static $bareauth = array('statuses/user_timeline', diff --git a/actions/conversation.php b/actions/conversation.php index c8755ba6ef..6b5d8d54d9 100644 --- a/actions/conversation.php +++ b/actions/conversation.php @@ -167,6 +167,8 @@ class ConversationTree extends NoticeList function _buildTree() { + $cnt = 0; + $this->tree = array(); $this->table = array(); diff --git a/actions/public.php b/actions/public.php index ef9ef0d1ab..d0317ac706 100644 --- a/actions/public.php +++ b/actions/public.php @@ -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(); diff --git a/actions/twitapifavorites.php b/actions/twitapifavorites.php index 8256668f3d..6f93610650 100644 --- a/actions/twitapifavorites.php +++ b/actions/twitapifavorites.php @@ -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 +} diff --git a/actions/twitapigroups.php b/actions/twitapigroups.php index 71a0776f46..82604ebff2 100644 --- a/actions/twitapigroups.php +++ b/actions/twitapigroups.php @@ -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); diff --git a/actions/twitapitags.php b/actions/twitapitags.php index 5c85275302..e19e1b1ed6 100644 --- a/actions/twitapitags.php +++ b/actions/twitapitags.php @@ -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); diff --git a/classes/Design.php b/classes/Design.php index 0927fcda70..43544f1c9d 100644 --- a/classes/Design.php +++ b/classes/Design.php @@ -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"; } - $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"; + $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"; + } + + $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; + } } diff --git a/classes/File.php b/classes/File.php index 0c4fbf7e69..959301edae 100644 --- a/classes/File.php +++ b/classes/File.php @@ -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); diff --git a/classes/Notice.php b/classes/Notice.php index 4e9aff4f57..9578d87b2b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -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; $iis_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); - $notice->uri = common_notice_uri($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; + } + } } diff --git a/classes/Session.php b/classes/Session.php index ac80279c5e..5ec509f5f9 100644 --- a/classes/Session.php +++ b/classes/Session.php @@ -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'); - self::logdeb("garbage collection result = $result"); + $session->find(); + + while ($session->fetch()) { + $ids[] = $session->id; + } + + $session->free(); + + foreach ($ids as $id) { + self::destroy($id); + } } static function setSaveHandler() diff --git a/config.php.sample b/config.php.sample index 36e62f70f2..c27645ff87 100644 --- a/config.php.sample +++ b/config.php.sample @@ -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"] diff --git a/db/074to080_pg.sql b/db/074to080_pg.sql new file mode 100644 index 0000000000..0a7171ae56 --- /dev/null +++ b/db/074to080_pg.sql @@ -0,0 +1,108 @@ +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'*/ , + textcolor integer /*comment 'text color'*/ , + linkcolor integer /*comment 'link color'*/, + backgroundimage varchar(255) /*comment 'background image, if any'*/, + disposition int default 1 /*comment 'bit 1 = hide background image, bit 2 = display background image, bit 4 = tile background image'*/, + primary key (id) +); +alter table "user" + add column design_id integer references design(id); +alter table "user" + add column viewdesigns integer default 1; + +alter table notice add column + conversation integer references notice (id); + +create index notice_conversation_idx on notice(conversation); + +alter table foreign_user + alter column id TYPE bigint; + +alter table foreign_user alter column id set not null; + +alter table foreign_link + alter column foreign_id TYPE bigint; + +alter table user_group + add column design_id integer; + +/*attachments and URLs stuff */ +create sequence file_seq; +create table file ( + id bigint default nextval('file_seq') primary key /* comment 'unique identifier' */, + url varchar(255) unique, + mimetype varchar(50), + size integer, + title varchar(255), + date integer, + protected integer, + filename text /* comment 'if a local file, name of the file' */, + modified timestamp default CURRENT_TIMESTAMP /* comment 'date this record was modified'*/ +); + +create sequence file_oembed_seq; +create table file_oembed ( + file_id bigint default nextval('file_oembed_seq') primary key /* comment 'unique identifier' */, + version varchar(20), + type varchar(20), + provider varchar(50), + provider_url varchar(255), + width integer, + height integer, + html text, + title varchar(255), + author_name varchar(50), + author_url varchar(255), + url varchar(255) +); + +create sequence file_redirection_seq; +create table file_redirection ( + url varchar(255) primary key, + file_id bigint, + redirections integer, + httpcode integer +); + +create sequence file_thumbnail_seq; +create table file_thumbnail ( + file_id bigint primary key, + url varchar(255) unique, + width integer, + height integer +); +create sequence file_to_post_seq; +create table file_to_post ( + file_id bigint, + post_id bigint, + + primary key (file_id, post_id) +); + + +create table group_block ( + group_id integer not null /* comment 'group profile is blocked from' */ references user_group (id), + blocked integer not null /* comment 'profile that is blocked' */references profile (id), + blocker integer not null /* comment 'user making the block'*/ references "user" (id), + modified timestamp /* comment 'date of blocking'*/ , + + primary key (group_id, blocked) +); + +create table group_alias ( + + alias varchar(64) /* comment 'additional nickname for the group'*/ , + group_id integer not null /* comment 'group profile is blocked from'*/ references user_group (id), + modified timestamp /* comment 'date alias was created'*/, + primary key (alias) + +); +create index group_alias_group_id_idx on group_alias (group_id); + +COMMIT; \ No newline at end of file diff --git a/index.php b/index.php index 69c0bc1b23..a73983b595 100644 --- a/index.php +++ b/index.php @@ -108,7 +108,7 @@ function checkMirror($action_obj) function main() { // quick check for fancy URL auto-detection support in installer. - if (isset($_SERVER['REDIRECT_URL']) && ((dirname($_SERVER['REQUEST_URI']) . '/check-fancy') === $_SERVER['REDIRECT_URL'])) { + if (isset($_SERVER['REDIRECT_URL']) && (preg_replace("/^\/$/","",(dirname($_SERVER['REQUEST_URI']))) . '/check-fancy') === $_SERVER['REDIRECT_URL']) { die("Fancy URL support detection succeeded. We suggest you enable this to get fancy (pretty) URLs."); } global $user, $action; diff --git a/install.php b/install.php index 901e502f1a..c222afa7b5 100644 --- a/install.php +++ b/install.php @@ -77,7 +77,7 @@ function checkPrereqs() if (!is_writable($fileFullPath)) { ?>

Cannot write directory:

On your server, try this command: chmod a+w

- element('link', array('rel' => 'stylesheet', 'type' => 'text/css', @@ -209,6 +210,7 @@ class Action extends HTMLOutputter // lawsuit 'media' => 'print')); Event::handle('EndShowLaconicaStyles', array($this)); } + if (Event::handle('StartShowUAStyles', array($this))) { $this->comment('[if IE]>viewdesigns) { + $design = $this->getDesign(); + + if (!empty($design)) { + $design->showCSS($this); + } + } + + Event::handle('EndShowDesign', array($this)); + } Event::handle('EndShowStyles', array($this)); } } @@ -1074,4 +1091,15 @@ class Action extends HTMLOutputter // lawsuit { return null; } + + /** + * A design for this action + * + * @return Design a design object to use + */ + + function getDesign() + { + return Design::siteDesign(); + } } diff --git a/lib/common.php b/lib/common.php index 9d7954fa98..b3d3018620 100644 --- a/lib/common.php +++ b/lib/common.php @@ -94,14 +94,6 @@ $config = array('name' => 'Just another Laconica microblog', 'server' => $_server, 'theme' => 'default', - 'design' => - array('backgroundcolor' => '#CEE1E9', - 'contentcolor' => '#FFFFFF', - 'sidebarcolor' => '#C8D1D5', - 'textcolor' => '#000000', - 'linkcolor' => '#002E6E', - 'backgroundimage' => null, - 'disposition' => 1), 'path' => $_path, 'logfile' => null, 'logo' => null, @@ -261,6 +253,14 @@ $config = 'sessions' => array('handle' => false, // whether to handle sessions ourselves 'debug' => false), // debugging output for sessions + 'design' => + array('backgroundcolor' => null, // null -> 'use theme default' + 'contentcolor' => null, + 'sidebarcolor' => null, + 'textcolor' => null, + 'linkcolor' => null, + 'backgroundimage' => null, + 'disposition' => null), ); $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); @@ -277,6 +277,10 @@ $config['db'] = 'quote_identifiers' => false, 'type' => 'mysql' ); +// Backward compatibility + +$config['site']['design'] =& $config['design']; + if (function_exists('date_default_timezone_set')) { /* Work internally in UTC */ date_default_timezone_set('UTC'); diff --git a/lib/currentuserdesignaction.php b/lib/currentuserdesignaction.php index 4c7e15a8b7..52516b624a 100644 --- a/lib/currentuserdesignaction.php +++ b/lib/currentuserdesignaction.php @@ -47,33 +47,10 @@ if (!defined('LACONICA')) { class CurrentUserDesignAction extends Action { - - /** - * Show the user's design stylesheet - * - * @return nothing - */ - - function showStylesheets() - { - parent::showStylesheets(); - - $user = common_current_user(); - - if (empty($user) || $user->viewdesigns) { - $design = $this->getDesign(); - - if (!empty($design)) { - $design->showCSS($this); - } - } - } - /** * A design for this action * - * if the user attribute has been set, returns that user's - * design. + * Returns the design preferences for the current user. * * @return Design a design object to use */ @@ -82,11 +59,15 @@ class CurrentUserDesignAction extends Action { $cur = common_current_user(); - if (empty($cur)) { - return null; + if (!empty($cur)) { + + $design = $cur->getDesign(); + + if (!empty($design)) { + return $design; + } } - return $cur->getDesign(); + return parent::getDesign(); } - } diff --git a/lib/designsettings.php b/lib/designsettings.php index fbffdb208f..1b0e621669 100644 --- a/lib/designsettings.php +++ b/lib/designsettings.php @@ -182,7 +182,7 @@ class DesignSettingsAction extends AccountSettingsAction 'class' => 'swatch', 'maxlength' => '7', 'size' => '7', - 'value' => '#' . $bgcolor->hexValue())); + 'value' => '')); $this->elementEnd('li'); $ccolor = new WebColor($design->contentcolor); @@ -195,7 +195,7 @@ class DesignSettingsAction extends AccountSettingsAction 'class' => 'swatch', 'maxlength' => '7', 'size' => '7', - 'value' => '#' . $ccolor->hexValue())); + 'value' => '')); $this->elementEnd('li'); $sbcolor = new WebColor($design->sidebarcolor); @@ -208,7 +208,7 @@ class DesignSettingsAction extends AccountSettingsAction 'class' => 'swatch', 'maxlength' => '7', 'size' => '7', - 'value' => '#' . $sbcolor->hexValue())); + 'value' => '')); $this->elementEnd('li'); $tcolor = new WebColor($design->textcolor); @@ -221,7 +221,7 @@ class DesignSettingsAction extends AccountSettingsAction 'class' => 'swatch', 'maxlength' => '7', 'size' => '7', - 'value' => '#' . $tcolor->hexValue())); + 'value' => '')); $this->elementEnd('li'); $lcolor = new WebColor($design->linkcolor); @@ -234,7 +234,7 @@ class DesignSettingsAction extends AccountSettingsAction 'class' => 'swatch', 'maxlength' => '7', 'size' => '7', - 'value' => '#' . $lcolor->hexValue())); + 'value' => '')); $this->elementEnd('li'); } catch (WebColorException $e) { diff --git a/lib/groupdesignaction.php b/lib/groupdesignaction.php index 58777c283a..c7cdff1fe9 100644 --- a/lib/groupdesignaction.php +++ b/lib/groupdesignaction.php @@ -49,26 +49,6 @@ class GroupDesignAction extends Action { /** The group in question */ var $group = null; - /** - * Show the groups's design stylesheet - * - * @return nothing - */ - function showStylesheets() - { - parent::showStylesheets(); - - $user = common_current_user(); - - if (empty($user) || $user->viewdesigns) { - $design = $this->getDesign(); - - if (!empty($design)) { - $design->showCSS($this); - } - } - } - /** * A design for this action * @@ -80,10 +60,12 @@ class GroupDesignAction extends Action { function getDesign() { - if (empty($this->group)) { - return null; + if (!empty($this->group)) { + $design = $this->group->getDesign(); + if (!empty($design)) { + return $design; + } } - - return $this->group->getDesign(); + return parent::getDesign(); } } diff --git a/lib/ownerdesignaction.php b/lib/ownerdesignaction.php index 785b8a93d3..b42df926d0 100644 --- a/lib/ownerdesignaction.php +++ b/lib/ownerdesignaction.php @@ -52,26 +52,6 @@ class OwnerDesignAction extends Action { var $user = null; - /** - * Show the owner's design stylesheet - * - * @return nothing - */ - function showStylesheets() - { - parent::showStylesheets(); - - $user = common_current_user(); - - if (empty($user) || $user->viewdesigns) { - $design = $this->getDesign(); - - if (!empty($design)) { - $design->showCSS($this); - } - } - } - /** * A design for this action * @@ -83,10 +63,15 @@ class OwnerDesignAction extends Action { function getDesign() { - if (empty($this->user)) { - return null; + if (!empty($this->user)) { + + $design = $this->user->getDesign(); + + if (!empty($design)) { + return $design; + } } - return $this->user->getDesign(); + return parent::getDesign(); } } diff --git a/lib/router.php b/lib/router.php index 8e48364979..19839b9972 100644 --- a/lib/router.php +++ b/lib/router.php @@ -113,6 +113,16 @@ class Router $m->connect('main/tagother/:id', array('action' => 'tagother')); + $m->connect('main/oembed.xml', + array('action' => 'api', + 'method' => 'oembed.xml', + 'apiaction' => 'oembed')); + + $m->connect('main/oembed.json', + array('action' => 'api', + 'method' => 'oembed.json', + 'apiaction' => 'oembed')); + // these take a code foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) { @@ -129,11 +139,6 @@ class Router $m->connect('index.php?action=' . $action, array('action' => $action)); } - $m->connect('main/:method', - array('action' => 'api', - 'method' => 'oembed(.xml|.json)?', - 'apiaction' => 'oembed')); - // settings foreach (array('profile', 'avatar', 'password', 'openid', 'im', diff --git a/lib/twitterapi.php b/lib/twitterapi.php index b2602e77ca..4115d9dcb4 100644 --- a/lib/twitterapi.php +++ b/lib/twitterapi.php @@ -213,6 +213,26 @@ class TwitterapiAction extends Action return $twitter_status; } + function twitter_group_array($group) + { + $twitter_group=array(); + $twitter_group['id']=$group->id; + $twitter_group['url']=$group->permalink(); + $twitter_group['nickname']=$group->nickname; + $twitter_group['fullname']=$group->fullname; + $twitter_group['homepage_url']=$group->homepage_url; + $twitter_group['original_logo']=$group->original_logo; + $twitter_group['homepage_logo']=$group->homepage_logo; + $twitter_group['stream_logo']=$group->stream_logo; + $twitter_group['mini_logo']=$group->mini_logo; + $twitter_group['homepage']=$group->homepage; + $twitter_group['description']=$group->description; + $twitter_group['location']=$group->location; + $twitter_group['created']=$this->date_twitter($group->created); + $twitter_group['modified']=$this->date_twitter($group->modified); + return $twitter_group; + } + function twitter_rss_entry_array($notice) { $profile = $notice->getProfile(); @@ -413,6 +433,15 @@ class TwitterapiAction extends Action $this->elementEnd('status'); } + function show_twitter_xml_group($twitter_group) + { + $this->elementStart('group'); + foreach($twitter_group as $element => $value) { + $this->element($element, null, $value); + } + $this->elementEnd('group'); + } + function show_twitter_xml_user($twitter_user, $role='user') { $this->elementStart($role); @@ -450,12 +479,12 @@ class TwitterapiAction extends Action $this->element('link', null, $entry['link']); # RSS only supports 1 enclosure per item - if($entry['enclosures']){ + if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){ $enclosure = $entry['enclosures'][0]; $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null); } - if($entry['tags']){ + if(array_key_exists('tags', $entry)){ foreach($entry['tags'] as $tag){ $this->element('category', null,$tag); } @@ -639,6 +668,22 @@ class TwitterapiAction extends Action $this->end_document('json'); } + function show_single_json_group($group) + { + $this->init_document('json'); + $twitter_group = $this->twitter_group_array($group); + $this->show_json_objects($twitter_group); + $this->end_document('json'); + } + + function show_single_xml_group($group) + { + $this->init_document('xml'); + $twitter_group = $this->twitter_group_array($group); + $this->show_twitter_xml_group($twitter_group); + $this->end_document('xml'); + } + // Anyone know what date format this is? // Twitter's dates look like this: "Mon Jul 14 23:52:38 +0000 2008" -- Zach function date_twitter($dt) diff --git a/lib/util.php b/lib/util.php index d784bb7933..c8e318efec 100644 --- a/lib/util.php +++ b/lib/util.php @@ -140,7 +140,7 @@ function common_have_session() function common_ensure_session() { $c = null; - if (array_key_exists(session_name, $_COOKIE)) { + if (array_key_exists(session_name(), $_COOKIE)) { $c = $_COOKIE[session_name()]; } if (!common_have_session()) { @@ -1410,20 +1410,21 @@ function common_client_ip() return null; } - if ($_SERVER['HTTP_X_FORWARDED_FOR']) { - if ($_SERVER['HTTP_CLIENT_IP']) { + if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) { + if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) { $proxy = $_SERVER['HTTP_CLIENT_IP']; } else { $proxy = $_SERVER['REMOTE_ADDR']; } $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; } else { - if ($_SERVER['HTTP_CLIENT_IP']) { + $proxy = null; + if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) { $ip = $_SERVER['HTTP_CLIENT_IP']; } else { $ip = $_SERVER['REMOTE_ADDR']; } } - return array($ip, $proxy); + return array($proxy, $ip); } diff --git a/plugins/FBConnect/FBConnectPlugin.php b/plugins/FBConnect/FBConnectPlugin.php index 2e32ad198f..6788793b25 100644 --- a/plugins/FBConnect/FBConnectPlugin.php +++ b/plugins/FBConnect/FBConnectPlugin.php @@ -122,9 +122,7 @@ class FBConnectPlugin extends Plugin FB_RequireFeatures( ["XFBML"], function() { - FB.init("%s", "../xd_receiver.html", - {"doNotUseCachedConnectState":true }); - + FB.init("%s", "../xd_receiver.html"); } ); } @@ -222,7 +220,7 @@ class FBConnectPlugin extends Plugin try { $facebook = getFacebook(); - $fbuid = $facebook->api_client->users_getLoggedInUser(); + $fbuid = $facebook->get_loggedin_user(); } catch (Exception $e) { common_log(LOG_WARNING, diff --git a/plugins/recaptcha/LICENSE b/plugins/recaptcha/LICENSE new file mode 100644 index 0000000000..b612f71f01 --- /dev/null +++ b/plugins/recaptcha/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2007 reCAPTCHA -- http://recaptcha.net +AUTHORS: + Mike Crawford + Ben Maurer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/recaptcha/README b/plugins/recaptcha/README new file mode 100644 index 0000000000..3100f697e4 --- /dev/null +++ b/plugins/recaptcha/README @@ -0,0 +1,23 @@ +Laconica reCAPTCHA plugin 0.2 8/3/09 +==================================== +Adds a captcha to your registration page to reduce automated spam bots registering. + +Use: +1. Get an API key from http://recaptcha.net + +2. In config.php add: +include_once('plugins/recaptcha.php'); +$captcha = new recaptcha(publickey, privatekey, showErrors); + +Changelog +========= +0.1 initial release +0.2 Work around for webkit browsers + +reCAPTCHA README +================ + +The reCAPTCHA PHP Lirary helps you use the reCAPTCHA API. Documentation +for this library can be found at + + http://recaptcha.net/plugins/php diff --git a/plugins/recaptcha/recaptcha.php b/plugins/recaptcha/recaptcha.php new file mode 100644 index 0000000000..5ef8352d18 --- /dev/null +++ b/plugins/recaptcha/recaptcha.php @@ -0,0 +1,106 @@ +. + * + * @category Plugin + * @package Laconica + * @author Eric Helgeson + * @copyright 2009 + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +define('RECAPTCHA', '0.2'); + +class recaptcha extends Plugin +{ + var $private_key; + var $public_key; + var $display_errors; + var $failed; + var $ssl; + + function __construct($public_key, $private_key, $display_errors=false) + { + parent::__construct(); + require_once(INSTALLDIR.'/plugins/recaptcha/recaptchalib.php'); + $this->public_key = $public_key; + $this->private_key = $private_key; + $this->display_errors = $display_errors; + } + + function checkssl(){ + if(common_config('site', 'ssl') === 'sometimes' || common_config('site', 'ssl') === 'always') { + return true; + } + return false; + } + + function onStartShowHTML($action) + { + //XXX: Horrible hack to make Safari, FF2, and Chrome work with + //reChapcha. reChapcha beaks xhtml strict + header('Content-Type: text/html'); + + $action->extraHeaders(); + + $action->startXML('html', + '-//W3C//DTD XHTML 1.0 Strict//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'); + + $action->raw(''); + return false; + } + + function onEndRegistrationFormData($action) + { + $action->elementStart('li'); + $action->raw(''); + if($this->checkssl() === true){ + $action->raw(recaptcha_get_html($this->public_key), null, true); + } else { + $action->raw(recaptcha_get_html($this->public_key)); + } + $action->elementEnd('li'); + return true; + } + + function onStartRegistrationTry($action) + { + $resp = recaptcha_check_answer ($this->private_key, + $_SERVER["REMOTE_ADDR"], + $action->trimmed('recaptcha_challenge_field'), + $action->trimmed('recaptcha_response_field')); + + if (!$resp->is_valid) + { + if($this->display_errors) + { + $action->showForm ("(reCAPTCHA said: " . $resp->error . ")"); + } + $action->showForm("Captcha does not match!"); + return false; + } + } +} diff --git a/plugins/recaptcha/recaptchalib.php b/plugins/recaptcha/recaptchalib.php new file mode 100644 index 0000000000..897c50981a --- /dev/null +++ b/plugins/recaptcha/recaptchalib.php @@ -0,0 +1,277 @@ + $value ) + $req .= $key . '=' . urlencode( stripslashes($value) ) . '&'; + + // Cut the last '&' + $req=substr($req,0,strlen($req)-1); + return $req; +} + + + +/** + * Submits an HTTP POST to a reCAPTCHA server + * @param string $host + * @param string $path + * @param array $data + * @param int port + * @return array response + */ +function _recaptcha_http_post($host, $path, $data, $port = 80) { + + $req = _recaptcha_qsencode ($data); + + $http_request = "POST $path HTTP/1.0\r\n"; + $http_request .= "Host: $host\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; + $http_request .= "Content-Length: " . strlen($req) . "\r\n"; + $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; + $http_request .= "\r\n"; + $http_request .= $req; + + $response = ''; + if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) { + die ('Could not open socket'); + } + + fwrite($fs, $http_request); + + while ( !feof($fs) ) + $response .= fgets($fs, 1160); // One TCP-IP packet + fclose($fs); + $response = explode("\r\n\r\n", $response, 2); + + return $response; +} + + + +/** + * Gets the challenge HTML (javascript and non-javascript version). + * This is called from the browser, and the resulting reCAPTCHA HTML widget + * is embedded within the HTML form it was called from. + * @param string $pubkey A public key for reCAPTCHA + * @param string $error The error given by reCAPTCHA (optional, default is null) + * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false) + + * @return string - The HTML to be embedded in the user's form. + */ +function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false) +{ + if ($pubkey == null || $pubkey == '') { + die ("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"); + } + + if ($use_ssl) { + $server = RECAPTCHA_API_SECURE_SERVER; + } else { + $server = RECAPTCHA_API_SERVER; + } + + $errorpart = ""; + if ($error) { + $errorpart = "&error=" . $error; + } + return ' + + '; +} + + + + +/** + * A ReCaptchaResponse is returned from recaptcha_check_answer() + */ +class ReCaptchaResponse { + var $is_valid; + var $error; +} + + +/** + * Calls an HTTP POST function to verify if the user's guess was correct + * @param string $privkey + * @param string $remoteip + * @param string $challenge + * @param string $response + * @param array $extra_params an array of extra variables to post to the server + * @return ReCaptchaResponse + */ +function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array()) +{ + if ($privkey == null || $privkey == '') { + die ("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"); + } + + if ($remoteip == null || $remoteip == '') { + die ("For security reasons, you must pass the remote ip to reCAPTCHA"); + } + + + + //discard spam submissions + if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { + $recaptcha_response = new ReCaptchaResponse(); + $recaptcha_response->is_valid = false; + $recaptcha_response->error = 'incorrect-captcha-sol'; + return $recaptcha_response; + } + + $response = _recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/verify", + array ( + 'privatekey' => $privkey, + 'remoteip' => $remoteip, + 'challenge' => $challenge, + 'response' => $response + ) + $extra_params + ); + + $answers = explode ("\n", $response [1]); + $recaptcha_response = new ReCaptchaResponse(); + + if (trim ($answers [0]) == 'true') { + $recaptcha_response->is_valid = true; + } + else { + $recaptcha_response->is_valid = false; + $recaptcha_response->error = $answers [1]; + } + return $recaptcha_response; + +} + +/** + * gets a URL where the user can sign up for reCAPTCHA. If your application + * has a configuration page where you enter a key, you should provide a link + * using this function. + * @param string $domain The domain where the page is hosted + * @param string $appname The name of your application + */ +function recaptcha_get_signup_url ($domain = null, $appname = null) { + return "http://recaptcha.net/api/getkey?" . _recaptcha_qsencode (array ('domain' => $domain, 'app' => $appname)); +} + +function _recaptcha_aes_pad($val) { + $block_size = 16; + $numpad = $block_size - (strlen ($val) % $block_size); + return str_pad($val, strlen ($val) + $numpad, chr($numpad)); +} + +/* Mailhide related code */ + +function _recaptcha_aes_encrypt($val,$ky) { + if (! function_exists ("mcrypt_encrypt")) { + die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); + } + $mode=MCRYPT_MODE_CBC; + $enc=MCRYPT_RIJNDAEL_128; + $val=_recaptcha_aes_pad($val); + return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); +} + + +function _recaptcha_mailhide_urlbase64 ($x) { + return strtr(base64_encode ($x), '+/', '-_'); +} + +/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ +function recaptcha_mailhide_url($pubkey, $privkey, $email) { + if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { + die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . + "you can do so at http://mailhide.recaptcha.net/apikey"); + } + + + $ky = pack('H*', $privkey); + $cryptmail = _recaptcha_aes_encrypt ($email, $ky); + + return "http://mailhide.recaptcha.net/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail); +} + +/** + * gets the parts of the email to expose to the user. + * eg, given johndoe@example,com return ["john", "example.com"]. + * the email is then displayed as john...@example.com + */ +function _recaptcha_mailhide_email_parts ($email) { + $arr = preg_split("/@/", $email ); + + if (strlen ($arr[0]) <= 4) { + $arr[0] = substr ($arr[0], 0, 1); + } else if (strlen ($arr[0]) <= 6) { + $arr[0] = substr ($arr[0], 0, 3); + } else { + $arr[0] = substr ($arr[0], 0, 4); + } + return $arr; +} + +/** + * Gets html to display an email address given a public an private key. + * to get a key, go to: + * + * http://mailhide.recaptcha.net/apikey + */ +function recaptcha_mailhide_html($pubkey, $privkey, $email) { + $emailparts = _recaptcha_mailhide_email_parts ($email); + $url = recaptcha_mailhide_url ($pubkey, $privkey, $email); + + return htmlentities($emailparts[0]) . "...@" . htmlentities ($emailparts [1]); + +} + + +?> diff --git a/scripts/maildaemon.php b/scripts/maildaemon.php index a4003b6b26..3ef4d06383 100755 --- a/scripts/maildaemon.php +++ b/scripts/maildaemon.php @@ -317,6 +317,9 @@ class MailerDaemon } else if ($parsed->ctype_primary == 'text' && $parsed->ctype_secondary=='plain') { $msg = $parsed->body; + if(strtolower($parsed->ctype_parameters['charset']) != "utf-8"){ + $msg = utf8_encode($msg); + } }else if(!empty($parsed->body)){ if(common_config('attachments', 'uploads')){ //only save attachments if uploads are enabled diff --git a/scripts/sessiongc.php b/scripts/sessiongc.php new file mode 100644 index 0000000000..314b641eb9 --- /dev/null +++ b/scripts/sessiongc.php @@ -0,0 +1,36 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$helptext = <<