Commit 62a5f270 authored by Evan Prodromou's avatar Evan Prodromou

Merge branch '0.9.x' of gitorious.org:statusnet/mainline into 0.9.x

parents 2b995c94 880b1b66
......@@ -59,7 +59,8 @@ class ApiStatusnetConfigAction extends ApiAction
'notice' => array('contentlimit'),
'throttle' => array('enabled', 'count', 'timespan'),
'xmpp' => array('enabled', 'server', 'port', 'user'),
'integration' => array('source')
'integration' => array('source'),
'attachments' => array('uploads', 'file_quota')
);
/**
......@@ -96,7 +97,7 @@ class ApiStatusnetConfigAction extends ApiAction
foreach ($this->keys as $section => $settings) {
$this->elementStart($section);
foreach ($settings as $setting) {
$value = common_config($section, $setting);
$value = $this->setting($section, $setting);
if (is_array($value)) {
$value = implode(',', $value);
} else if ($value === false || $value == '0') {
......@@ -125,7 +126,7 @@ class ApiStatusnetConfigAction extends ApiAction
$result[$section] = array();
foreach ($settings as $setting) {
$result[$section][$setting]
= common_config($section, $setting);
= $this->setting($section, $setting);
}
}
$this->initDocument('json');
......@@ -143,6 +144,20 @@ class ApiStatusnetConfigAction extends ApiAction
}
}
function setting($section, $key) {
$result = common_config($section, $key);
if ($key == 'file_quota') {
// hack: adjust for the live upload limit
if (common_config($section, 'uploads')) {
$max = ImageFile::maxFileSizeInt();
} else {
$max = 0;
}
return min($result, $max);
}
return $result;
}
/**
* Return true if read only.
*
......
......@@ -118,11 +118,13 @@ class BackupaccountAction extends Action
{
$cur = common_current_user();
$stream = new UserActivityStream($cur);
$stream = new UserActivityStream($cur, true, UserActivityStream::OUTPUT_RAW);
header('Content-Disposition: attachment; filename='.$cur->nickname.'.atom');
header('Content-Type: application/atom+xml; charset=utf-8');
// @fixme atom feed logic is in getString...
// but we just want it to output to the outputter.
$this->raw($stream->getString());
}
......
......@@ -202,13 +202,20 @@ class SearchNoticeListItem extends NoticeListItem {
$options = implode('|', array_map('preg_quote', array_map('htmlspecialchars', $terms),
array_fill(0, sizeof($terms), '/')));
$pattern = "/($options)/i";
$result = preg_replace($pattern, '<strong>\\1</strong>', $text);
$result = '';
/* Divide up into text (highlight me) and tags (don't touch) */
$chunks = preg_split('/(<[^>]+>)/', $text, 0, PREG_SPLIT_DELIM_CAPTURE);
foreach ($chunks as $i => $chunk) {
if ($i % 2 == 1) {
// odd: delimiter (tag)
$result .= $chunk;
} else {
// even: freetext between tags
$result .= preg_replace($pattern, '<strong>\\1</strong>', $chunk);
}
}
/* Remove highlighting from inside links, loop incase multiple highlights in links */
$pattern = '/(\w+="[^"]*)<strong>('.$options.')<\/strong>([^"]*")/iU';
do {
$result = preg_replace($pattern, '\\1\\2\\3', $result, -1, $count);
} while ($count);
return $result;
}
}
......@@ -153,7 +153,7 @@ class Notice extends Memcached_DataObject
function saveTags()
{
/* extract all #hastags */
$count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/', strtolower($this->content), $match);
$count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/u', strtolower($this->content), $match);
if (!$count) {
return true;
}
......
......@@ -854,8 +854,11 @@ class Action extends HTMLOutputter // lawsuit
function showFooter()
{
$this->elementStart('div', array('id' => 'footer'));
$this->showSecondaryNav();
$this->showLicenses();
if (Event::handle('StartShowInsideFooter', array($this))) {
$this->showSecondaryNav();
$this->showLicenses();
Event::handle('EndShowInsideFooter', array($this));
}
$this->elementEnd('div');
}
......
......@@ -116,6 +116,8 @@ class Router
static $bare = array('requesttoken', 'accesstoken', 'userauthorization',
'postnotice', 'updateprofile', 'finishremotesubscribe');
const REGEX_TAG = '[^\/]+'; // [\pL\pN_\-\.]{1,64} better if we can do unicode regexes
static function get()
{
if (!Router::$inst) {
......@@ -348,14 +350,14 @@ class Router
$m->connect('tag', array('action' => 'publictagcloud'));
$m->connect('tag/:tag/rss',
array('action' => 'tagrss'),
array('tag' => '[\pL\pN_\-\.]{1,64}'));
array('tag' => self::REGEX_TAG));
$m->connect('tag/:tag',
array('action' => 'tag'),
array('tag' => '[\pL\pN_\-\.]{1,64}'));
array('tag' => self::REGEX_TAG));
$m->connect('peopletag/:tag',
array('action' => 'peopletag'),
array('tag' => '[a-zA-Z0-9]+'));
array('tag' => self::REGEX_TAG));
// groups
......@@ -812,7 +814,7 @@ class Router
$m->connect($a.'/:tag',
array('action' => $a,
'nickname' => $nickname),
array('tag' => '[a-zA-Z0-9]+'));
array('tag' => self::REGEX_TAG));
}
foreach (array('rss', 'groups') as $a) {
......@@ -839,12 +841,12 @@ class Router
$m->connect('tag/:tag/rss',
array('action' => 'userrss',
'nickname' => $nickname),
array('tag' => '[\pL\pN_\-\.]{1,64}'));
array('tag' => self::REGEX_TAG));
$m->connect('tag/:tag',
array('action' => 'showstream',
'nickname' => $nickname),
array('tag' => '[\pL\pN_\-\.]{1,64}'));
array('tag' => self::REGEX_TAG));
$m->connect('rsd.xml',
array('action' => 'rsd',
......@@ -875,7 +877,7 @@ class Router
foreach (array('subscriptions', 'subscribers') as $a) {
$m->connect(':nickname/'.$a.'/:tag',
array('action' => $a),
array('tag' => '[a-zA-Z0-9]+',
array('tag' => self::REGEX_TAG,
'nickname' => Nickname::DISPLAY_FMT));
}
......@@ -903,12 +905,12 @@ class Router
$m->connect(':nickname/tag/:tag/rss',
array('action' => 'userrss'),
array('nickname' => Nickname::DISPLAY_FMT),
array('tag' => '[\pL\pN_\-\.]{1,64}'));
array('tag' => self::REGEX_TAG));
$m->connect(':nickname/tag/:tag',
array('action' => 'showstream'),
array('nickname' => Nickname::DISPLAY_FMT),
array('tag' => '[\pL\pN_\-\.]{1,64}'));
array('tag' => self::REGEX_TAG));
$m->connect(':nickname/rsd.xml',
array('action' => 'rsd'),
......
......@@ -29,15 +29,48 @@ class UserActivityStream extends AtomUserNoticeFeed
{
public $activities = array();
function __construct($user, $indent = true)
const OUTPUT_STRING = 1;
const OUTPUT_RAW = 2;
public $outputMode = self::OUTPUT_STRING;
/**
*
* @param User $user
* @param boolean $indent
* @param boolean $outputMode: UserActivityStream::OUTPUT_STRING to return a string,
* or UserActivityStream::OUTPUT_RAW to go to raw output.
* Raw output mode will attempt to stream, keeping less
* data in memory but will leave $this->activities incomplete.
*/
function __construct($user, $indent = true, $outputMode = UserActivityStream::OUTPUT_STRING)
{
parent::__construct($user, null, $indent);
$this->outputMode = $outputMode;
if ($this->outputMode == self::OUTPUT_STRING) {
// String buffering? Grab all the notices now.
$notices = $this->getNotices();
} elseif ($this->outputMode == self::OUTPUT_RAW) {
// Raw output... need to restructure from the stringer init.
$this->xw = new XMLWriter();
$this->xw->openURI('php://output');
if(is_null($indent)) {
$indent = common_config('site', 'indent');
}
$this->xw->setIndent($indent);
// We'll fetch notices later.
$notices = array();
} else {
throw new Exception('Invalid outputMode provided to ' . __METHOD__);
}
// Assume that everything but notices is feasible
// to pull at once and work with in memory...
$subscriptions = $this->getSubscriptions();
$subscribers = $this->getSubscribers();
$groups = $this->getGroups();
$faves = $this->getFaves();
$notices = $this->getNotices();
$objs = array_merge($subscriptions, $subscribers, $groups, $faves, $notices);
......@@ -45,16 +78,44 @@ class UserActivityStream extends AtomUserNoticeFeed
usort($objs, 'UserActivityStream::compareObject');
// We'll keep these around for later, and interleave them into
// the output stream with the user's notices.
foreach ($objs as $obj) {
$this->activities[] = $obj->asActivity();
}
}
/**
* Interleave the pre-sorted subs/groups/faves with the user's
* notices, all in reverse chron order.
*/
function renderEntries()
{
$end = time() + 1;
foreach ($this->activities as $act) {
$start = $act->time;
if ($this->outputMode == self::OUTPUT_RAW && $start != $end) {
// In raw mode, we haven't pre-fetched notices.
// Grab the chunks of notices between other activities.
$notices = $this->getNoticesBetween($start, $end);
foreach ($notices as $noticeAct) {
$noticeAct->asActivity()->outputTo($this, false, false);
}
}
// Only show the author sub-element if it's different from default user
$act->outputTo($this, false, ($act->actor->id != $this->user->uri));
$end = $start;
}
if ($this->outputMode == self::OUTPUT_RAW) {
// Grab anything after the last pre-sorted activity.
$notices = $this->getNoticesBetween(0, $end);
foreach ($notices as $noticeAct) {
$noticeAct->asActivity()->outputTo($this, false, false);
}
}
}
......@@ -121,7 +182,13 @@ class UserActivityStream extends AtomUserNoticeFeed
return $faves;
}
function getNotices()
/**
*
* @param int $start unix timestamp for earliest
* @param int $end unix timestamp for latest
* @return array of Notice objects
*/
function getNoticesBetween($start=0, $end=0)
{
$notices = array();
......@@ -129,6 +196,17 @@ class UserActivityStream extends AtomUserNoticeFeed
$notice->profile_id = $this->user->id;
if ($start) {
$tsstart = common_sql_date($start);
$notice->whereAdd("created >= '$tsstart'");
}
if ($end) {
$tsend = common_sql_date($end);
$notice->whereAdd("created < '$tsend'");
}
$notice->orderBy('created DESC');
if ($notice->find()) {
while ($notice->fetch()) {
$notices[] = clone($notice);
......@@ -138,6 +216,11 @@ class UserActivityStream extends AtomUserNoticeFeed
return $notices;
}
function getNotices()
{
return $this->getNoticesBetween();
}
function getGroups()
{
$groups = array();
......
......@@ -787,7 +787,7 @@ function common_render_text($text)
$r = preg_replace('/[\x{0}-\x{8}\x{b}-\x{c}\x{e}-\x{19}]/', '', $r);
$r = common_replace_urls_callback($r, 'common_linkify');
$r = preg_replace('/(^|\&quot\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/e', "'\\1#'.common_tag_link('\\2')", $r);
$r = preg_replace('/(^|\&quot\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/ue', "'\\1#'.common_tag_link('\\2')", $r);
// XXX: machine tags
return $r;
}
......
......@@ -179,28 +179,22 @@ class FacebookBridgePlugin extends Plugin
// Always add the admin panel route
$m->connect('admin/facebook', array('action' => 'facebookadminpanel'));
// Only add these routes if an application has been setup on
// Facebook for the plugin to use.
if ($this->hasApplication()) {
$m->connect(
'main/facebooklogin',
array('action' => 'facebooklogin')
);
$m->connect(
'main/facebookfinishlogin',
array('action' => 'facebookfinishlogin')
);
$m->connect(
'settings/facebook',
array('action' => 'facebooksettings')
);
$m->connect(
'facebook/deauthorize',
array('action' => 'facebookdeauthorize')
);
}
$m->connect(
'main/facebooklogin',
array('action' => 'facebooklogin')
);
$m->connect(
'main/facebookfinishlogin',
array('action' => 'facebookfinishlogin')
);
$m->connect(
'settings/facebook',
array('action' => 'facebooksettings')
);
$m->connect(
'facebook/deauthorize',
array('action' => 'facebookdeauthorize')
);
return true;
}
......
......@@ -51,7 +51,14 @@ class Facebookclient
function __construct($notice)
{
$this->facebook = self::getFacebook();
$this->notice = $notice;
if (empty($this->facebook)) {
throw new FacebookApiException(
"Could not create Facebook client! Bad application ID or secret?"
);
}
$this->notice = $notice;
$this->flink = Foreign_link::getByUserID(
$notice->profile_id,
......@@ -89,6 +96,22 @@ class Facebookclient
$secret = common_config('facebook', 'global_secret');
}
if (empty($appId)) {
common_log(
LOG_WARNING,
"Couldn't find Facebook application ID!",
__FILE__
);
}
if (empty($secret)) {
common_log(
LOG_WARNING,
"Couldn't find Facebook application ID!",
__FILE__
);
}
return new Facebook(
array(
'appId' => $appId,
......@@ -174,6 +197,9 @@ class Facebookclient
return $this->sendGraph();
}
}
// dequeue
return true;
}
/*
......
......@@ -68,6 +68,9 @@ class MobileProfilePlugin extends WAP20Plugin
$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'])) {
$this->serveMobile = true;
} else if (isset($_COOKIE['MobileOverride'])) {
// Cookie override is controlled by link at bottom.
$this->serveMobile = (bool)$_COOKIE['MobileOverride'];
} else {
// If they like the WAP 2.0 mimetype, serve them MP
// @fixme $type is undefined, making this if case useless and spewing errors.
......@@ -381,9 +384,40 @@ class MobileProfilePlugin extends WAP20Plugin
}
}
function onStartShowScripts($action)
function onEndShowScripts($action)
{
$action->inlineScript('
$(function() {
$("#mobile-toggle-disable").click(function() {
$.cookie("MobileOverride", "0", {path: "/"});
window.location.reload();
return false;
});
$("#mobile-toggle-enable").click(function() {
$.cookie("MobileOverride", "1", {path: "/"});
window.location.reload();
return false;
});
});'
);
}
function onEndShowInsideFooter($action)
{
if ($this->serveMobile) {
// TRANS: Link to switch site layout from mobile to desktop mode. Appears at very bottom of page.
$linkText = _m('Switch to desktop site layout.');
$key = 'mobile-toggle-disable';
} else {
// TRANS: Link to switch site layout from desktop to mobile mode. Appears at very bottom of page.
$linkText = _m('Switch to mobile site layout.');
$key = 'mobile-toggle-enable';
}
$action->elementStart('p');
$action->element('a', array('href' => '#', 'id' => $key), $linkText);
$action->elementEnd('p');
return true;
}
function _common_path($relative, $ssl=false)
......
......@@ -554,8 +554,8 @@ class TwitterImport
}
// Move all the entities into order so we can
// replace them in reverse order and thus
// not mess up their indices
// replace them and escape surrounding plaintext
// in order
$toReplace = array();
......@@ -577,56 +577,85 @@ class TwitterImport
}
}
// sort in reverse order by key
// sort in forward order by key
krsort($toReplace);
ksort($toReplace);
$result = '';
$cursor = 0;
foreach ($toReplace as $part) {
list($type, $object) = $part;
$start = $object->indices[0];
$end = $object->indices[1];
if ($cursor < $start) {
// Copy in the preceding plaintext
$result .= $this->twitEscape(mb_substr($text, $cursor, $start - $cursor));
$cursor = $start;
}
$orig = $this->twitEscape(mb_substr($text, $start, $end - $start));
switch($type) {
case self::URL:
$linkText = $this->makeUrlLink($object);
$linkText = $this->makeUrlLink($object, $orig);
break;
case self::HASHTAG:
$linkText = $this->makeHashtagLink($object);
$linkText = $this->makeHashtagLink($object, $orig);
break;
case self::MENTION:
$linkText = $this->makeMentionLink($object);
$linkText = $this->makeMentionLink($object, $orig);
break;
default:
$linkText = $orig;
continue;
}
$text = mb_substr($text, 0, $object->indices[0]) . $linkText . mb_substr($text, $object->indices[1]);
$result .= $linkText;
$cursor = $end;
}
return $text;
$last = $this->twitEscape(mb_substr($text, $cursor));
$result .= $last;
return $result;
}
function twitEscape($str)
{
// Twitter seems to preemptive turn < and > into &lt; and &gt;
// but doesn't for &, so while you may have some magic protection
// against XSS by not bothing to escape manually, you still get
// invalid XHTML. Thanks!
//
// Looks like their web interface pretty much sends anything
// through intact, so.... to do equivalent, decode all entities
// and then re-encode the special ones.
return htmlspecialchars(html_entity_decode($str, ENT_COMPAT, 'UTF-8'));
}
function makeUrlLink($object)
function makeUrlLink($object, $orig)
{
return "<a href='{$object->url}' class='extlink'>{$object->url}</a>";
return "<a href='{$object->url}' class='extlink'>{$orig}</a>";
}
function makeHashtagLink($object)
function makeHashtagLink($object, $orig)
{
return "#" . self::tagLink($object->text);
return "#" . self::tagLink($object->text, substr($orig, 1));
}
function makeMentionLink($object)
function makeMentionLink($object, $orig)
{
return "@".self::atLink($object->screen_name, $object->name);
return "@".self::atLink($object->screen_name, $object->name, substr($orig, 1));
}
static function tagLink($tag)
static function tagLink($tag, $orig)
{
return "<a href='https://search.twitter.com/search?q=%23{$tag}' class='hashtag'>{$tag}</a>";
return "<a href='https://search.twitter.com/search?q=%23{$tag}' class='hashtag'>{$orig}</a>";
}
static function atLink($screenName, $fullName=null)
static function atLink($screenName, $fullName, $orig)
{
if (!empty($fullName)) {
return "<a href='http://twitter.com/#!/{$screenName}' title='{$fullName}'>{$screenName}</a>";
return "<a href='http://twitter.com/#!/{$screenName}' title='{$fullName}'>{$orig}</a>";
} else {
return "<a href='http://twitter.com/#!/{$screenName}'>{$screenName}</a>";
return "<a href='http://twitter.com/#!/{$screenName}'>{$orig}</a>";
}
}
......
......@@ -36,7 +36,7 @@ require_once INSTALLDIR.'/scripts/commandline.inc';
try {
$user = getUser();
$actstr = new UserActivityStream($user);
$actstr = new UserActivityStream($user, true, UserActivityStream::OUTPUT_RAW);
print $actstr->getString();
} catch (Exception $e) {
print $e->getMessage()."\n";
......
......@@ -42,6 +42,21 @@ class HashTagDetectionTests extends PHPUnit_Framework_TestCase
'say {#<span class="tag"><a href="' . common_local_url('tag', array('tag' => common_canonical_tag('hello'))) . '" rel="tag">hello</a></span>} people'),
array('say \'#hello\' people',
'say \'#<span class="tag"><a href="' . common_local_url('tag', array('tag' => common_canonical_tag('hello'))) . '" rel="tag">hello</a></span>\' people'),
// Unicode legit letters
array('#éclair yummy',
'#<span class="tag"><a href="' . common_local_url('tag', array('tag' => common_canonical_tag('éclair'))) . '" rel="tag">éclair</a></span> yummy'),
array('#维基百科 zh.wikipedia!',
'#<span class="tag"><a href="' . common_local_url('tag', array('tag' => common_canonical_tag('维基百科'))) . '" rel="tag">维基百科</a></span> zh.wikipedia!'),
array('#Россия russia',
'#<span class="tag"><a href="' . common_local_url('tag', array('tag' => common_canonical_tag('Россия'))) . '" rel="tag">Россия</a></span> russia'),
// Unicode punctuators -- the ideographic "," separates the tag, just as "," does
array('#维基百科,zh.wikipedia!',
'#<span class="tag"><a href="' . common_local_url('tag', array('tag' => common_canonical_tag('维基百科'))) . '" rel="tag">维基百科</a></span>,zh.wikipedia!'),
array('#维基百科,zh.wikipedia!',
'#<span class="tag"><a href="' . common_local_url('tag', array('tag' => common_canonical_tag('维基百科'))) . '" rel="tag">维基百科</a></span>,zh.wikipedia!'),
);
}
}
......
This diff is collapsed.
/* Temporary copy of base styles for overriding */
input.checkbox,
input.radio {
top:0;
}
.form_notice textarea {
width: 328px;
}
.form_notice .form_note + label {
position:absolute;
top:25px;
left:83%;
text-indent:-9999px;
height:16px;
width:16px;
display:block;
left: 390px;
top: 27px;
}
.form_notice #notice_action-submit {
width: 106px;
max-width: 106px;
}
.form_notice #notice_data-attach_selected,
.form_notice #notice_data-geo_selected {
width:78.75%;
}
.form_notice #notice_data-attach_selected button,
.form_notice #notice_data-geo_selected button {
padding:0 4px;
}
.notice-options input.submit {
font-size:0;
text-align:right;
text-indent:0;
}
.notice div.entry-content .timestamp a {
margin-right:4px;
}
.entity_profile {
width:64%;
}
.notice {
z-index:1;
}
.notice:hover {
z-index:9999;
}
.notice .thumbnail img {
z-index:9999;
}
.form_settings fieldset fieldset legend {
line-height:auto;
}
/* IE specific styles */
#site_nav_global_primary ul {
margin-right: 0px;
}
.notice-options input.submit {
color:#FFFFFF;
}
.form_notice #notice_data-attach {
filter: alpha(opacity=0);
}
.form_notice .form_note + label {
background:transparent url(../../rebase/images/icons/icons-01.gif) no-repeat 0 -328px;
}
.form_notice #notice_data-geo_wrap label {
background:transparent url(../../rebase/images/icons/icons-01.gif) no-repeat 0 -1780px;
}
.form_notice #notice_data-geo_wrap label.checked {
background:transparent url(../../rebase/images/icons/icons-01.gif) no-repeat 0 -1846px;
}
/* mobile style */
body {
background-image: none;
min-width: 0;
}