diff --git a/actions/apigroupcreate.php b/actions/apigroupcreate.php index 028d76a782..145806356c 100644 --- a/actions/apigroupcreate.php +++ b/actions/apigroupcreate.php @@ -123,7 +123,9 @@ class ApiGroupCreateAction extends ApiAuthAction 'description' => $this->description, 'location' => $this->location, 'aliases' => $this->aliases, - 'userid' => $this->user->id)); + 'userid' => $this->user->id, + 'local' => true)); + switch($this->format) { case 'xml': $this->showSingleXmlGroup($group); @@ -306,9 +308,9 @@ class ApiGroupCreateAction extends ApiAuthAction function groupNicknameExists($nickname) { - $group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); - if (!empty($group)) { + if (!empty($local)) { return true; } diff --git a/actions/apigrouplistall.php b/actions/apigrouplistall.php index d2ef2978aa..e1b54a8322 100644 --- a/actions/apigrouplistall.php +++ b/actions/apigrouplistall.php @@ -134,13 +134,13 @@ class ApiGroupListAllAction extends ApiPrivateAuthAction function getGroups() { - $groups = array(); - - // XXX: Use the $page, $count, $max_id, $since_id, and $since parameters + $qry = 'SELECT user_group.* '. + 'from user_group join local_group on user_group.id = local_group.group_id '. + 'order by created desc '; $group = new User_group(); - $group->orderBy('created DESC'); - $group->find(); + + $group->query($qry); while ($group->fetch()) { $groups[] = clone($group); diff --git a/actions/apistatusnetconfig.php b/actions/apistatusnetconfig.php index 0345a9bc07..bff8313b5c 100644 --- a/actions/apistatusnetconfig.php +++ b/actions/apistatusnetconfig.php @@ -32,8 +32,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/api.php'; - /** * Gives a full dump of configuration variables for this instance * of StatusNet, minus variables that may be security-sensitive (like diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index 0bb4860ea7..04456ffea4 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -107,8 +107,6 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $sitename = common_config('site', 'name'); $avatar = $this->group->homepage_logo; $title = sprintf(_("%s timeline"), $this->group->nickname); - $taguribase = TagURI::base(); - $id = "tag:$taguribase:GroupTimeline:" . $this->group->id; $subtitle = sprintf( _('Updates from %1$s on %2$s!'), @@ -138,19 +136,9 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction try { - // If this was called using an integer ID, i.e.: using the canonical - // URL for this group's feed, then pass the Group object into the feed, - // so the OStatus plugin, and possibly other plugins, can access it. - // Feels sorta hacky. -- Z + $atom = new AtomGroupNoticeFeed($this->group); - $atom = null; - $id = $this->arg('id'); - - if (strval(intval($id)) === strval($id)) { - $atom = new AtomGroupNoticeFeed($this->group); - } else { - $atom = new AtomGroupNoticeFeed(); - } + // @todo set all this Atom junk up inside the feed class $atom->setId($id); $atom->setTitle($title); @@ -169,6 +157,8 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $aargs['id'] = $id; } + $atom->setId($this->getSelfUri('ApiTimelineGroup', $aargs)); + $atom->addLink( $this->getSelfUri('ApiTimelineGroup', $aargs), array('rel' => 'self', 'type' => 'application/atom+xml') diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 3e849cc786..b3ded97c0f 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -116,8 +116,6 @@ class ApiTimelineUserAction extends ApiBareAuthAction $sitename = common_config('site', 'name'); $title = sprintf(_("%s timeline"), $this->user->nickname); - $taguribase = TagURI::base(); - $id = "tag:$taguribase:UserTimeline:" . $this->user->id; $link = common_local_url( 'showstream', array('nickname' => $this->user->nickname) @@ -148,21 +146,10 @@ class ApiTimelineUserAction extends ApiBareAuthAction header('Content-Type: application/atom+xml; charset=utf-8'); - // If this was called using an integer ID, i.e.: using the canonical - // URL for this user's feed, then pass the User object into the feed, - // so the OStatus plugin, and possibly other plugins, can access it. - // Feels sorta hacky. -- Z + // @todo set all this Atom junk up inside the feed class - $atom = null; - $id = $this->arg('id'); + $atom = new AtomUserNoticeFeed($this->user); - if (strval(intval($id)) === strval($id)) { - $atom = new AtomUserNoticeFeed($this->user); - } else { - $atom = new AtomUserNoticeFeed(); - } - - $atom->setId($id); $atom->setTitle($title); $atom->setSubtitle($subtitle); $atom->setLogo($logo); @@ -181,6 +168,8 @@ class ApiTimelineUserAction extends ApiBareAuthAction $aargs['id'] = $id; } + $atom->setId($this->getSelfUri('ApiTimelineUser', $aargs)); + $atom->addLink( $this->getSelfUri('ApiTimelineUser', $aargs), array('rel' => 'self', 'type' => 'application/atom+xml') diff --git a/actions/blockedfromgroup.php b/actions/blockedfromgroup.php index 0b4caf5bf3..a0598db270 100644 --- a/actions/blockedfromgroup.php +++ b/actions/blockedfromgroup.php @@ -74,7 +74,14 @@ class BlockedfromgroupAction extends GroupDesignAction return false; } - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $this->clientError(_('No such group.'), 404); diff --git a/actions/editgroup.php b/actions/editgroup.php index ad0b6e185d..4b596cade9 100644 --- a/actions/editgroup.php +++ b/actions/editgroup.php @@ -86,10 +86,14 @@ class EditgroupAction extends GroupDesignAction } $groupid = $this->trimmed('groupid'); + if ($groupid) { $this->group = User_group::staticGet('id', $groupid); } else { - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if ($local) { + $this->group = User_group::staticGet('id', $local->group_id); + } } if (!$this->group) { @@ -245,6 +249,7 @@ class EditgroupAction extends GroupDesignAction $this->group->homepage = $homepage; $this->group->description = $description; $this->group->location = $location; + $this->group->mainpage = common_local_url('showgroup', array('nickname' => $nickname)); $result = $this->group->update($orig); @@ -259,6 +264,12 @@ class EditgroupAction extends GroupDesignAction $this->serverError(_('Could not create aliases.')); } + if ($nickname != $orig->nickname) { + common_log(LOG_INFO, "Saving local group info."); + $local = Local_group::staticGet('group_id', $this->group->id); + $local->setNickname($nickname); + } + $this->group->query('COMMIT'); if ($this->group->nickname != $orig->nickname) { @@ -272,10 +283,10 @@ class EditgroupAction extends GroupDesignAction function nicknameExists($nickname) { - $group = User_group::staticGet('nickname', $nickname); + $group = Local_group::staticGet('nickname', $nickname); if (!empty($group) && - $group->id != $this->group->id) { + $group->group_id != $this->group->id) { return true; } diff --git a/actions/foafgroup.php b/actions/foafgroup.php index f5fd7fe885..ebdf1cee25 100644 --- a/actions/foafgroup.php +++ b/actions/foafgroup.php @@ -56,7 +56,14 @@ class FoafGroupAction extends Action return false; } - $this->group = User_group::staticGet('nickname', $this->nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $this->clientError(_('No such group.'), 404); @@ -113,7 +120,7 @@ class FoafGroupAction extends Action if ($this->group->homepage_logo) { $this->element('depiction', array('rdf:resource' => $this->group->homepage_logo)); } - + $members = $this->group->getMembers(); $member_details = array(); while ($members->fetch()) { @@ -123,7 +130,7 @@ class FoafGroupAction extends Action ); $this->element('member', array('rdf:resource' => $member_uri)); } - + $admins = $this->group->getAdmins(); while ($admins->fetch()) { $admin_uri = common_local_url('userbyid', array('id'=>$admins->id)); @@ -132,7 +139,7 @@ class FoafGroupAction extends Action } $this->elementEnd('Group'); - + ksort($member_details); foreach ($member_details as $uri => $details) { if ($details['is_admin']) @@ -158,7 +165,7 @@ class FoafGroupAction extends Action )); } } - + $this->elementEnd('rdf:RDF'); $this->endXML(); } diff --git a/actions/groupdesignsettings.php b/actions/groupdesignsettings.php index e290ba5141..526226a285 100644 --- a/actions/groupdesignsettings.php +++ b/actions/groupdesignsettings.php @@ -90,7 +90,10 @@ class GroupDesignSettingsAction extends DesignSettingsAction if ($groupid) { $this->group = User_group::staticGet('id', $groupid); } else { - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if ($local) { + $this->group = User_group::staticGet('id', $local->group_id); + } } if (!$this->group) { diff --git a/actions/grouplogo.php b/actions/grouplogo.php index 3c9b562962..f414a23cc3 100644 --- a/actions/grouplogo.php +++ b/actions/grouplogo.php @@ -92,7 +92,10 @@ class GrouplogoAction extends GroupDesignAction if ($groupid) { $this->group = User_group::staticGet('id', $groupid); } else { - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + if ($local) { + $this->group = User_group::staticGet('id', $local->group_id); + } } if (!$this->group) { diff --git a/actions/groupmembers.php b/actions/groupmembers.php index f16e972a41..a16debd7b0 100644 --- a/actions/groupmembers.php +++ b/actions/groupmembers.php @@ -77,7 +77,14 @@ class GroupmembersAction extends GroupDesignAction return false; } - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $this->clientError(_('No such group.'), 404); diff --git a/actions/grouprss.php b/actions/grouprss.php index 866fc66eb1..490f6f945c 100644 --- a/actions/grouprss.php +++ b/actions/grouprss.php @@ -92,7 +92,14 @@ class groupRssAction extends Rss10Action return false; } - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $this->clientError(_('No such group.'), 404); diff --git a/actions/groups.php b/actions/groups.php index 10a1d5964d..8aacff8b0e 100644 --- a/actions/groups.php +++ b/actions/groups.php @@ -109,17 +109,21 @@ class GroupsAction extends Action } $offset = ($this->page-1) * GROUPS_PER_PAGE; - $limit = GROUPS_PER_PAGE + 1; + $limit = GROUPS_PER_PAGE + 1; + + $qry = 'SELECT user_group.* '. + 'from user_group join local_group on user_group.id = local_group.group_id '. + 'order by user_group.created desc '. + 'limit ' . $limit . ' offset ' . $offset; $groups = new User_group(); - $groups->orderBy('created DESC'); - $groups->limit($offset, $limit); $cnt = 0; - if ($groups->find()) { - $gl = new GroupList($groups, null, $this); - $cnt = $gl->show(); - } + + $groups->query($qry); + + $gl = new GroupList($groups, null, $this); + $cnt = $gl->show(); $this->pagination($this->page > 1, $cnt > GROUPS_PER_PAGE, $this->page, 'groups'); diff --git a/actions/hcard.php b/actions/hcard.php new file mode 100644 index 0000000000..55d0f65c8f --- /dev/null +++ b/actions/hcard.php @@ -0,0 +1,120 @@ +. + * + * @category Personal + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * User profile page + * + * @category Personal + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class HcardAction extends Action +{ + var $user; + var $profile; + + function prepare($args) + { + parent::prepare($args); + + $nickname_arg = $this->arg('nickname'); + $nickname = common_canonical_nickname($nickname_arg); + + // Permanent redirect on non-canonical nickname + + if ($nickname_arg != $nickname) { + $args = array('nickname' => $nickname); + common_redirect(common_local_url('hcard', $args), 301); + return false; + } + + $this->user = User::staticGet('nickname', $nickname); + + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->profile = $this->user->getProfile(); + + if (!$this->profile) { + $this->serverError(_('User has no profile.')); + return false; + } + + return true; + } + + function handle($args) + { + parent::handle($args); + $this->showPage(); + } + + function title() + { + return $this->profile->getBestName(); + } + + function showContent() + { + $up = new ShortUserProfile($this, $this->user, $this->profile); + $up->show(); + } + + function showHeader() + { + return; + } + + function showAside() + { + return; + } + + function showSecondaryNav() + { + return; + } +} + +class ShortUserProfile extends UserProfile +{ + function showEntityActions() + { + return; + } +} \ No newline at end of file diff --git a/actions/joingroup.php b/actions/joingroup.php index 235e5ab4c2..f87e5dae29 100644 --- a/actions/joingroup.php +++ b/actions/joingroup.php @@ -62,23 +62,33 @@ class JoingroupAction extends Action } $nickname_arg = $this->trimmed('nickname'); - $nickname = common_canonical_nickname($nickname_arg); + $id = intval($this->arg('id')); + if ($id) { + $this->group = User_group::staticGet('id', $id); + } else if ($nickname_arg) { + $nickname = common_canonical_nickname($nickname_arg); - // Permanent redirect on non-canonical nickname + // Permanent redirect on non-canonical nickname - if ($nickname_arg != $nickname) { - $args = array('nickname' => $nickname); - common_redirect(common_local_url('joingroup', $args), 301); + if ($nickname_arg != $nickname) { + $args = array('nickname' => $nickname); + common_redirect(common_local_url('leavegroup', $args), 301); + return false; + } + + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); + } else { + $this->clientError(_('No nickname or ID.'), 404); return false; } - if (!$nickname) { - $this->clientError(_('No nickname.'), 404); - return false; - } - - $this->group = User_group::staticGet('nickname', $nickname); - if (!$this->group) { $this->clientError(_('No such group.'), 404); return false; diff --git a/actions/leavegroup.php b/actions/leavegroup.php index 9b9d83b6ca..329b5aafe1 100644 --- a/actions/leavegroup.php +++ b/actions/leavegroup.php @@ -62,23 +62,33 @@ class LeavegroupAction extends Action } $nickname_arg = $this->trimmed('nickname'); - $nickname = common_canonical_nickname($nickname_arg); + $id = intval($this->arg('id')); + if ($id) { + $this->group = User_group::staticGet('id', $id); + } else if ($nickname_arg) { + $nickname = common_canonical_nickname($nickname_arg); - // Permanent redirect on non-canonical nickname + // Permanent redirect on non-canonical nickname - if ($nickname_arg != $nickname) { - $args = array('nickname' => $nickname); - common_redirect(common_local_url('leavegroup', $args), 301); + if ($nickname_arg != $nickname) { + $args = array('nickname' => $nickname); + common_redirect(common_local_url('leavegroup', $args), 301); + return false; + } + + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); + } else { + $this->clientError(_('No nickname or ID.'), 404); return false; } - if (!$nickname) { - $this->clientError(_('No nickname.'), 404); - return false; - } - - $this->group = User_group::staticGet('nickname', $nickname); - if (!$this->group) { $this->clientError(_('No such group.'), 404); return false; diff --git a/actions/newgroup.php b/actions/newgroup.php index 25da7f8fc7..75bc293ec6 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -180,6 +180,8 @@ class NewgroupAction extends Action } } + $mainpage = common_local_url('showgroup', array('nickname' => $nickname)); + $cur = common_current_user(); // Checked in prepare() above @@ -192,16 +194,18 @@ class NewgroupAction extends Action 'description' => $description, 'location' => $location, 'aliases' => $aliases, - 'userid' => $cur->id)); + 'userid' => $cur->id, + 'mainpage' => $mainpage, + 'local' => true)); common_redirect($group->homeUrl(), 303); } function nicknameExists($nickname) { - $group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); - if (!empty($group)) { + if (!empty($local)) { return true; } diff --git a/actions/showgroup.php b/actions/showgroup.php index eb12389029..0139ba157d 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -122,7 +122,15 @@ class ShowgroupAction extends GroupDesignAction return false; } - $this->group = User_group::staticGet('nickname', $nickname); + $local = Local_group::staticGet('nickname', $nickname); + + if (!$local) { + common_log(LOG_NOTICE, "Couldn't find local group for nickname '$nickname'"); + $this->clientError(_('No such group.'), 404); + return false; + } + + $this->group = User_group::staticGet('id', $local->group_id); if (!$this->group) { $alias = Group_alias::staticGet('alias', $nickname); diff --git a/actions/subscribe.php b/actions/subscribe.php index 3745311b66..b1243f3933 100644 --- a/actions/subscribe.php +++ b/actions/subscribe.php @@ -145,7 +145,7 @@ class SubscribeAction extends Action $this->element('title', null, _('Subscribed')); $this->elementEnd('head'); $this->elementStart('body'); - $unsubscribe = new UnsubscribeForm($this, $this->other->getProfile()); + $unsubscribe = new UnsubscribeForm($this, $this->other); $unsubscribe->show(); $this->elementEnd('body'); $this->elementEnd('html'); diff --git a/actions/twitapisearchatom.php b/actions/twitapisearchatom.php index e389ddec84..24aa619bd7 100644 --- a/actions/twitapisearchatom.php +++ b/actions/twitapisearchatom.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/api.php'; - /** * Action for outputting search results in Twitter compatible Atom * format. diff --git a/actions/twitapisearchjson.php b/actions/twitapisearchjson.php index 741ed78d63..b5c006aa7b 100644 --- a/actions/twitapisearchjson.php +++ b/actions/twitapisearchjson.php @@ -31,7 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/api.php'; require_once INSTALLDIR.'/lib/jsonsearchresultslist.php'; /** diff --git a/actions/twitapitrends.php b/actions/twitapitrends.php index 779405e6d6..5a04569a22 100644 --- a/actions/twitapitrends.php +++ b/actions/twitapitrends.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/api.php'; - /** * Returns the top ten queries that are currently trending * diff --git a/classes/Local_group.php b/classes/Local_group.php new file mode 100644 index 0000000000..42312ec63c --- /dev/null +++ b/classes/Local_group.php @@ -0,0 +1,46 @@ +decache(); + $qry = 'UPDATE local_group set nickname = "'.$nickname.'" where group_id = ' . $this->group_id; + + $result = $this->query($qry); + + if ($result) { + $this->nickname = $nickname; + $this->fixupTimestamps(); + $this->encache(); + } else { + common_log_db_error($local, 'UPDATE', __FILE__); + throw new ServerException(_('Could not update local group.')); + } + + return $result; + } +} diff --git a/classes/Notice.php b/classes/Notice.php index e8d5c45cb2..ac4640534c 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -121,6 +121,9 @@ class Notice extends Memcached_DataObject $result = parent::delete(); } + /** + * Extract #hashtags from this notice's content and save them to the database. + */ function saveTags() { /* extract all #hastags */ @@ -129,14 +132,22 @@ class Notice extends Memcached_DataObject return true; } + /* Add them to the database */ + return $this->saveKnownTags($match[1]); + } + + /** + * 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) + { //turn each into their canonical tag //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag - $hashtags = array(); - for($i=0; $isaveTag($hashtag); @@ -145,6 +156,10 @@ class Notice extends Memcached_DataObject return true; } + /** + * Record a single hash tag as associated with this notice. + * Tag format and uniqueness must be validated by caller. + */ function saveTag($hashtag) { $tag = new Notice_tag(); @@ -194,6 +209,8 @@ class Notice extends Memcached_DataObject * place of extracting @-replies from content. * array 'groups' list of group IDs to deliver to, in place of * extracting ! tags from content + * array 'tags' list of hashtag strings to save with the notice + * in place of extracting # tags from content * @fixme tag override * * @return Notice @@ -343,6 +360,8 @@ class Notice extends Memcached_DataObject $notice->blowOnInsert(); + // Save per-notice metadata... + if (isset($replies)) { $notice->saveKnownReplies($replies); } else { @@ -355,6 +374,16 @@ class Notice extends Memcached_DataObject $notice->saveGroups(); } + if (isset($tags)) { + $notice->saveKnownTags($tags); + } else { + $notice->saveTags(); + } + + // @fixme pass in data for URLs too? + $notice->saveUrls(); + + // Prepare inbox delivery, may be queued to background. $notice->distribute(); return $notice; @@ -1067,6 +1096,7 @@ class Notice extends Memcached_DataObject 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', 'xmlns:georss' => 'http://www.georss.org/georss', 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', + 'xmlns:media' => 'http://purl.org/syndication/atommedia', 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0', 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); } else { diff --git a/classes/User_group.php b/classes/User_group.php index 1382aa407c..7240e27037 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -10,21 +10,23 @@ class User_group extends Memcached_DataObject public $__table = 'user_group'; // table name public $id; // int(4) primary_key not_null - public $nickname; // varchar(64) unique_key + public $nickname; // varchar(64) public $fullname; // varchar(255) public $homepage; // varchar(255) - public $description; // text() + public $description; // text public $location; // varchar(255) public $original_logo; // varchar(255) public $homepage_logo; // varchar(255) public $stream_logo; // varchar(255) public $mini_logo; // varchar(255) public $design_id; // int(4) - public $created; // datetime() not_null - public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP + public $created; // datetime not_null default_0000-00-00%2000%3A00%3A00 + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP + public $uri; // varchar(255) unique_key + public $mainpage; // varchar(255) /* Static get */ - function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('User_group',$k,$v); } + function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('User_group',$k,$v); } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE @@ -41,13 +43,33 @@ class User_group extends Memcached_DataObject { $url = null; if (Event::handle('StartUserGroupHomeUrl', array($this, &$url))) { - $url = common_local_url('showgroup', - array('nickname' => $this->nickname)); + // normally stored in mainpage, but older ones may be null + if (!empty($this->mainpage)) { + $url = $this->mainpage; + } else { + $url = common_local_url('showgroup', + array('nickname' => $this->nickname)); + } } Event::handle('EndUserGroupHomeUrl', array($this, &$url)); return $url; } + function getUri() + { + $uri = null; + if (Event::handle('StartUserGroupGetUri', array($this, &$uri))) { + if (!empty($this->uri)) { + $uri = $this->uri; + } else { + $uri = common_local_url('groupbyid', + array('id' => $this->id)); + } + } + Event::handle('EndUserGroupGetUri', array($this, &$uri)); + return $uri; + } + function permalink() { $url = null; @@ -377,25 +399,41 @@ class User_group extends Memcached_DataObject return $xs->getString(); } + /** + * Returns an XML string fragment with group information as an + * Activity Streams element. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @return string + */ function asActivitySubject() { - $xs = new XMLStringer(true); + return $this->asActivityNoun('subject'); + } - $xs->elementStart('activity:subject'); - $xs->element('activity:object', null, 'http://activitystrea.ms/schema/1.0/group'); - $xs->element('id', null, $this->permalink()); - $xs->element('title', null, $this->getBestName()); - $xs->element( - 'link', array( - 'rel' => 'avatar', - 'href' => empty($this->homepage_logo) - ? User_group::defaultLogo(AVATAR_PROFILE_SIZE) - : $this->homepage_logo - ) - ); - $xs->elementEnd('activity:subject'); + /** + * Returns an XML string fragment with group information as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity', 'georss', and 'poco' namespace has been + * previously defined. + * + * @param string $element one of 'actor', 'subject', 'object', 'target' + * + * @return string + */ + function asActivityNoun($element) + { + $noun = ActivityObject::fromGroup($this); + return $noun->asString('activity:' . $element); + } - return $xs->getString(); + function getAvatar() + { + return empty($this->homepage_logo) + ? User_group::defaultLogo(AVATAR_PROFILE_SIZE) + : $this->homepage_logo; } static function register($fields) { @@ -413,28 +451,31 @@ class User_group extends Memcached_DataObject $group->homepage = $homepage; $group->description = $description; $group->location = $location; + $group->uri = $uri; + $group->mainpage = $mainpage; $group->created = common_sql_now(); $result = $group->insert(); if (!$result) { common_log_db_error($group, 'INSERT', __FILE__); - $this->serverError( - _('Could not create group.'), - 500, - $this->format - ); - return; + throw new ServerException(_('Could not create group.')); } + + if (!isset($uri) || empty($uri)) { + $orig = clone($group); + $group->uri = common_local_url('groupbyid', array('id' => $group->id)); + $result = $group->update($orig); + if (!$result) { + common_log_db_error($group, 'UPDATE', __FILE__); + throw new ServerException(_('Could not set group uri.')); + } + } + $result = $group->setAliases($aliases); if (!$result) { - $this->serverError( - _('Could not create aliases.'), - 500, - $this->format - ); - return; + throw new ServerException(_('Could not create aliases.')); } $member = new Group_member(); @@ -448,12 +489,22 @@ class User_group extends Memcached_DataObject if (!$result) { common_log_db_error($member, 'INSERT', __FILE__); - $this->serverError( - _('Could not set group membership.'), - 500, - $this->format - ); - return; + throw new ServerException(_('Could not set group membership.')); + } + + if ($local) { + $local_group = new Local_group(); + + $local_group->group_id = $group->id; + $local_group->nickname = $nickname; + $local_group->created = common_sql_now(); + + $result = $local_group->insert(); + + if (!$result) { + common_log_db_error($local_group, 'INSERT', __FILE__); + throw new ServerException(_('Could not save local group info.')); + } } $group->query('COMMIT'); diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 81c1b68b23..3fb8ee208b 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -245,13 +245,6 @@ modified = 384 group_id = K profile_id = K -[invitation] -code = 130 -user_id = 129 -address = 130 -address_type = 130 -created = 142 - [inbox] user_id = 129 notice_ids = 66 @@ -259,9 +252,26 @@ notice_ids = 66 [inbox__keys] user_id = K +[invitation] +code = 130 +user_id = 129 +address = 130 +address_type = 130 +created = 142 + [invitation__keys] code = K +[local_group] +group_id = 129 +nickname = 2 +created = 142 +modified = 384 + +[local_group__keys] +group_id = K +nickname = U + [location_namespace] id = 129 description = 2 @@ -369,7 +379,7 @@ icon = 130 source_url = 2 organization = 2 homepage = 2 -callback_url = 130 +callback_url = 2 type = 17 access_type = 17 created = 142 @@ -440,13 +450,13 @@ tag = K [queue_item] id = 129 -frame = 66 +frame = 194 transport = 130 created = 142 claimed = 14 [queue_item__keys] -id = K +id = N [related_group] group_id = 129 @@ -593,10 +603,11 @@ mini_logo = 2 design_id = 1 created = 142 modified = 384 +uri = 2 +mainpage = 2 [user_group__keys] id = N -nickname = U [user_openid] canonical = 130 @@ -627,4 +638,3 @@ modified = 384 [user_location_prefs__keys] user_id = K - diff --git a/db/beta5tobeta6.sql b/db/beta5tobeta6.sql new file mode 100644 index 0000000000..e9dff17efe --- /dev/null +++ b/db/beta5tobeta6.sql @@ -0,0 +1,28 @@ +alter table oauth_application + modify column name varchar(255) not null unique key comment 'name of the application', + modify column access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write'; + +alter table user_group +add column uri varchar(255) unique key comment 'universal identifier', +add column mainpage varchar(255) comment 'page for group info to link to', +drop index nickname; + +create table conversation ( + id integer auto_increment primary key comment 'unique identifier', + uri varchar(225) unique comment 'URI of the conversation', + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified' +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +create table local_group ( + group_id integer primary key comment 'group represented' references user_group (id), + nickname varchar(64) unique key comment 'group represented', + + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified' + +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + +insert into local_group (group_id, nickname, created) +select id, nickname, created from user_group; + diff --git a/db/statusnet.sql b/db/statusnet.sql index 97117c80aa..4158f0167d 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -406,7 +406,7 @@ create table profile_block ( create table user_group ( id integer auto_increment primary key comment 'unique identifier', - nickname varchar(64) unique key comment 'nickname for addressing', + nickname varchar(64) comment 'nickname for addressing', fullname varchar(255) comment 'display name', homepage varchar(255) comment 'URL, cached so we dont regenerate', description text comment 'group description', @@ -421,6 +421,9 @@ create table user_group ( created datetime not null comment 'date this record was created', modified timestamp comment 'date this record was modified', + uri varchar(255) unique key comment 'universal identifier', + mainpage varchar(255) comment 'page for group info to link to', + index user_group_nickname_idx (nickname) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; @@ -641,3 +644,13 @@ create table conversation ( modified timestamp comment 'date this record was modified' ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; +create table local_group ( + + group_id integer primary key comment 'group represented' references user_group (id), + nickname varchar(64) unique key comment 'group represented', + + created datetime not null comment 'date this record was created', + modified timestamp comment 'date this record was modified' + +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; + diff --git a/js/jquery.js b/js/jquery.js index 237e1b9081..b3b95307a1 100644 --- a/js/jquery.js +++ b/js/jquery.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v1.4.1 + * jQuery JavaScript Library v1.4.2 * http://jquery.com/ * * Copyright 2010, John Resig @@ -11,7 +11,7 @@ * Copyright 2010, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * - * Date: Mon Jan 25 19:43:33 2010 -0500 + * Date: Sat Feb 13 22:33:48 2010 -0500 */ (function( window, undefined ) { @@ -86,6 +86,15 @@ jQuery.fn = jQuery.prototype = { this.length = 1; return this; } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context ) { + this.context = document; + this[0] = document.body; + this.selector = "body"; + this.length = 1; + return this; + } // Handle HTML strings if ( typeof selector === "string" ) { @@ -116,7 +125,9 @@ jQuery.fn = jQuery.prototype = { ret = buildFragment( [ match[1] ], [ doc ] ); selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes; } - + + return jQuery.merge( this, selector ); + // HANDLE: $("#id") } else { elem = document.getElementById( match[2] ); @@ -143,6 +154,7 @@ jQuery.fn = jQuery.prototype = { this.selector = selector; this.context = document; selector = document.getElementsByTagName( selector ); + return jQuery.merge( this, selector ); // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { @@ -165,16 +177,14 @@ jQuery.fn = jQuery.prototype = { this.context = selector.context; } - return jQuery.isArray( selector ) ? - this.setArray( selector ) : - jQuery.makeArray( selector, this ); + return jQuery.makeArray( selector, this ); }, // Start with an empty selector selector: "", // The current version of jQuery being used - jquery: "1.4.1", + jquery: "1.4.2", // The default length of a jQuery object is 0 length: 0, @@ -204,7 +214,14 @@ jQuery.fn = jQuery.prototype = { // (returning the new matched element set) pushStack: function( elems, name, selector ) { // Build a new jQuery matched element set - var ret = jQuery( elems || null ); + var ret = jQuery(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } // Add the old object onto the stack (as a reference) ret.prevObject = this; @@ -221,18 +238,6 @@ jQuery.fn = jQuery.prototype = { return ret; }, - // Force the current matched set of elements to become - // the specified array of elements (destroying the stack in the process) - // You should use pushStack() in order to do this, but maintain the stack - setArray: function( elems ) { - // Resetting the length to 0, then using the native Array push - // is a super-fast way to populate an object with array-like properties - this.length = 0; - push.apply( this, elems ); - - return this; - }, - // Execute a callback for every element in the matched set. // (You can seed the arguments with an array of args, but this is // only used internally.) @@ -492,6 +497,9 @@ jQuery.extend({ if ( typeof data !== "string" || !data ) { return null; } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); // Make sure the incoming data is actual JSON // Logic borrowed from http://json.org/json2.js @@ -619,6 +627,7 @@ jQuery.extend({ for ( var l = second.length; j < l; j++ ) { first[ i++ ] = second[ j ]; } + } else { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; @@ -807,7 +816,7 @@ function access( elems, key, value, exec, fn, pass ) { } // Getting an attribute - return length ? fn( elems[0], key ) : null; + return length ? fn( elems[0], key ) : undefined; } function now() { @@ -871,7 +880,10 @@ function now() { // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected, + parentNode: div.removeChild( div.appendChild( document.createElement("div") ) ).parentNode === null, + // Will be defined later + deleteExpando: true, checkClone: false, scriptEval: false, noCloneEvent: true, @@ -893,6 +905,15 @@ function now() { delete window[ id ]; } + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete script.test; + + } catch(e) { + jQuery.support.deleteExpando = false; + } + root.removeChild( script ); if ( div.attachEvent && div.fireEvent ) { @@ -923,6 +944,7 @@ function now() { document.body.appendChild( div ); jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; document.body.removeChild( div ).style.display = 'none'; + div = null; }); @@ -962,7 +984,6 @@ jQuery.props = { frameborder: "frameBorder" }; var expando = "jQuery" + now(), uuid = 0, windowData = {}; -var emptyObject = {}; jQuery.extend({ cache: {}, @@ -988,8 +1009,7 @@ jQuery.extend({ var id = elem[ expando ], cache = jQuery.cache, thisCache; - // Handle the case where there's no name immediately - if ( !name && !id ) { + if ( !id && typeof name === "string" && data === undefined ) { return null; } @@ -1003,17 +1023,16 @@ jQuery.extend({ if ( typeof name === "object" ) { elem[ expando ] = id; thisCache = cache[ id ] = jQuery.extend(true, {}, name); - } else if ( cache[ id ] ) { - thisCache = cache[ id ]; - } else if ( typeof data === "undefined" ) { - thisCache = emptyObject; - } else { - thisCache = cache[ id ] = {}; + + } else if ( !cache[ id ] ) { + elem[ expando ] = id; + cache[ id ] = {}; } + thisCache = cache[ id ]; + // Prevent overriding the named cache with undefined values if ( data !== undefined ) { - elem[ expando ] = id; thisCache[ name ] = data; } @@ -1045,15 +1064,11 @@ jQuery.extend({ // Otherwise, we want to remove all of the element's data } else { - // Clean up the element expando - try { - delete elem[ expando ]; - } catch( e ) { - // IE has trouble directly removing the expando - // but it's ok with using removeAttribute - if ( elem.removeAttribute ) { - elem.removeAttribute( expando ); - } + if ( jQuery.support.deleteExpando ) { + delete elem[ jQuery.expando ]; + + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); } // Completely remove the data cache @@ -1230,12 +1245,13 @@ jQuery.fn.extend({ elem.className = value; } else { - var className = " " + elem.className + " "; + var className = " " + elem.className + " ", setClass = elem.className; for ( var c = 0, cl = classNames.length; c < cl; c++ ) { if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { - elem.className += " " + classNames[c]; + setClass += " " + classNames[c]; } } + elem.className = jQuery.trim( setClass ); } } } @@ -1264,7 +1280,7 @@ jQuery.fn.extend({ for ( var c = 0, cl = classNames.length; c < cl; c++ ) { className = className.replace(" " + classNames[c] + " ", " "); } - elem.className = className.substring(1, className.length - 1); + elem.className = jQuery.trim( className ); } else { elem.className = ""; @@ -1520,15 +1536,16 @@ jQuery.extend({ } // elem is actually elem.style ... set the style - // Using attr for specific style information is now deprecated. Use style insead. + // Using attr for specific style information is now deprecated. Use style instead. return jQuery.style( elem, name, value ); } }); -var fcleanup = function( nm ) { - return nm.replace(/[^\w\s\.\|`]/g, function( ch ) { - return "\\" + ch; - }); -}; +var rnamespaces = /\.(.*)$/, + fcleanup = function( nm ) { + return nm.replace(/[^\w\s\.\|`]/g, function( ch ) { + return "\\" + ch; + }); + }; /* * A number of helper functions used for managing events. @@ -1550,107 +1567,104 @@ jQuery.event = { elem = window; } + var handleObjIn, handleObj; + + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + // Make sure that the function being executed has a unique ID if ( !handler.guid ) { handler.guid = jQuery.guid++; } - // if data is passed, bind to handler - if ( data !== undefined ) { - // Create temporary function pointer to original handler - var fn = handler; + // Init the element's event structure + var elemData = jQuery.data( elem ); - // Create unique handler function, wrapped around original handler - handler = jQuery.proxy( fn ); - - // Store data in unique handler - handler.data = data; + // If no elemData is found then we must be trying to bind to one of the + // banned noData elements + if ( !elemData ) { + return; } - // Init the element's event structure - var events = jQuery.data( elem, "events" ) || jQuery.data( elem, "events", {} ), - handle = jQuery.data( elem, "handle" ), eventHandle; + var events = elemData.events = elemData.events || {}, + eventHandle = elemData.handle, eventHandle; - if ( !handle ) { - eventHandle = function() { + if ( !eventHandle ) { + elemData.handle = eventHandle = function() { // Handle the second event of a trigger and when // an event is called after a page has unloaded return typeof jQuery !== "undefined" && !jQuery.event.triggered ? jQuery.event.handle.apply( eventHandle.elem, arguments ) : undefined; }; - - handle = jQuery.data( elem, "handle", eventHandle ); - } - - // If no handle is found then we must be trying to bind to one of the - // banned noData elements - if ( !handle ) { - return; } // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native - // event in IE. - handle.elem = elem; + // This is to prevent a memory leak with non-native events in IE. + eventHandle.elem = elem; // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); - types = types.split( /\s+/ ); + types = types.split(" "); - var type, i = 0; + var type, i = 0, namespaces; while ( (type = types[ i++ ]) ) { + handleObj = handleObjIn ? + jQuery.extend({}, handleObjIn) : + { handler: handler, data: data }; + // Namespaced event handlers - var namespaces = type.split("."); - type = namespaces.shift(); + if ( type.indexOf(".") > -1 ) { + namespaces = type.split("."); + type = namespaces.shift(); + handleObj.namespace = namespaces.slice(0).sort().join("."); - if ( i > 1 ) { - handler = jQuery.proxy( handler ); - - if ( data !== undefined ) { - handler.data = data; - } + } else { + namespaces = []; + handleObj.namespace = ""; } - handler.type = namespaces.slice(0).sort().join("."); + handleObj.type = type; + handleObj.guid = handler.guid; // Get the current list of functions bound to this event var handlers = events[ type ], - special = this.special[ type ] || {}; + special = jQuery.event.special[ type ] || {}; // Init the event handler queue if ( !handlers ) { - handlers = events[ type ] = {}; + handlers = events[ type ] = []; // Check for a special event handler // Only use addEventListener/attachEvent if the special // events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, handler) === false ) { + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { - elem.addEventListener( type, handle, false ); + elem.addEventListener( type, eventHandle, false ); + } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, handle ); + elem.attachEvent( "on" + type, eventHandle ); } } } if ( special.add ) { - var modifiedHandler = special.add.call( elem, handler, data, namespaces, handlers ); - if ( modifiedHandler && jQuery.isFunction( modifiedHandler ) ) { - modifiedHandler.guid = modifiedHandler.guid || handler.guid; - modifiedHandler.data = modifiedHandler.data || handler.data; - modifiedHandler.type = modifiedHandler.type || handler.type; - handler = modifiedHandler; - } - } - + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + // Add the function to the element's handler list - handlers[ handler.guid ] = handler; + handlers.push( handleObj ); // Keep track of which events have been used, for global triggering - this.global[ type ] = true; + jQuery.event.global[ type ] = true; } // Nullify elem to prevent memory leaks in IE @@ -1660,90 +1674,121 @@ jQuery.event = { global: {}, // Detach an event or set of events from an element - remove: function( elem, types, handler ) { + remove: function( elem, types, handler, pos ) { // don't do events on text and comment nodes if ( elem.nodeType === 3 || elem.nodeType === 8 ) { return; } - var events = jQuery.data( elem, "events" ), ret, type, fn; + var ret, type, fn, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, + elemData = jQuery.data( elem ), + events = elemData && elemData.events; - if ( events ) { - // Unbind all events for the element - if ( types === undefined || (typeof types === "string" && types.charAt(0) === ".") ) { - for ( type in events ) { - this.remove( elem, type + (types || "") ); - } - } else { - // types is actually an event object here - if ( types.type ) { - handler = types.handler; - types = types.type; + if ( !elemData || !events ) { + return; + } + + // types is actually an event object here + if ( types && types.type ) { + handler = types.handler; + types = types.type; + } + + // Unbind all events for the element + if ( !types || typeof types === "string" && types.charAt(0) === "." ) { + types = types || ""; + + for ( type in events ) { + jQuery.event.remove( elem, type + types ); + } + + return; + } + + // Handle multiple events separated by a space + // jQuery(...).unbind("mouseover mouseout", fn); + types = types.split(" "); + + while ( (type = types[ i++ ]) ) { + origType = type; + handleObj = null; + all = type.indexOf(".") < 0; + namespaces = []; + + if ( !all ) { + // Namespaced event handlers + namespaces = type.split("."); + type = namespaces.shift(); + + namespace = new RegExp("(^|\\.)" + + jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)") + } + + eventType = events[ type ]; + + if ( !eventType ) { + continue; + } + + if ( !handler ) { + for ( var j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( all || namespace.test( handleObj.namespace ) ) { + jQuery.event.remove( elem, origType, handleObj.handler, j ); + eventType.splice( j--, 1 ); + } } - // Handle multiple events separated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - types = types.split(/\s+/); - var i = 0; - while ( (type = types[ i++ ]) ) { - // Namespaced event handlers - var namespaces = type.split("."); - type = namespaces.shift(); - var all = !namespaces.length, - cleaned = jQuery.map( namespaces.slice(0).sort(), fcleanup ), - namespace = new RegExp("(^|\\.)" + cleaned.join("\\.(?:.*\\.)?") + "(\\.|$)"), - special = this.special[ type ] || {}; + continue; + } - if ( events[ type ] ) { - // remove the given handler for the given type - if ( handler ) { - fn = events[ type ][ handler.guid ]; - delete events[ type ][ handler.guid ]; + special = jQuery.event.special[ type ] || {}; - // remove all handlers for the given type - } else { - for ( var handle in events[ type ] ) { - // Handle the removal of namespaced events - if ( all || namespace.test( events[ type ][ handle ].type ) ) { - delete events[ type ][ handle ]; - } - } + for ( var j = pos || 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( handler.guid === handleObj.guid ) { + // remove the given handler for the given type + if ( all || namespace.test( handleObj.namespace ) ) { + if ( pos == null ) { + eventType.splice( j--, 1 ); } if ( special.remove ) { - special.remove.call( elem, namespaces, fn); + special.remove.call( elem, handleObj ); } + } - // remove generic event handler if no more handlers exist - for ( ret in events[ type ] ) { - break; - } - if ( !ret ) { - if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, jQuery.data( elem, "handle" ), false ); - } else if ( elem.detachEvent ) { - elem.detachEvent( "on" + type, jQuery.data( elem, "handle" ) ); - } - } - ret = null; - delete events[ type ]; - } + if ( pos != null ) { + break; } } } - // Remove the expando if it's no longer used - for ( ret in events ) { - break; - } - if ( !ret ) { - var handle = jQuery.data( elem, "handle" ); - if ( handle ) { - handle.elem = null; + // remove generic event handler if no more handlers exist + if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + removeEvent( elem, type, elemData.handle ); } - jQuery.removeData( elem, "events" ); - jQuery.removeData( elem, "handle" ); + + ret = null; + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + var handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + delete elemData.events; + delete elemData.handle; + + if ( jQuery.isEmptyObject( elemData ) ) { + jQuery.removeData( elem ); } } }, @@ -1774,7 +1819,7 @@ jQuery.event = { event.stopPropagation(); // Only trigger if we've ever bound an event for it - if ( this.global[ type ] ) { + if ( jQuery.event.global[ type ] ) { jQuery.each( jQuery.cache, function() { if ( this.events && this.events[type] ) { jQuery.event.trigger( event, data, this.handle.elem ); @@ -1825,9 +1870,12 @@ jQuery.event = { } else if ( !event.isDefaultPrevented() ) { var target = event.target, old, - isClick = jQuery.nodeName(target, "a") && type === "click"; + isClick = jQuery.nodeName(target, "a") && type === "click", + special = jQuery.event.special[ type ] || {}; + + if ( (!special._default || special._default.call( elem, event ) === false) && + !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { - if ( !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { try { if ( target[ type ] ) { // Make sure that we don't accidentally re-trigger the onFOO events @@ -1837,7 +1885,7 @@ jQuery.event = { target[ "on" + type ] = null; } - this.triggered = true; + jQuery.event.triggered = true; target[ type ](); } @@ -1848,53 +1896,57 @@ jQuery.event = { target[ "on" + type ] = old; } - this.triggered = false; + jQuery.event.triggered = false; } } }, handle: function( event ) { - // returned undefined or false - var all, handlers; + var all, handlers, namespaces, namespace, events; event = arguments[0] = jQuery.event.fix( event || window.event ); event.currentTarget = this; // Namespaced event handlers - var namespaces = event.type.split("."); - event.type = namespaces.shift(); + all = event.type.indexOf(".") < 0 && !event.exclusive; - // Cache this now, all = true means, any handler - all = !namespaces.length && !event.exclusive; + if ( !all ) { + namespaces = event.type.split("."); + event.type = namespaces.shift(); + namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); + } - var namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"); + var events = jQuery.data(this, "events"), handlers = events[ event.type ]; - handlers = ( jQuery.data(this, "events") || {} )[ event.type ]; + if ( events && handlers ) { + // Clone the handlers to prevent manipulation + handlers = handlers.slice(0); - for ( var j in handlers ) { - var handler = handlers[ j ]; + for ( var j = 0, l = handlers.length; j < l; j++ ) { + var handleObj = handlers[ j ]; - // Filter the functions by class - if ( all || namespace.test(handler.type) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handler; - event.data = handler.data; + // Filter the functions by class + if ( all || namespace.test( handleObj.namespace ) ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + event.handler = handleObj.handler; + event.data = handleObj.data; + event.handleObj = handleObj; + + var ret = handleObj.handler.apply( this, arguments ); - var ret = handler.apply( this, arguments ); + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); + if ( event.isImmediatePropagationStopped() ) { + break; } } - - if ( event.isImmediatePropagationStopped() ) { - break; - } - } } @@ -1973,44 +2025,39 @@ jQuery.event = { }, live: { - add: function( proxy, data, namespaces, live ) { - jQuery.extend( proxy, data || {} ); + add: function( handleObj ) { + jQuery.event.add( this, handleObj.origType, jQuery.extend({}, handleObj, {handler: liveHandler}) ); + }, - proxy.guid += data.selector + data.live; - data.liveProxy = proxy; - - jQuery.event.add( this, data.live, liveHandler, data ); + remove: function( handleObj ) { + var remove = true, + type = handleObj.origType.replace(rnamespaces, ""); - }, - - remove: function( namespaces ) { - if ( namespaces.length ) { - var remove = 0, name = new RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)"); - - jQuery.each( (jQuery.data(this, "events").live || {}), function() { - if ( name.test(this.type) ) { - remove++; - } - }); - - if ( remove < 1 ) { - jQuery.event.remove( this, namespaces[0], liveHandler ); + jQuery.each( jQuery.data(this, "events").live || [], function() { + if ( type === this.origType.replace(rnamespaces, "") ) { + remove = false; + return false; } + }); + + if ( remove ) { + jQuery.event.remove( this, handleObj.origType, liveHandler ); } - }, - special: {} + } + }, + beforeunload: { - setup: function( data, namespaces, fn ) { + setup: function( data, namespaces, eventHandle ) { // We only want to do this special case on windows if ( this.setInterval ) { - this.onbeforeunload = fn; + this.onbeforeunload = eventHandle; } return false; }, - teardown: function( namespaces, fn ) { - if ( this.onbeforeunload === fn ) { + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { this.onbeforeunload = null; } } @@ -2018,6 +2065,14 @@ jQuery.event = { } }; +var removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + elem.removeEventListener( type, handle, false ); + } : + function( elem, type, handle ) { + elem.detachEvent( "on" + type, handle ); + }; + jQuery.Event = function( src ) { // Allow instantiation without the 'new' keyword if ( !this.preventDefault ) { @@ -2095,27 +2150,24 @@ var withinElement = function( event ) { // Check if mouse(over|out) are still within the same parent element var parent = event.relatedTarget; - // Traverse up the tree - while ( parent && parent !== this ) { - // Firefox sometimes assigns relatedTarget a XUL element - // which we cannot access the parentNode property of - try { + // Firefox sometimes assigns relatedTarget a XUL element + // which we cannot access the parentNode property of + try { + // Traverse up the tree + while ( parent && parent !== this ) { parent = parent.parentNode; - - // assuming we've left the element since we most likely mousedover a xul element - } catch(e) { - break; } - } - if ( parent !== this ) { - // set the correct event type - event.type = event.data; + if ( parent !== this ) { + // set the correct event type + event.type = event.data; - // handle event if we actually just moused on to a non sub-element - jQuery.event.handle.apply( this, arguments ); - } + // handle event if we actually just moused on to a non sub-element + jQuery.event.handle.apply( this, arguments ); + } + // assuming we've left the element since we most likely mousedover a xul element + } catch(e) { } }, // In case of event delegation, we only need to rename the event.type, @@ -2143,64 +2195,65 @@ jQuery.each({ // submit delegation if ( !jQuery.support.submitBubbles ) { -jQuery.event.special.submit = { - setup: function( data, namespaces, fn ) { - if ( this.nodeName.toLowerCase() !== "form" ) { - jQuery.event.add(this, "click.specialSubmit." + fn.guid, function( e ) { - var elem = e.target, type = elem.type; + jQuery.event.special.submit = { + setup: function( data, namespaces ) { + if ( this.nodeName.toLowerCase() !== "form" ) { + jQuery.event.add(this, "click.specialSubmit", function( e ) { + var elem = e.target, type = elem.type; - if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { - return trigger( "submit", this, arguments ); - } - }); + if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { + return trigger( "submit", this, arguments ); + } + }); - jQuery.event.add(this, "keypress.specialSubmit." + fn.guid, function( e ) { - var elem = e.target, type = elem.type; + jQuery.event.add(this, "keypress.specialSubmit", function( e ) { + var elem = e.target, type = elem.type; - if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { - return trigger( "submit", this, arguments ); - } - }); + if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { + return trigger( "submit", this, arguments ); + } + }); - } else { - return false; + } else { + return false; + } + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialSubmit" ); } - }, - - remove: function( namespaces, fn ) { - jQuery.event.remove( this, "click.specialSubmit" + (fn ? "."+fn.guid : "") ); - jQuery.event.remove( this, "keypress.specialSubmit" + (fn ? "."+fn.guid : "") ); - } -}; + }; } // change delegation, happens here so we have bind. if ( !jQuery.support.changeBubbles ) { -var formElems = /textarea|input|select/i; + var formElems = /textarea|input|select/i, -function getVal( elem ) { - var type = elem.type, val = elem.value; + changeFilters, - if ( type === "radio" || type === "checkbox" ) { - val = elem.checked; + getVal = function( elem ) { + var type = elem.type, val = elem.value; - } else if ( type === "select-multiple" ) { - val = elem.selectedIndex > -1 ? - jQuery.map( elem.options, function( elem ) { - return elem.selected; - }).join("-") : - ""; + if ( type === "radio" || type === "checkbox" ) { + val = elem.checked; - } else if ( elem.nodeName.toLowerCase() === "select" ) { - val = elem.selectedIndex; - } + } else if ( type === "select-multiple" ) { + val = elem.selectedIndex > -1 ? + jQuery.map( elem.options, function( elem ) { + return elem.selected; + }).join("-") : + ""; - return val; -} + } else if ( elem.nodeName.toLowerCase() === "select" ) { + val = elem.selectedIndex; + } -function testChange( e ) { + return val; + }, + + testChange = function testChange( e ) { var elem = e.target, data, val; if ( !formElems.test( elem.nodeName ) || elem.readOnly ) { @@ -2223,61 +2276,61 @@ function testChange( e ) { e.type = "change"; return jQuery.event.trigger( e, arguments[1], elem ); } -} + }; -jQuery.event.special.change = { - filters: { - focusout: testChange, + jQuery.event.special.change = { + filters: { + focusout: testChange, - click: function( e ) { - var elem = e.target, type = elem.type; + click: function( e ) { + var elem = e.target, type = elem.type; - if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { - return testChange.call( this, e ); - } - }, + if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { + return testChange.call( this, e ); + } + }, - // Change has to be called before submit - // Keydown will be called before keypress, which is used in submit-event delegation - keydown: function( e ) { - var elem = e.target, type = elem.type; + // Change has to be called before submit + // Keydown will be called before keypress, which is used in submit-event delegation + keydown: function( e ) { + var elem = e.target, type = elem.type; - if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || - (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || - type === "select-multiple" ) { - return testChange.call( this, e ); - } - }, + if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || + (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || + type === "select-multiple" ) { + return testChange.call( this, e ); + } + }, - // Beforeactivate happens also before the previous element is blurred - // with this event you can't trigger a change event, but you can store - // information/focus[in] is not needed anymore - beforeactivate: function( e ) { - var elem = e.target; - - if ( elem.nodeName.toLowerCase() === "input" && elem.type === "radio" ) { + // Beforeactivate happens also before the previous element is blurred + // with this event you can't trigger a change event, but you can store + // information/focus[in] is not needed anymore + beforeactivate: function( e ) { + var elem = e.target; jQuery.data( elem, "_change_data", getVal(elem) ); } + }, + + setup: function( data, namespaces ) { + if ( this.type === "file" ) { + return false; + } + + for ( var type in changeFilters ) { + jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + } + + return formElems.test( this.nodeName ); + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialChange" ); + + return formElems.test( this.nodeName ); } - }, - setup: function( data, namespaces, fn ) { - for ( var type in changeFilters ) { - jQuery.event.add( this, type + ".specialChange." + fn.guid, changeFilters[type] ); - } - - return formElems.test( this.nodeName ); - }, - remove: function( namespaces, fn ) { - for ( var type in changeFilters ) { - jQuery.event.remove( this, type + ".specialChange" + (fn ? "."+fn.guid : ""), changeFilters[type] ); - } - - return formElems.test( this.nodeName ); - } -}; - -var changeFilters = jQuery.event.special.change.filters; + }; + changeFilters = jQuery.event.special.change.filters; } function trigger( type, elem, args ) { @@ -2325,11 +2378,16 @@ jQuery.each(["bind", "one"], function( i, name ) { return fn.apply( this, arguments ); }) : fn; - return type === "unload" && name !== "one" ? - this.one( type, data, fn ) : - this.each(function() { - jQuery.event.add( this, type, handler, data ); - }); + if ( type === "unload" && name !== "one" ) { + this.one( type, data, fn ); + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.add( this[i], type, handler, data ); + } + } + + return this; }; }); @@ -2340,13 +2398,29 @@ jQuery.fn.extend({ for ( var key in type ) { this.unbind(key, type[key]); } - return this; + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.remove( this[i], type, fn ); + } } - return this.each(function() { - jQuery.event.remove( this, type, fn ); - }); + return this; }, + + delegate: function( selector, types, data, fn ) { + return this.live( types, data, fn, selector ); + }, + + undelegate: function( selector, types, fn ) { + if ( arguments.length === 0 ) { + return this.unbind( "live" ); + + } else { + return this.die( types, null, fn, selector ); + } + }, + trigger: function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); @@ -2390,32 +2464,60 @@ jQuery.fn.extend({ } }); +var liveMap = { + focus: "focusin", + blur: "focusout", + mouseenter: "mouseover", + mouseleave: "mouseout" +}; + jQuery.each(["live", "die"], function( i, name ) { - jQuery.fn[ name ] = function( types, data, fn ) { - var type, i = 0; + jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { + var type, i = 0, match, namespaces, preType, + selector = origSelector || this.selector, + context = origSelector ? this : jQuery( this.context ); if ( jQuery.isFunction( data ) ) { fn = data; data = undefined; } - types = (types || "").split( /\s+/ ); + types = (types || "").split(" "); while ( (type = types[ i++ ]) != null ) { - type = type === "focus" ? "focusin" : // focus --> focusin - type === "blur" ? "focusout" : // blur --> focusout - type === "hover" ? types.push("mouseleave") && "mouseenter" : // hover support - type; - + match = rnamespaces.exec( type ); + namespaces = ""; + + if ( match ) { + namespaces = match[0]; + type = type.replace( rnamespaces, "" ); + } + + if ( type === "hover" ) { + types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); + continue; + } + + preType = type; + + if ( type === "focus" || type === "blur" ) { + types.push( liveMap[ type ] + namespaces ); + type = type + namespaces; + + } else { + type = (liveMap[ type ] || type) + namespaces; + } + if ( name === "live" ) { // bind live handler - jQuery( this.context ).bind( liveConvert( type, this.selector ), { - data: data, selector: this.selector, live: type - }, fn ); + context.each(function(){ + jQuery.event.add( this, liveConvert( type, selector ), + { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); + }); } else { // unbind live handler - jQuery( this.context ).unbind( liveConvert( type, this.selector ), fn ? { guid: fn.guid + this.selector + type } : null ); + context.unbind( liveConvert( type, selector ), fn ); } } @@ -2425,45 +2527,46 @@ jQuery.each(["live", "die"], function( i, name ) { function liveHandler( event ) { var stop, elems = [], selectors = [], args = arguments, - related, match, fn, elem, j, i, l, data, - live = jQuery.extend({}, jQuery.data( this, "events" ).live); + related, match, handleObj, elem, j, i, l, data, + events = jQuery.data( this, "events" ); // Make sure we avoid non-left-click bubbling in Firefox (#3861) - if ( event.button && event.type === "click" ) { + if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) { return; } - for ( j in live ) { - fn = live[j]; - if ( fn.live === event.type || - fn.altLive && jQuery.inArray(event.type, fn.altLive) > -1 ) { + event.liveFired = this; + + var live = events.live.slice(0); + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { + selectors.push( handleObj.selector ); - data = fn.data; - if ( !(data.beforeFilter && data.beforeFilter[event.type] && - !data.beforeFilter[event.type](event)) ) { - selectors.push( fn.selector ); - } } else { - delete live[j]; + live.splice( j--, 1 ); } } match = jQuery( event.target ).closest( selectors, event.currentTarget ); for ( i = 0, l = match.length; i < l; i++ ) { - for ( j in live ) { - fn = live[j]; - elem = match[i].elem; - related = null; + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( match[i].selector === handleObj.selector ) { + elem = match[i].elem; + related = null; - if ( match[i].selector === fn.selector ) { // Those two events require additional checking - if ( fn.live === "mouseenter" || fn.live === "mouseleave" ) { - related = jQuery( event.relatedTarget ).closest( fn.selector )[0]; + if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { + related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; } if ( !related || related !== elem ) { - elems.push({ elem: elem, fn: fn }); + elems.push({ elem: elem, handleObj: handleObj }); } } } @@ -2472,8 +2575,10 @@ function liveHandler( event ) { for ( i = 0, l = elems.length; i < l; i++ ) { match = elems[i]; event.currentTarget = match.elem; - event.data = match.fn.data; - if ( match.fn.apply( match.elem, args ) === false ) { + event.data = match.handleObj.data; + event.handleObj = match.handleObj; + + if ( match.handleObj.origHandler.apply( match.elem, args ) === false ) { stop = false; break; } @@ -2483,7 +2588,7 @@ function liveHandler( event ) { } function liveConvert( type, selector ) { - return "live." + (type ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&"); + return "live." + (type && type !== "*" ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&"); } jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + @@ -3228,8 +3333,10 @@ var makeArray = function(array, results) { // Perform a simple check to determine if the browser is capable of // converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 ); + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; // Provide a fallback method if it does not work } catch(e){ @@ -3533,7 +3640,7 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { } var contains = document.compareDocumentPosition ? function(a, b){ - return a.compareDocumentPosition(b) & 16; + return !!(a.compareDocumentPosition(b) & 16); } : function(a, b){ return a !== b && (a.contains ? a.contains(b) : true); }; @@ -3570,7 +3677,7 @@ jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.filters; jQuery.unique = Sizzle.uniqueSort; -jQuery.getText = getText; +jQuery.text = getText; jQuery.isXMLDoc = isXML; jQuery.contains = contains; @@ -3856,7 +3963,8 @@ var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i, rtagName = /<([\w:]+)/, rtbody = / self-closes a tag + .replace(/=([^="'>\s]+\/)>/g, '="$1">') .replace(rleadingWhitespace, "")], ownerDocument)[0]; } else { return this.cloneNode(true); @@ -4044,7 +4188,7 @@ jQuery.fn.extend({ null; // See if we can take a shortcut and just use innerHTML - } else if ( typeof value === "string" && !/