'source' => array('type' => 'varchar', 'length' => 32, 'description' => 'source of comment, like "web", "im", or "clientname"'),
'conversation' => array('type' => 'int', 'description' => 'id of root notice in this conversation'),
'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'),
- 'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'),
+ 'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => null),
'verb' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'),
'scope' => array('type' => 'int',
'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers; null = default'),
* Record the given set of hash tags in the db for this notice.
* Given tag strings will be normalized and checked for dupes.
*/
- function saveKnownTags($hashtags)
+ function saveKnownTags(array $hashtags)
{
//turn each into their canonical tag
//this is needed to remove dupes before saving e.g. #hash.tag = #hashtag
* @return Notice
* @throws ClientException
*/
- static function saveNew($profile_id, $content, $source, array $options=null) {
+ static function saveNew($profile_id, $content, $source, array $options=array()) {
$defaults = array('uri' => null,
'url' => null,
'conversation' => null, // URI of conversation
'object_type' => null,
'verb' => null);
- if (!empty($options) && is_array($options)) {
+ /*
+ * Above type-hint is already array, so simply count it, this saves
+ * "some" CPU cycles.
+ */
+ if (count($options) > 0) {
$options = array_merge($defaults, $options);
- extract($options);
- } else {
- extract($defaults);
}
+ extract($options);
+
if (!isset($is_local)) {
$is_local = Notice::LOCAL_PUBLIC;
}
throw new ClientException(_('You cannot repeat your own notice.'));
}
- if ($repeat->scope != Notice::SITE_SCOPE &&
- $repeat->scope != Notice::PUBLIC_SCOPE) {
+ if ($repeat->isPrivateScope()) {
// TRANS: Client error displayed when trying to repeat a non-public notice.
throw new ClientException(_('Cannot repeat a private notice.'), 403);
}
*
* @return void
*/
- function saveKnownUrls($urls)
+ function saveKnownUrls(array $urls)
{
if (common_config('attachments', 'process_links')) {
// @fixme validation?
return $this->_replies[$this->getID()];
}
- function _setReplies($replies)
+ function _setReplies(array $replies)
{
$this->_replies[$this->getID()] = $replies;
}
}
$groups = User_group::multiGet('id', $ids);
- $this->_groups[$this->id] = $groups->fetchAll();
+ $this->_setGroups($groups->fetchAll());
return $this->_groups[$this->id];
}
- function _setGroups($groups)
+ function _setGroups(array $groups)
{
$this->_groups[$this->id] = $groups;
}
*/
public function getTags()
{
+ // Check default scope (non-private notices)
+ $inScope = (!$this->isPrivateScope());
+
+ // Get current profile
+ $profile = Profile::current();
+
+ // Is the general scope check okay and the user in logged in?
+ //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . ']: inScope=' . intval($inScope) . ',profile[]=' . gettype($profile));
+ if (($inScope === TRUE) && ($profile instanceof Profile)) {
+ /*
+ * Check scope, else a privacy leaks happens this way:
+ *
+ * 1) Bob and Alice follow each other and write private notices
+ * (this->scope=2) to each other.
+ * 2) Bob uses tags in his private notice to alice (which she can
+ * read from him).
+ * 3) Alice adds that notice (with tags) to her favorites
+ * ("faving") it.
+ * 4) The tags from Bob's private notice becomes visible in Alice's
+ * profile.
+ *
+ * This has the simple background that the scope is not being
+ * re-checked. This has to be done here at this point because given
+ * above scenario is a privacy leak as the tags may be *really*
+ * private (nobody else shall see them) such as initmate words or
+ * very political words.
+ */
+ $inScope = $this->inScope($profile);
+ //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . ']: inScope=' . intval($inScope) . ' - After inScope() has been called.');
+ }
+
$tags = array();
$keypart = sprintf('notice:tags:%d', $this->id);
} else {
$tag = new Notice_tag();
$tag->notice_id = $this->id;
- if ($tag->find()) {
+
+ // Check scope for privacy-leak protection (see some lines above why)
+ if (($inScope === TRUE) && ($tag->find())) {
while ($tag->fetch()) {
$tags[] = $tag->tag;
}
($this->is_local != Notice::GATEWAY));
}
+ public function isPrivateScope () {
+ return ($this->scope != Notice::SITE_SCOPE &&
+ $this->scope != Notice::PUBLIC_SCOPE);
+ }
+
/**
* Check that the given profile is allowed to read, respond to, or otherwise
* act on this notice.
*
* @return boolean whether the profile is in the notice's scope
*/
- function inScope($profile)
+ function inScope(Profile $profile=null)
{
if (is_null($profile)) {
$keypart = sprintf('notice:in-scope-for:%d:null', $this->id);
return ($result == 1) ? true : false;
}
- protected function _inScope($profile)
+ protected function _inScope(Profile $profile=null)
{
$scope = is_null($this->scope) ? self::defaultScope() : $this->getScope();
return !$this->isHiddenSpam($profile);
}
- function isHiddenSpam($profile) {
+ function isHiddenSpam(Profile $profile=null) {
// Hide posts by silenced users from everyone but moderators.
return $scope;
}
- static function fillProfiles($notices)
+ static function fillProfiles(array $notices)
{
$map = self::getProfiles($notices);
foreach ($notices as $entry=>$notice) {
return array_values($map);
}
- static function getProfiles(&$notices)
+ static function getProfiles(array &$notices)
{
$ids = array();
foreach ($notices as $notice) {
return Profile::pivotGet('id', $ids);
}
- static function fillGroups(&$notices)
+ static function fillGroups(array &$notices)
{
$ids = self::_idsOf($notices);
$gis = Group_inbox::listGet('notice_id', $ids);
return array_keys($ids);
}
- static function fillAttachments(&$notices)
+ static function fillAttachments(array &$notices)
{
$ids = self::_idsOf($notices);
$f2pMap = File_to_post::listGet('post_id', $ids);
}
}
- static function fillReplies(&$notices)
+ static function fillReplies(array &$notices)
{
$ids = self::_idsOf($notices);
$replyMap = Reply::listGet('notice_id', $ids);
}
}
+ /**
+ * Checks whether the current profile is allowed (in scope) to see this notice.
+ *
+ * @return $inScope Whether the current profile is allowed to see this notice
+ */
+ function isCurrentProfileInScope () {
+ // Check scope, default is allowed
+ $inScope = TRUE;
+
+ //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] this->tag=' . $this->tag . ',this->id=' . $this->id . ',this->scope=' . $this->scope);
+
+ // Is it private scope?
+ if ($this->isPrivateScope()) {
+ // 2) Get current profile
+ $profile = Profile::current();
+
+ // Is the profile not set?
+ if (!$profile instanceof Profile) {
+ // Public viewer shall not see a tag from a private dent (privacy leak)
+ //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] Not logged in (public view).');
+ $inScope = FALSE;
+ } elseif (!$this->inScope($profile)) {
+ // Current profile is not in scope (not allowed to see) of notice
+ //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] profile->id=' . $profile->id . ' is not allowed to see this notice.');
+ $inScope = FALSE;
+ }
+ }
+
+ // Return result
+ //* NOISY-DEBUG: */ common_debug('[' . __METHOD__ . ':' . __LINE__ . '] this->tag=' . $this->tag . ',this->weight=' . $this->weight . ',inScope=' . intval($inScope) . ' - EXIT!');
+ return $inScope;
+ }
+
static public function beforeSchemaUpdate()
{
$table = strtolower(get_called_class());
return false;
}
- static function getFeedAuthor($feedEl)
+ static function getFeedAuthor(DOMElement $feedEl)
{
// Try old and deprecated activity:subject
return null;
}
- static function compareTypes($type, $objects)
+ static function compareTypes($type, array $objects)
{
$type = self::resolveUri($type);
- foreach ((array)$objects as $object) {
+ foreach ($objects as $object) {
if ($type === self::resolveUri($object)) {
return true;
}
}
static function findLocalObject(array $uris, $type=ActivityObject::NOTE) {
- $object = null;
- // TODO: Extend this in plugins etc.
- if (Event::handle('StartFindLocalActivityObject', array($uris, $type, &$object))) {
+ $obj_class = null;
+ // TODO: Extend this in plugins etc. and describe in EVENTS.txt
+ if (Event::handle('StartFindLocalActivityObject', array($uris, $type, &$obj_class))) {
switch (self::resolveUri($type)) {
case ActivityObject::PERSON:
// GROUP will also be here in due time...
- $object = new Profile();
+ $obj_class = 'Profile';
break;
default:
- $object = new Notice();
+ $obj_class = 'Notice';
}
}
- foreach (array_unique($uris) as $uri) {
+ $object = null;
+ $uris = array_unique($uris);
+ foreach ($uris as $uri) {
try {
// the exception thrown will cancel before reaching $object
- $object = call_user_func(array($object, 'fromUri'), $uri);
+ $object = call_user_func("{$obj_class}::fromUri", $uri);
break;
- } catch (Exception $e) {
- common_debug('Could not find local activity object from uri: '.$uri);
+ } catch (UnknownUriException $e) {
+ common_debug('Could not find local activity object from uri: '.$e->object_uri);
}
}
- if (!empty($object)) {
- Event::handle('EndFindLocalActivityObject', array($object->getUri(), $type, $object));
- } else {
- throw new ServerException('Could not find any activityobject stored locally with given URI');
+ if (!$object instanceof Managed_DataObject) {
+ throw new ServerException('Could not find any activityobject stored locally with given URIs: '.var_export($uris,true));
}
+ Event::handle('EndFindLocalActivityObject', array($object->getUri(), $object->getObjectType(), $object));
return $object;
}
define('GNUSOCIAL_ENGINE_URL', 'https://www.gnu.org/software/social/');
define('GNUSOCIAL_BASE_VERSION', '1.2.0');
- define('GNUSOCIAL_LIFECYCLE', 'beta2'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
+ define('GNUSOCIAL_LIFECYCLE', 'beta3'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release'
define('GNUSOCIAL_VERSION', GNUSOCIAL_BASE_VERSION . '-' . GNUSOCIAL_LIFECYCLE);
-define('GNUSOCIAL_CODENAME', 'Not decided yet');
+define('GNUSOCIAL_CODENAME', 'Only a fixed bug is a good bug.');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
return GNUsocial::haveConfig();
}
+function common_get_temp_dir()
+{
+ // Try to get it from php.ini first
+ $temp_path = trim(ini_get('upload_tmp_dir'));
+
+ // Is it empty?
+ if (strlen($temp_path) == 0) {
+ // Then try sys_get_temp_dir()
+ $temp_path = trim(sys_get_temp_dir());
+
+ // Still empty?
+ if (strlen($temp_path) == 0) {
+ // Then set it to /tmp (ugly)
+ // @TODO Hard-coded non-Windows stuff!
+ $temp_path = '/tmp';
+ }
+ }
+
+ // Return found path
+ return $temp_path;
+}
+
function GNUsocial_class_autoload($cls)
{
if (file_exists(INSTALLDIR.'/classes/' . $cls . '.php')) {
}
}
+
// Autoload function queue, starting with our own discovery method
spl_autoload_register('GNUsocial_class_autoload');
$this->showParent();
} catch (NoParentNoticeException $e) {
// no parent notice
+ } catch (InvalidUrlException $e) {
+ // parent had an invalid URL so we can't show it
}
if ($this->addressees) { $this->showAddressees(); }
$this->elementEnd('div');
{
if (Event::handle('StartShowNoticeOptions', array($this))) {
$user = common_current_user();
- if ($user) {
+
+ if ($user instanceof User) {
$this->out->elementStart('div', 'notice-options');
if (Event::handle('StartShowNoticeOptionItems', array($this))) {
$this->showReplyLink();
}
$this->out->elementEnd('div');
}
+
Event::handle('EndShowNoticeOptions', array($this));
}
}
'uri' => $uri,
'verb' => ActivityVerb::UNFAVORITE,
'object_type' => (($notice->verb == ActivityVerb::POST) ?
- $notice->object_type : ActivityObject::ACTIVITY)));
+ $notice->object_type : null)));
return true;
}
return true;
}
- function onStartShowNoticeItem($nli)
+ function onStartShowNoticeItem(NoticeListItem $nli)
{
$notice = $nli->notice;
* @link http://status.net/
*/
- if (!defined('STATUSNET')) {
- // This check helps protect against security problems;
- // your code file can't be executed directly from the web.
- exit(1);
- }
+ if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Event plugin
}
function types() {
- return array(Happening::OBJECT_TYPE,
+ return array(Happening::OBJECT_TYPE);
+ }
+
+ function verbs() {
+ return array(ActivityVerb::POST,
RSVP::POSITIVE,
RSVP::NEGATIVE,
RSVP::POSSIBLE);
$url = $url_object->item(0)->nodeValue;
}
- $notice = null;
-
switch ($activity->verb) {
case ActivityVerb::POST:
// FIXME: get startTime, endTime, location and URL
break;
case RSVP::POSITIVE:
case RSVP::NEGATIVE:
+ case RSVP::POSSIBLE:
+ return Notice::saveActivity($activity, $actor, $options);
+ break;
+ default:
+ // TRANS: Exception thrown when event plugin comes across a undefined verb.
+ throw new Exception(_m('Unknown verb for events.'));
+ }
+ }
+
+ protected function saveObjectFromActivity(Activity $activity, Notice $stored, array $options=array())
+ {
+ $happeningObj = $activity->objects[0];
+
+ switch ($activity->verb) {
+ case RSVP::POSITIVE:
+ case RSVP::NEGATIVE:
case RSVP::POSSIBLE:
$happening = Happening::getKV('uri', $happeningObj->id);
if (empty($happening)) {
// TRANS: Exception thrown when trying to RSVP for an unknown event.
throw new Exception(_m('RSVP for unknown event.'));
}
- $notice = RSVP::saveNew($actor, $happening, $activity->verb, $options);
+ $object = RSVP::saveNewFromNotice($stored, $happening, $activity->verb);
+ // Our data model expects this
+ $stored->object_type = $activity->verb;
+ return $object;
break;
default:
- // TRANS: Exception thrown when event plugin comes across a undefined verb.
- throw new Exception(_m('Unknown verb for events.'));
+ common_log(LOG_ERR, 'Unknown verb for events.');
+ return NULL;
}
-
- return $notice;
}
/**
{
switch ($notice->object_type) {
case Happening::OBJECT_TYPE:
- common_log(LOG_DEBUG, "Deleting event from notice...");
+ common_debug("Deleting event from notice...");
$happening = Happening::fromNotice($notice);
$happening->delete();
break;
case RSVP::POSITIVE:
case RSVP::NEGATIVE:
case RSVP::POSSIBLE:
- common_log(LOG_DEBUG, "Deleting rsvp from notice...");
+ common_debug("Deleting rsvp from notice...");
$rsvp = RSVP::fromNotice($notice);
- common_log(LOG_DEBUG, "to delete: $rsvp->id");
+ common_debug("to delete: $rsvp->id");
$rsvp->delete();
break;
default:
- common_log(LOG_DEBUG, "Not deleting related, wtf...");
+ common_debug("Not deleting related, wtf...");
}
}
- function onEndShowScripts($action)
+ function onEndShowScripts(Action $action)
{
$action->script($this->path('js/event.js'));
}
- function onEndShowStyles($action)
+ function onEndShowStyles(Action $action)
{
$action->cssLink($this->path('css/event.css'));
return true;
print "Resuming core schema upgrade...";
}
- function saveNew($profile, $event, $verb, $options=array())
+ function saveNew(Profile $profile, $event, $verb, array $options = array())
{
- if (array_key_exists('uri', $options)) {
- $other = RSVP::getKV('uri', $options['uri']);
- if (!empty($other)) {
- // TRANS: Client exception thrown when trying to save an already existing RSVP ("please respond").
- throw new ClientException(_m('RSVP already exists.'));
- }
+ $eventNotice = $event->getNotice();
+ $options = array_merge(array('source' => 'web'), $options);
+
+ $act = new Activity();
+ $act->type = ActivityObject::ACTIVITY;
+ $act->verb = $verb;
+ $act->time = $options['created'] ? strtotime($options['created']) : time();
+ $act->title = _m("RSVP");
+ $act->actor = $profile->asActivityObject();
+ $act->target = $eventNotice->asActivityObject();
+ $act->objects = array(clone($act->target));
+ $act->content = RSVP::toHTML($profile, $event, self::codeFor($verb));
+
+ $act->id = common_local_url('showrsvp', array('id' => UUID::gen()));
+ $act->link = $act->id;
+
+ $saved = Notice::saveActivity($act, $profile, $options);
+
+ return $saved;
+ }
+
+ function saveNewFromNotice($notice, $event, $verb)
+ {
+ $other = RSVP::getKV('uri', $notice->uri);
+ if (!empty($other)) {
+ // TRANS: Client exception thrown when trying to save an already existing RSVP ("please respond").
+ throw new ClientException(_m('RSVP already exists.'));
}
+ $profile = $notice->getProfile();
+
try {
$other = RSVP::getByKeys( [ 'profile_id' => $profile->getID(),
'event_uri' => $event->getUri(),
$rsvp = new RSVP();
- $rsvp->id = UUID::gen();
- $rsvp->profile_id = $profile->getID();
- $rsvp->event_uri = $event->getUri();
- $rsvp->response = self::codeFor($verb);
-
- if (array_key_exists('created', $options)) {
- $rsvp->created = $options['created'];
- } else {
- $rsvp->created = common_sql_now();
- }
-
- if (array_key_exists('uri', $options)) {
- $rsvp->uri = $options['uri'];
- } else {
- $rsvp->uri = common_local_url('showrsvp',
- array('id' => $rsvp->id));
- }
+ preg_match('/\/([^\/]+)\/*/', $notice->uri, $match);
+ $rsvp->id = $match[1] ? $match[1] : UUID::gen();
+ $rsvp->profile_id = $profile->id;
+ $rsvp->event_id = $event->id;
+ $rsvp->response = self::codeFor($verb);
+ $rsvp->created = $notice->created;
+ $rsvp->uri = $notice->uri;
$rsvp->insert();
self::blow('rsvp:for-event:%s', $event->getUri());
- // XXX: come up with something sexier
-
- $content = $rsvp->asString();
-
- $rendered = $rsvp->asHTML();
-
- $options = array_merge(array('object_type' => $verb),
- $options);
-
- if (!array_key_exists('uri', $options)) {
- $options['uri'] = $rsvp->uri;
- }
-
- $eventNotice = $event->getNotice();
-
- if (!empty($eventNotice)) {
- $options['reply_to'] = $eventNotice->getID();
- }
-
- $saved = Notice::saveNew($profile->getID(),
- $content,
- array_key_exists('source', $options) ?
- $options['source'] : 'web',
- $options);
-
- return $saved;
+ return $rsvp;
}
function codeFor($verb)
$this->response);
}
- static function toHTML($profile, $event, $response)
+ static function toHTML(Profile $profile, Event $event, $response)
{
$fmt = null;
public function delete($useWhere=false)
{
- $profile = Profile::getKV('id', $this->user_id);
- $notice = Notice::getKV('id', $this->notice_id);
-
$result = null;
- if (Event::handle('StartDisfavorNotice', array($profile, $notice, &$result))) {
+ try {
+ $profile = $this->getActor();
+ $notice = $this->getTarget();
- $result = parent::delete($useWhere);
+ if (Event::handle('StartDisfavorNotice', array($profile, $notice, &$result))) {
- self::blowCacheForProfileId($this->user_id);
- self::blowCacheForNoticeId($this->notice_id);
- self::blow('popular');
+ $result = parent::delete($useWhere);
- if ($result) {
- Event::handle('EndDisfavorNotice', array($profile, $notice));
+ if ($result !== false) {
+ Event::handle('EndDisfavorNotice', array($profile, $notice));
+ }
}
+
+ } catch (NoResultException $e) {
+ // In case there's some inconsistency where the profile or notice was deleted without losing the fave db entry
+ common_log(LOG_INFO, '"'.get_class($e->obj).'" with id=='.var_export($e->obj->id, true).' object not found when deleting favorite, ignoring...');
+ } catch (EmptyIdException $e) {
+ // Some buggy instances of GNU social have had favroites with notice id==0 stored in the database
+ common_log(LOG_INFO, '"'.get_class($e->obj).'"object had empty id deleting favorite, ignoring...');
+ }
+
+ // If we catch an exception above, then $result===null because parent::delete only returns an int>=0 or boolean false
+ if (is_null($result)) {
+ // Delete it without the event, as something is wrong and we don't want it anyway.
+ $result = parent::delete($useWhere);
}
+ // Err, apparently we can reference $this->user_id after parent::delete,
+ // I guess it's safe because this is the order it was before!
+ self::blowCacheForProfileId($this->user_id);
+ self::blowCacheForNoticeId($this->notice_id);
+ self::blow('popular');
+
return $result;
}
*
* @return array Array of Fave objects
*/
- static public function byNotice($notice)
+ static public function byNotice(Notice $notice)
{
if (!isset(self::$_faves[$notice->id])) {
self::fillFaves(array($notice->id));
public function getTarget()
{
- // throws exception on failure
- $target = new Notice();
- $target->id = $this->notice_id;
- if (!$target->find(true)) {
- throw new NoResultException($target);
- }
-
- return $target;
+ return Notice::getByID($this->notice_id);
}
public function getTargetObject()