Merge remote branch 'origin/1.0.x' into 1.0.x

This commit is contained in:
Evan Prodromou 2011-03-16 09:52:35 -04:00
commit 3598134330
50 changed files with 4214 additions and 412 deletions

View File

@ -1131,3 +1131,11 @@ StartActivityObjectOutputJson: Called at start of JSON output generation for Act
EndActivityObjectOutputJson: Called at end of JSON output generation for ActivityObject chunks: the array has not yet been filled out.
- $obj ActivityObject
- &$out: array to be serialized; you're free to modify it
StartNoticeWhoGets: Called at start of inbox delivery prep; plugins can schedule notices to go to particular profiles that would otherwise not have reached them. Canceling will take over the entire addressing operation. Be aware that output can be cached or used several times, so should remain idempotent.
- $notice Notice
- &$ni: in/out array mapping profile IDs to constants: NOTICE_INBOX_SOURCE_SUB etc
EndNoticeWhoGets: Called at end of inbox delivery prep; plugins can filter out profiles from receiving inbox delivery here. Be aware that output can be cached or used several times, so should remain idempotent.
- $notice Notice
- &$ni: in/out array mapping profile IDs to constants: NOTICE_INBOX_SOURCE_SUB etc

View File

@ -100,7 +100,7 @@ class ApiGroupListAction extends ApiBareAuthAction
);
$subtitle = sprintf(
// TRANS: Used as subtitle in check for group membership. %1$s is a user name, %2$s is the site name.
// TRANS: Used as subtitle in check for group membership. %1$s is the site name, %2$s is a user name.
_('%1$s groups %2$s is a member of.'),
$sitename,
$this->user->nickname

View File

@ -322,8 +322,11 @@ class ApiTimelineUserAction extends ApiBareAuthAction
$this->clientError(_('Atom post must not be empty.'));
}
$dom = DOMDocument::loadXML($xml);
if (!$dom) {
$old = error_reporting(error_reporting() & ~(E_WARNING | E_NOTICE));
$dom = new DOMDocument();
$ok = $dom->loadXML($xml);
error_reporting($old);
if (!$ok) {
// TRANS: Client error displayed attempting to post an API that is not well-formed XML.
$this->clientError(_('Atom post must be well-formed XML.'));
}

View File

@ -138,11 +138,14 @@ class NoticesearchAction extends SearchAction
$this->elementEnd('div');
return;
}
$terms = preg_split('/[\s,]+/', $q);
$nl = new SearchNoticeList($notice, $this, $terms);
$cnt = $nl->show();
$this->pagination($page > 1, $cnt > NOTICES_PER_PAGE,
$page, 'noticesearch', array('q' => $q));
if (Event::handle('StartNoticeSearchShowResults', array($this, $q, $notice))) {
$terms = preg_split('/[\s,]+/', $q);
$nl = new SearchNoticeList($notice, $this, $terms);
$cnt = $nl->show();
$this->pagination($page > 1, $cnt > NOTICES_PER_PAGE,
$page, 'noticesearch', array('q' => $q));
Event::handle('EndNoticeSearchShowResults', array($this, $q, $notice));
}
}
function showScripts()

View File

@ -78,6 +78,9 @@ class ShownoticeAction extends OwnerDesignAction
function prepare($args)
{
parent::prepare($args);
if ($this->boolean('ajax')) {
StatusNet::setApi(true);
}
$id = $this->arg('notice');
@ -188,22 +191,26 @@ class ShownoticeAction extends OwnerDesignAction
{
parent::handle($args);
if ($this->notice->is_local == Notice::REMOTE_OMB) {
if (!empty($this->notice->url)) {
$target = $this->notice->url;
} else if (!empty($this->notice->uri) && preg_match('/^https?:/', $this->notice->uri)) {
// Old OMB posts saved the remote URL only into the URI field.
$target = $this->notice->uri;
} else {
// Shouldn't happen.
$target = false;
}
if ($target && $target != $this->selfUrl()) {
common_redirect($target, 301);
return false;
if ($this->boolean('ajax')) {
$this->showAjax();
} else {
if ($this->notice->is_local == Notice::REMOTE_OMB) {
if (!empty($this->notice->url)) {
$target = $this->notice->url;
} else if (!empty($this->notice->uri) && preg_match('/^https?:/', $this->notice->uri)) {
// Old OMB posts saved the remote URL only into the URI field.
$target = $this->notice->uri;
} else {
// Shouldn't happen.
$target = false;
}
if ($target && $target != $this->selfUrl()) {
common_redirect($target, 301);
return false;
}
}
$this->showPage();
}
$this->showPage();
}
/**
@ -232,6 +239,21 @@ class ShownoticeAction extends OwnerDesignAction
$this->elementEnd('ol');
}
function showAjax()
{
header('Content-Type: text/xml;charset=utf-8');
$this->xw->startDocument('1.0', 'UTF-8');
$this->elementStart('html');
$this->elementStart('head');
$this->element('title', null, _('Notice'));
$this->elementEnd('head');
$this->elementStart('body');
$nli = new NoticeListItem($this->notice, $this);
$nli->show();
$this->elementEnd('body');
$this->elementEnd('html');
}
/**
* Don't show page notice
*

View File

@ -812,41 +812,48 @@ class Notice extends Memcached_DataObject
$ni = array();
foreach ($users as $id) {
$ni[$id] = NOTICE_INBOX_SOURCE_SUB;
}
// Give plugins a chance to add folks in at start...
if (Event::handle('StartNoticeWhoGets', array($this, &$ni))) {
foreach ($groups as $group) {
$users = $group->getUserMembers();
foreach ($users as $id) {
if (!array_key_exists($id, $ni)) {
$ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
$ni[$id] = NOTICE_INBOX_SOURCE_SUB;
}
foreach ($groups as $group) {
$users = $group->getUserMembers();
foreach ($users as $id) {
if (!array_key_exists($id, $ni)) {
$ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
}
}
}
}
foreach ($recipients as $recipient) {
if (!array_key_exists($recipient, $ni)) {
$ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
foreach ($recipients as $recipient) {
if (!array_key_exists($recipient, $ni)) {
$ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
}
}
}
// Exclude any deleted, non-local, or blocking recipients.
$profile = $this->getProfile();
$originalProfile = null;
if ($this->repeat_of) {
// Check blocks against the original notice's poster as well.
$original = Notice::staticGet('id', $this->repeat_of);
if ($original) {
$originalProfile = $original->getProfile();
// Exclude any deleted, non-local, or blocking recipients.
$profile = $this->getProfile();
$originalProfile = null;
if ($this->repeat_of) {
// Check blocks against the original notice's poster as well.
$original = Notice::staticGet('id', $this->repeat_of);
if ($original) {
$originalProfile = $original->getProfile();
}
}
}
foreach ($ni as $id => $source) {
$user = User::staticGet('id', $id);
if (empty($user) || $user->hasBlocked($profile) ||
($originalProfile && $user->hasBlocked($originalProfile))) {
unset($ni[$id]);
foreach ($ni as $id => $source) {
$user = User::staticGet('id', $id);
if (empty($user) || $user->hasBlocked($profile) ||
($originalProfile && $user->hasBlocked($originalProfile))) {
unset($ni[$id]);
}
}
// Give plugins a chance to filter out...
Event::handle('EndNoticeWhoGets', array($this, &$ni));
}
if (!empty($c)) {
@ -1999,6 +2006,11 @@ class Notice extends Memcached_DataObject
$this->is_local == Notice::LOCAL_NONPUBLIC);
}
/**
* Get the list of hash tags saved with this notice.
*
* @return array of strings
*/
public function getTags()
{
$tags = array();

View File

@ -681,6 +681,9 @@ class Action extends HTMLOutputter // lawsuit
function showCore()
{
$this->elementStart('div', array('id' => 'core'));
$this->elementStart('div', array('id' => 'aside_primary_wrapper'));
$this->elementStart('div', array('id' => 'content_wrapper'));
$this->elementStart('div', array('id' => 'site_nav_local_views_wrapper'));
if (Event::handle('StartShowLocalNavBlock', array($this))) {
$this->showLocalNavBlock();
Event::handle('EndShowLocalNavBlock', array($this));
@ -694,6 +697,9 @@ class Action extends HTMLOutputter // lawsuit
Event::handle('EndShowAside', array($this));
}
$this->elementEnd('div');
$this->elementEnd('div');
$this->elementEnd('div');
$this->elementEnd('div');
}
/**

View File

@ -93,8 +93,14 @@ class InfoAction extends Action
function showCore()
{
$this->elementStart('div', array('id' => 'core'));
$this->elementStart('div', array('id' => 'aside_primary_wrapper'));
$this->elementStart('div', array('id' => 'content_wrapper'));
$this->elementStart('div', array('id' => 'site_nav_local_views_wrapper'));
$this->showContentBlock();
$this->elementEnd('div');
$this->elementEnd('div');
$this->elementEnd('div');
$this->elementEnd('div');
}
function showHeader()

View File

@ -54,6 +54,7 @@ class ExtendedProfilePlugin extends Plugin
function onAutoload($cls)
{
$lower = strtolower($cls);
switch ($lower)
{
case 'extendedprofile':
@ -62,6 +63,9 @@ class ExtendedProfilePlugin extends Plugin
case 'profiledetailsettingsaction':
require_once dirname(__FILE__) . '/' . $lower . '.php';
return false;
case 'userautocompleteaction':
require_once dirname(__FILE__) . '/action/' . mb_substr($lower, 0, -6) . '.php';
return false;
case 'profile_detail':
require_once dirname(__FILE__) . '/' . ucfirst($lower) . '.php';
return false;
@ -81,11 +85,19 @@ class ExtendedProfilePlugin extends Plugin
*/
function onStartInitializeRouter($m)
{
$m->connect(':nickname/detail',
array('action' => 'profiledetail'),
array('nickname' => Nickname::DISPLAY_FMT));
$m->connect('settings/profile/detail',
array('action' => 'profiledetailsettings'));
$m->connect(
':nickname/detail',
array('action' => 'profiledetail'),
array('nickname' => Nickname::DISPLAY_FMT)
);
$m->connect(
'/settings/profile/finduser',
array('action' => 'Userautocomplete')
);
$m->connect(
'settings/profile/detail',
array('action' => 'profiledetailsettings')
);
return true;
}
@ -95,8 +107,6 @@ class ExtendedProfilePlugin extends Plugin
$schema = Schema::get();
$schema->ensureTable('profile_detail', Profile_detail::schemaDef());
// @hack until key definition support is merged
Profile_detail::fixIndexes($schema);
return true;
}

View File

@ -21,130 +21,122 @@ if (!defined('STATUSNET')) {
exit(1);
}
class Profile_detail extends Memcached_DataObject
/**
* DataObject class to store extended profile fields. Allows for storing
* multiple values per a "field_name" (field_name property is not unique).
*
* Example:
*
* Jed's Phone Numbers
* home : 510-384-1992
* mobile: 510-719-1139
* work : 415-231-1121
*
* We can store these phone numbers in a "field" represented by three
* Profile_detail objects, each named 'phone_number' like this:
*
* $phone1 = new Profile_detail();
* $phone1->field_name = 'phone_number';
* $phone1->rel = 'home';
* $phone1->field_value = '510-384-1992';
* $phone1->value_index = 1;
*
* $phone1 = new Profile_detail();
* $phone1->field_name = 'phone_number';
* $phone1->rel = 'mobile';
* $phone1->field_value = '510-719-1139';
* $phone1->value_index = 2;
*
* $phone1 = new Profile_detail();
* $phone1->field_name = 'phone_number';
* $phone1->rel = 'work';
* $phone1->field_value = '415-231-1121';
* $phone1->value_index = 3;
*
*/
class Profile_detail extends Managed_DataObject
{
public $__table = 'submirror';
public $__table = 'profile_detail';
public $id;
public $profile_id;
public $field;
public $field_index; // relative ordering of multiple values in the same field
public $value; // primary text value
public $rel; // detail for some field types; eg "home", "mobile", "work" for phones or "aim", "irc", "xmpp" for IM
public $profile_id; // profile this is for
public $rel; // detail for some field types; eg "home", "mobile", "work" for phones or "aim", "irc", "xmpp" for IM
public $field_name; // name
public $field_value; // primary text value
public $value_index; // relative ordering of multiple values in the same field
public $date; // related date
public $ref_profile; // for people types, allows pointing to a known profile in the system
public $created;
public $modified;
public /*static*/ function staticGet($k, $v=null)
/**
* Get an instance by key
*
* This is a utility method to get a single instance with a given key value.
*
* @param string $k Key to use to lookup
* @param mixed $v Value to lookup
*
* @return User_greeting_count object found, or null for no hits
*
*/
function staticGet($k, $v=null)
{
return parent::staticGet(__CLASS__, $k, $v);
return Memcached_DataObject::staticGet('Profile_detail', $k, $v);
}
/**
* return table definition for DB_DataObject
* Get an instance by compound key
*
* DB_DataObject needs to know something about the table to manipulate
* instances. This method provides all the DB_DataObject needs to know.
* This is a utility method to get a single instance with a given set of
* key-value pairs. Usually used for the primary key for a compound key; thus
* the name.
*
* @param array $kv array of key-value mappings
*
* @return Bookmark object found, or null for no hits
*
* @return array array of column definitions
*/
function table()
function pkeyGet($kv)
{
return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'field' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
'field_index' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'value' => DB_DATAOBJECT_STR,
'rel' => DB_DATAOBJECT_STR,
'ref_profile' => DB_DATAOBJECT_ID,
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
return Memcached_DataObject::pkeyGet('Profile_detail', $kv);
}
static function schemaDef()
{
// @fixme need a reverse key on (subscribed, subscriber) as well
return array(new ColumnDef('id', 'integer',
null, false, 'PRI'),
// @fixme need a unique index on these three
new ColumnDef('profile_id', 'integer',
null, false),
new ColumnDef('field', 'varchar',
16, false),
new ColumnDef('field_index', 'integer',
null, false),
new ColumnDef('value', 'text',
null, true),
new ColumnDef('rel', 'varchar',
16, true),
new ColumnDef('ref_profile', 'integer',
null, true),
new ColumnDef('created', 'datetime',
null, false),
new ColumnDef('modified', 'datetime',
null, false));
}
/**
* Temporary hack to set up the compound index, since we can't do
* it yet through regular Schema interface. (Coming for 1.0...)
*
* @param Schema $schema
* @return void
*/
static function fixIndexes($schema)
{
try {
// @fixme this won't be a unique index... SIGH
$schema->createIndex('profile_detail', array('profile_id', 'field', 'field_index'));
} catch (Exception $e) {
common_log(LOG_ERR, __METHOD__ . ': ' . $e->getMessage());
}
}
/**
* return key definitions for DB_DataObject
*
* DB_DataObject needs to know about keys that the table has; this function
* defines them.
*
* @return array key definitions
*/
function keys()
{
return array_keys($this->keyTypes());
}
/**
* return key definitions for Memcached_DataObject
*
* Our caching system uses the same key definitions, but uses a different
* method to get them.
*
* @return array key definitions
*/
function keyTypes()
{
// @fixme keys
// need a sane key for reverse lookup too
return array('id' => 'K');
}
function sequenceKey()
{
return array('id', true);
return array(
'description'
=> 'Additional profile details for the ExtendedProfile plugin',
'fields' => array(
'id' => array('type' => 'serial', 'not null' => true),
'profile_id' => array('type' => 'int', 'not null' => true),
'field_name' => array(
'type' => 'varchar',
'length' => 16,
'not null' => true
),
'value_index' => array('type' => 'int'),
'field_value' => array('type' => 'text'),
'date' => array('type' => 'datetime'),
'rel' => array('type' => 'varchar', 'length' => 16),
'rel_profile' => array('type' => 'int'),
'created' => array(
'type' => 'datetime',
'not null' => true
),
'modified' => array(
'type' => 'timestamp',
'not null' => true
),
),
'primary key' => array('id'),
'unique keys' => array(
'profile_detail_profile_id_field_name_value_index'
=> array('profile_id', 'field_name', 'value_index'),
)
);
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Action for showing Twitter-like JSON search results
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Search
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @copyright 2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
class UserautocompleteAction extends Action
{
var $query;
/**
* Initialization.
*
* @param array $args Web and URL arguments
*
* @return boolean true if nothing goes wrong
*/
function prepare($args)
{
parent::prepare($args);
$this->query = $this->trimmed('term');
return true;
}
/**
* Handle a request
*
* @param array $args Arguments from $_REQUEST
*
* @return void
*/
function handle($args)
{
parent::handle($args);
$this->showResults();
}
/**
* Search for users matching the query and spit the results out
* as a quick-n-dirty JSON document
*
* @return void
*/
function showResults()
{
$people = array();
$profile = new Profile();
$search_engine = $profile->getSearchEngine('profile');
$search_engine->set_sort_mode('nickname_desc');
$search_engine->limit(0, 10);
$search_engine->query(strtolower($this->query . '*'));
$cnt = $profile->find();
if ($cnt > 0) {
$sql = 'SELECT profile.* FROM profile, user WHERE profile.id = user.id '
. ' AND LEFT(LOWER(profile.nickname), '
. strlen($this->query)
. ') = \'%s\' '
. ' LIMIT 0, 10';
$profile->query(sprintf($sql, $this->query));
}
while ($profile->fetch()) {
$people[] = $profile->nickname;
}
header('Content-Type: application/json; charset=utf-8');
print json_encode($people);
}
/**
* Do we need to write to the database?
*
* @return boolean true
*/
function isReadOnly($args)
{
return true;
}
}

View File

@ -0,0 +1,164 @@
/* Note the #content is only needed to override weird crap in default styles */
#profiledetail .entity_actions {
margin-top: 0px;
margin-bottom: 0px;
}
#profiledetail #content h3 {
margin-bottom: 5px;
}
#content table.extended-profile {
width: 100%;
border-collapse: separate;
border-spacing: 0px 8px;
margin-bottom: 10px;
}
#content table.extended-profile th {
color: #777;
background-color: #ECECF2;
width: 150px;
text-align: right;
padding: 2px 8px 2px 0px;
}
#content table.extended-profile th.employer, #content table.extended-profile th.institution {
display: none;
}
#content table.extended-profile td {
padding: 2px 0px 2px 8px;
}
.experience-item, .education-item {
float: left;
padding-bottom: 4px;
}
.experience-item .label, .education-item .label {
float: left;
clear: left;
position: relative;
left: -8px;
margin-right: 2px;
margin-bottom: 8px;
color: #777;
background-color: #ECECF2;
width: 150px;
text-align: right;
padding: 2px 8px 2px 0px;
}
.experience-item .field, .education-item .field {
float: left;
padding-top: 2px;
padding-bottom: 2px;
}
#profiledetailsettings #content table.extended-profile td {
padding: 0px 0px 0px 8px;
}
#profiledetailsettings input {
margin-right: 8px;
}
.form_settings .extended-profile label {
display: none;
}
.extended-profile textarea {
width: 280px;
}
.extended-profile input[type=text] {
width: 280px;
}
.extended-profile .phone-item input[type=text], .extended-profile .im-item input[type=text], .extended-profile .website-item input[type=text] {
width: 175px;
}
.extended-profile input.hasDatepicker {
width: 100px;
}
.experience-item input[type=text], .education-item input[type=text] {
float: left;
}
.extended-profile .current-checkbox {
float: left;
position: relative;
top: 2px;
}
.form_settings .extended-profile input.checkbox {
margin-left: 0px;
left: 0px;
top: 2px;
}
.form_settings .extended-profile label.checkbox {
max-width: 100%;
float: none;
display: inline;
left: -20px;
}
.extended-profile select {
padding-right: 2px;
font-size: 0.88em;
}
.extended-profile a.add_row, .extended-profile a.remove_row {
display: block;
height: 16px;
width: 16px;
overflow: hidden;
background-image: url('../../../theme/rebase/images/icons/icons-01.gif');
background-repeat: no-repeat;
}
.extended-profile a.remove_row {
background-position: 0px -1252px;
float: right;
position: relative;
top: 6px;
line-height: 4em;
}
.extended-profile a.add_row {
clear: both;
position: relative;
top: 6px;
left: 2px;
background-position: 0px -1186px;
width: 120px;
padding-left: 20px;
line-height: 1.2em;
}
#content table.extended-profile .supersizeme th {
border-bottom: 28px solid #fff;
}
#profiledetailsettings .experience-item, #profiledetailsettings .education-item {
margin-bottom: 10px;
width: 100%;
}
#profiledetailsettings .education-item textarea {
float: left;
margin-bottom: 8px;
}
#profiledetailsettings tr:last-child .experience-item, #profiledetailsettings tr:last-child .education-item {
margin-bottom: 0px;
}
#profiledetailsettings .experience-item a.add_row, #profiledetailsettings .education-item a.add_row {
left: 160px;
}

View File

@ -21,27 +21,256 @@ if (!defined('STATUSNET')) {
exit(1);
}
/**
* Class to represent extended profile data
*/
class ExtendedProfile
{
protected $fields;
/**
* Constructor
*
* @param Profile $profile
*/
function __construct(Profile $profile)
{
$this->profile = $profile;
$this->profile = $profile;
$this->user = $profile->getUser();
$this->fields = $this->loadFields();
$this->sections = $this->getSections();
$this->fields = $this->loadFields();
//common_debug(var_export($this->sections, true));
//common_debug(var_export($this->fields, true));
}
/**
* Load extended profile fields
*
* @return array $fields the list of fields
*/
function loadFields()
{
$detail = new Profile_detail();
$detail->profile_id = $this->profile->id;
$detail->find();
while ($detail->get()) {
$fields[$detail->field][] = clone($detail);
$fields = array();
while ($detail->fetch()) {
$fields[$detail->field_name][] = clone($detail);
}
return $fields;
}
/**
* Get a the self-tags associated with this profile
*
* @return string the concatenated string of tags
*/
function getTags()
{
return implode(' ', $this->user->getSelfTags());
}
/**
* Return a simple string value. Checks for fields that should
* be stored in the regular profile and returns values from it
* if appropriate.
*
* @param string $name name of the detail field to get the
* value from
*
* @return string the value
*/
function getTextValue($name)
{
$key = strtolower($name);
$profileFields = array('fullname', 'location', 'bio');
if (in_array($key, $profileFields)) {
return $this->profile->$name;
} else if (array_key_exists($key, $this->fields)) {
return $this->fields[$key][0]->field_value;
} else {
return null;
}
}
function getDateValue($name) {
$key = strtolower($name);
if (array_key_exists($key, $this->fields)) {
return $this->fields[$key][0]->date;
} else {
return null;
}
}
// XXX: getPhones, getIms, and getWebsites pretty much do the same thing,
// so refactor.
function getPhones()
{
$phones = (isset($this->fields['phone'])) ? $this->fields['phone'] : null;
$pArrays = array();
if (empty($phones)) {
$pArrays[] = array(
'label' => _m('Phone'),
'index' => 0,
'type' => 'phone',
'vcard' => 'tel',
'rel' => 'office',
'value' => null
);
} else {
for ($i = 0; $i < sizeof($phones); $i++) {
$pa = array(
'label' => _m('Phone'),
'type' => 'phone',
'index' => intval($phones[$i]->value_index),
'rel' => $phones[$i]->rel,
'value' => $phones[$i]->field_value,
'vcard' => 'tel'
);
$pArrays[] = $pa;
}
}
return $pArrays;
}
function getIms()
{
$ims = (isset($this->fields['im'])) ? $this->fields['im'] : null;
$iArrays = array();
if (empty($ims)) {
$iArrays[] = array(
'label' => _m('IM'),
'type' => 'im'
);
} else {
for ($i = 0; $i < sizeof($ims); $i++) {
$ia = array(
'label' => _m('IM'),
'type' => 'im',
'index' => intval($ims[$i]->value_index),
'rel' => $ims[$i]->rel,
'value' => $ims[$i]->field_value,
);
$iArrays[] = $ia;
}
}
return $iArrays;
}
function getWebsites()
{
$sites = (isset($this->fields['website'])) ? $this->fields['website'] : null;
$wArrays = array();
if (empty($sites)) {
$wArrays[] = array(
'label' => _m('Website'),
'type' => 'website'
);
} else {
for ($i = 0; $i < sizeof($sites); $i++) {
$wa = array(
'label' => _m('Website'),
'type' => 'website',
'index' => intval($sites[$i]->value_index),
'rel' => $sites[$i]->rel,
'value' => $sites[$i]->field_value,
);
$wArrays[] = $wa;
}
}
return $wArrays;
}
function getExperiences()
{
$companies = (isset($this->fields['company'])) ? $this->fields['company'] : null;
$start = (isset($this->fields['start'])) ? $this->fields['start'] : null;
$end = (isset($this->fields['end'])) ? $this->fields['end'] : null;
$eArrays = array();
if (empty($companies)) {
$eArrays[] = array(
'label' => _m('Employer'),
'type' => 'experience',
'company' => null,
'start' => null,
'end' => null,
'current' => false,
'index' => 0
);
} else {
for ($i = 0; $i < sizeof($companies); $i++) {
$ea = array(
'label' => _m('Employer'),
'type' => 'experience',
'company' => $companies[$i]->field_value,
'index' => intval($companies[$i]->value_index),
'current' => $end[$i]->rel,
'start' => $start[$i]->date,
'end' => $end[$i]->date
);
$eArrays[] = $ea;
}
}
return $eArrays;
}
function getEducation()
{
$schools = (isset($this->fields['school'])) ? $this->fields['school'] : null;
$degrees = (isset($this->fields['degree'])) ? $this->fields['degree'] : null;
$descs = (isset($this->fields['degree_descr'])) ? $this->fields['degree_descr'] : null;
$start = (isset($this->fields['school_start'])) ? $this->fields['school_start'] : null;
$end = (isset($this->fields['school_end'])) ? $this->fields['school_end'] : null;
$iArrays = array();
if (empty($schools)) {
$iArrays[] = array(
'type' => 'education',
'label' => _m('Institution'),
'school' => null,
'degree' => null,
'description' => null,
'start' => null,
'end' => null,
'index' => 0
);
} else {
for ($i = 0; $i < sizeof($schools); $i++) {
$ia = array(
'type' => 'education',
'label' => _m('Institution'),
'school' => $schools[$i]->field_value,
'degree' => isset($degrees[$i]->field_value) ? $degrees[$i]->field_value : null,
'description' => isset($descs[$i]->field_value) ? $descs[$i]->field_value : null,
'index' => intval($schools[$i]->value_index),
'start' => $start[$i]->date,
'end' => $end[$i]->date
);
$iArrays[] = $ia;
}
}
return $iArrays;
}
/**
* Return all the sections of the extended profile
*
* @return array the big list of sections and fields
*/
function getSections()
{
return array(
@ -81,22 +310,9 @@ class ExtendedProfile
'contact' => array(
'label' => _m('Contact'),
'fields' => array(
'phone' => array(
'label' => _m('Phone'),
'type' => 'phone',
'multi' => true,
'vcard' => 'tel',
),
'im' => array(
'label' => _m('IM'),
'type' => 'im',
'multi' => true,
),
'website' => array(
'label' => _m('Websites'),
'type' => 'website',
'multi' => true,
),
'phone' => $this->getPhones(),
'im' => $this->getIms(),
'website' => $this->getWebsites()
),
),
'personal' => array(
@ -119,19 +335,13 @@ class ExtendedProfile
'experience' => array(
'label' => _m('Work experience'),
'fields' => array(
'experience' => array(
'type' => 'experience',
'label' => _m('Employer'),
),
'experience' => $this->getExperiences()
),
),
'education' => array(
'label' => _m('Education'),
'fields' => array(
'education' => array(
'type' => 'education',
'label' => _m('Institution'),
),
'education' => $this->getEducation()
),
),
);

View File

@ -21,13 +21,35 @@ if (!defined('STATUSNET')) {
exit(1);
}
class ExtendedProfileWidget extends Widget
/**
* Class for outputting a widget to display or edit
* extended profiles
*/
class ExtendedProfileWidget extends Form
{
const EDITABLE=true;
const EDITABLE = true;
/**
* The parent profile
*
* @var Profile
*/
protected $profile;
/**
* The extended profile
*
* @var Extended_profile
*/
protected $ext;
/**
* Constructor
*
* @param XMLOutputter $out
* @param Profile $profile
* @param boolean $editable
*/
public function __construct(XMLOutputter $out=null, Profile $profile=null, $editable=false)
{
parent::__construct($out);
@ -38,7 +60,37 @@ class ExtendedProfileWidget extends Widget
$this->editable = $editable;
}
/**
* Show the extended profile, or the edit form
*/
public function show()
{
if ($this->editable) {
parent::show();
} else {
$this->showSections();
}
}
/**
* Show form data
*/
public function formData()
{
// For JQuery UI modal dialog
$this->out->elementStart(
'div',
array('id' => 'confirm-dialog', 'title' => 'Confirmation Required')
);
$this->out->text('Really delete this entry?');
$this->out->elementEnd('div');
$this->showSections();
}
/**
* Show each section of the extended profile
*/
public function showSections()
{
$sections = $this->ext->getSections();
foreach ($sections as $name => $section) {
@ -46,21 +98,45 @@ class ExtendedProfileWidget extends Widget
}
}
/**
* Show an extended profile section
*
* @param string $name name of the section
* @param array $section array of fields for the section
*/
protected function showExtendedProfileSection($name, $section)
{
$this->out->element('h3', null, $section['label']);
$this->out->elementStart('table', array('class' => 'extended-profile'));
foreach ($section['fields'] as $fieldName => $field) {
$this->showExtendedProfileField($fieldName, $field);
switch($fieldName) {
case 'phone':
case 'im':
case 'website':
case 'experience':
case 'education':
$this->showMultiple($fieldName, $field);
break;
default:
$this->showExtendedProfileField($fieldName, $field);
}
}
$this->out->elementEnd('table');
}
/**
* Show an extended profile field
*
* @param string $name name of the field
* @param array $field set of key/value pairs for the field
*/
protected function showExtendedProfileField($name, $field)
{
$this->out->elementStart('tr');
$this->out->element('th', null, $field['label']);
$this->out->element('th', str_replace(' ','_',strtolower($field['label'])), $field['label']);
$this->out->elementStart('td');
if ($this->editable) {
@ -73,30 +149,504 @@ class ExtendedProfileWidget extends Widget
$this->out->elementEnd('tr');
}
protected function showFieldValue($name, $field)
{
$this->out->text($name);
protected function showMultiple($name, $fields) {
foreach ($fields as $field) {
$this->showExtendedProfileField($name, $field);
}
}
// XXX: showPhone, showIm and showWebsite all work the same, so
// combine
protected function showPhone($name, $field)
{
$this->out->elementStart('div', array('class' => 'phone-display'));
$this->out->text($field['value']);
if (!empty($field['rel'])) {
$this->out->text(' (' . $field['rel'] . ')');
}
$this->out->elementEnd('div');
}
protected function showIm($name, $field)
{
$this->out->elementStart('div', array('class' => 'im-display'));
$this->out->text($field['value']);
if (!empty($field['rel'])) {
$this->out->text(' (' . $field['rel'] . ')');
}
$this->out->elementEnd('div');
}
protected function showWebsite($name, $field)
{
$this->out->elementStart('div', array('class' => 'website-display'));
$url = $field['value'];
$this->out->element(
"a",
array(
'href' => $url,
'class' => 'extended-profile-link',
'target' => "_blank"
),
$url
);
if (!empty($field['rel'])) {
$this->out->text(' (' . $field['rel'] . ')');
}
$this->out->elementEnd('div');
}
protected function showEditableIm($name, $field)
{
$index = isset($field['index']) ? $field['index'] : 0;
$id = "extprofile-$name-$index";
$rel = $id . '-rel';
$this->out->elementStart(
'div', array(
'id' => $id . '-edit',
'class' => 'im-item'
)
);
$this->out->input(
$id,
null,
isset($field['value']) ? $field['value'] : null
);
$this->out->dropdown(
$id . '-rel',
'Type',
array(
'jabber' => 'Jabber',
'gtalk' => 'GTalk',
'aim' => 'AIM',
'yahoo' => 'Yahoo! Messenger',
'msn' => 'MSN',
'skype' => 'Skype',
'other' => 'Other'
),
null,
false,
isset($field['rel']) ? $field['rel'] : null
);
$this->showMultiControls();
$this->out->elementEnd('div');
}
protected function showEditablePhone($name, $field)
{
$index = isset($field['index']) ? $field['index'] : 0;
$id = "extprofile-$name-$index";
$rel = $id . '-rel';
$this->out->elementStart(
'div', array(
'id' => $id . '-edit',
'class' => 'phone-item'
)
);
$this->out->input(
$id,
null,
isset($field['value']) ? $field['value'] : null
);
$this->out->dropdown(
$id . '-rel',
'Type',
array(
'office' => 'Office',
'mobile' => 'Mobile',
'home' => 'Home',
'pager' => 'Pager',
'other' => 'Other'
),
null,
false,
isset($field['rel']) ? $field['rel'] : null
);
$this->showMultiControls();
$this->out->elementEnd('div');
}
protected function showEditableWebsite($name, $field)
{
$index = isset($field['index']) ? $field['index'] : 0;
$id = "extprofile-$name-$index";
$rel = $id . '-rel';
$this->out->elementStart(
'div', array(
'id' => $id . '-edit',
'class' => 'website-item'
)
);
$this->out->input(
$id,
null,
isset($field['value']) ? $field['value'] : null
);
$this->out->dropdown(
$id . '-rel',
'Type',
array(
'blog' => 'Blog',
'homepage' => 'Homepage',
'facebook' => 'Facebook',
'linkedin' => 'LinkedIn',
'flickr' => 'Flickr',
'google' => 'Google Profile',
'other' => 'Other',
'twitter' => 'Twitter'
),
null,
false,
isset($field['rel']) ? $field['rel'] : null
);
$this->showMultiControls();
$this->out->elementEnd('div');
}
protected function showExperience($name, $field)
{
$this->out->elementStart('div', 'experience-item');
$this->out->element('div', 'label', _m('Company'));
if (!empty($field['company'])) {
$this->out->element('div', 'field', $field['company']);
$this->out->element('div', 'label', _m('Start'));
$this->out->element(
'div',
array('class' => 'field date'),
date('j M Y', strtotime($field['start'])
)
);
$this->out->element('div', 'label', _m('End'));
$this->out->element(
'div',
array('class' => 'field date'),
date('j M Y', strtotime($field['end'])
)
);
if ($field['current']) {
$this->out->element(
'div',
array('class' => 'field current'),
'(' . _m('Current') . ')'
);
}
}
$this->out->elementEnd('div');
}
protected function showEditableExperience($name, $field)
{
$index = isset($field['index']) ? $field['index'] : 0;
$id = "extprofile-$name-$index";
$this->out->elementStart(
'div', array(
'id' => $id . '-edit',
'class' => 'experience-item'
)
);
$this->out->element('div', 'label', _m('Company'));
$this->out->input(
$id,
null,
isset($field['company']) ? $field['company'] : null
);
$this->out->element('div', 'label', _m('Start'));
$this->out->input(
$id . '-start',
null,
isset($field['start']) ? date('j M Y', strtotime($field['start'])) : null
);
$this->out->element('div', 'label', _m('End'));
$this->out->input(
$id . '-end',
null,
isset($field['end']) ? date('j M Y', strtotime($field['end'])) : null
);
$this->out->hidden(
$id . '-current',
'false'
);
$this->out->elementStart('div', 'current-checkbox');
$this->out->checkbox(
$id . '-current',
_m('Current'),
$field['current']
);
$this->out->elementEnd('div');
$this->showMultiControls();
$this->out->elementEnd('div');
}
protected function showEducation($name, $field)
{
$this->out->elementStart('div', 'education-item');
$this->out->element('div', 'label', _m('Institution'));
$this->out->element('div', 'field', $field['school']);
$this->out->element('div', 'label', _m('Degree'));
$this->out->element('div', 'field', $field['degree']);
$this->out->element('div', 'label', _m('Description'));
$this->out->element('div', 'field', $field['description']);
$this->out->element('div', 'label', _m('Start'));
$this->out->element(
'div',
array('class' => 'field date'),
date('j M Y', strtotime($field['start'])
)
);
$this->out->element('div', 'label', _m('End'));
$this->out->element(
'div',
array('class' => 'field date'),
date('j M Y', strtotime($field['end'])
)
);
$this->out->elementEnd('div');
}
protected function showEditableEducation($name, $field)
{
$index = isset($field['index']) ? $field['index'] : 0;
$id = "extprofile-$name-$index";
$this->out->elementStart(
'div', array(
'id' => $id . '-edit',
'class' => 'education-item'
)
);
$this->out->element('div', 'label', _m('Institution'));
$this->out->input(
$id,
null,
isset($field['school']) ? $field['school'] : null
);
$this->out->element('div', 'label', _m('Degree'));
$this->out->input(
$id . '-degree',
null,
isset($field['degree']) ? $field['degree'] : null
);
$this->out->element('div', 'label', _m('Description'));
$this->out->element('div', 'field', $field['description']);
$this->out->textarea(
$id . '-description',
null,
isset($field['description']) ? $field['description'] : null
);
$this->out->element('div', 'label', _m('Start'));
$this->out->input(
$id . '-start',
null,
isset($field['start']) ? date('j M Y', strtotime($field['start'])) : null
);
$this->out->element('div', 'label', _m('End'));
$this->out->input(
$id . '-end',
null,
isset($field['end']) ? date('j M Y', strtotime($field['end'])) : null
);
$this->showMultiControls();
$this->out->elementEnd('div');
}
function showMultiControls()
{
$this->out->element(
'a',
array(
'class' => 'remove_row',
'href' => 'javascript://',
'style' => 'display: none;'
),
'-'
);
$this->out->element(
'a',
array(
'class' => 'add_row',
'href' => 'javascript://',
'style' => 'display: none;'
),
'Add another item'
);
}
/**
* Outputs the value of a field
*
* @param string $name name of the field
* @param array $field set of key/value pairs for the field
*/
protected function showFieldValue($name, $field)
{
$type = strval(@$field['type']);
switch($type)
{
case '':
case 'text':
case 'textarea':
$this->out->text($this->ext->getTextValue($name));
break;
case 'date':
$value = $this->ext->getDateValue($name);
if (!empty($value)) {
$this->out->element(
'div',
array('class' => 'field date'),
date('j M Y', strtotime($value))
);
}
break;
case 'person':
$this->out->text($this->ext->getTextValue($name));
break;
case 'tags':
$this->out->text($this->ext->getTags());
break;
case 'phone':
$this->showPhone($name, $field);
break;
case 'website':
$this->showWebsite($name, $field);
break;
case 'im':
$this->showIm($name, $field);
break;
case 'experience':
$this->showExperience($name, $field);
break;
case 'education':
$this->showEducation($name, $field);
break;
default:
$this->out->text("TYPE: $type");
}
}
/**
* Show an editable version of the field
*
* @param string $name name fo the field
* @param array $field array of key/value pairs for the field
*/
protected function showEditableField($name, $field)
{
$out = $this->out;
//$out = new HTMLOutputter();
// @fixme
$type = strval(@$field['type']);
$id = "extprofile-" . $name;
$value = 'placeholder';
switch ($type) {
case '':
case 'text':
$out->input($id, null, $value);
break;
case 'textarea':
$out->textarea($id, null, $value);
break;
default:
$out->input($id, null, "TYPE: $type");
case '':
case 'text':
$out->input($id, null, $this->ext->getTextValue($name));
break;
case 'date':
$out->input(
$id,
null,
date('j M Y', strtotime($this->ext->getDateValue($name)))
);
break;
case 'person':
$out->input($id, null, $this->ext->getTextValue($name));
break;
case 'textarea':
$out->textarea($id, null, $this->ext->getTextValue($name));
break;
case 'tags':
$out->input($id, null, $this->ext->getTags());
break;
case 'phone':
$this->showEditablePhone($name, $field);
break;
case 'im':
$this->showEditableIm($name, $field);
break;
case 'website':
$this->showEditableWebsite($name, $field);
break;
case 'experience':
$this->showEditableExperience($name, $field);
break;
case 'education':
$this->showEditableEducation($name, $field);
break;
default:
$out->input($id, null, "TYPE: $type");
}
}
/**
* Action elements
*
* @return void
*/
function formActions()
{
$this->out->submit(
'save',
_m('BUTTON','Save'),
'submit form_action-secondary',
'save',
_('Save details')
);
}
/**
* ID of the form
*
* @return string ID of the form
*/
function id()
{
return 'profile-details-' . $this->profile->id;
}
/**
* class of the form
*
* @return string of the form class
*/
function formClass()
{
return 'form_profile_details form_settings';
}
/**
* Action of the form
*
* @return string URL of the action
*/
function action()
{
return common_local_url('profiledetailsettings');
}
}

View File

@ -0,0 +1,144 @@
var SN_EXTENDED = SN_EXTENDED || {};
SN_EXTENDED.reorder = function(cls) {
var divs = $('div[class=' + cls + ']');
$(divs).each(function(i, div) {
$(div).find('a.add_row').hide();
$(div).find('a.remove_row').show();
SN_EXTENDED.replaceIndex(SN_EXTENDED.rowIndex(div), i);
});
var lastDiv = $(divs).last().closest('tr');
lastDiv.addClass('supersizeme');
$(divs).last().find('a.add_row').show();
if (divs.length == 1) {
$(divs).find('a.remove_row').fadeOut("slow");
}
};
SN_EXTENDED.rowIndex = function(div) {
var idstr = $(div).attr('id');
var id = idstr.match(/\d+/);
return id;
};
SN_EXTENDED.rowCount = function(cls) {
var divs = $.find('div[class=' + cls + ']');
return divs.length;
};
SN_EXTENDED.replaceIndex = function(elem, oldIndex, newIndex) {
$(elem).find('*').each(function() {
$.each(this.attributes, function(i, attrib) {
var regexp = /extprofile-.*-\d.*/;
var value = attrib.value;
var match = value.match(regexp);
if (match !== null) {
attrib.value = value.replace("-" + oldIndex, "-" + newIndex);
}
});
});
}
SN_EXTENDED.resetRow = function(elem) {
$(elem).find('input, textarea').attr('value', '');
$(elem).find('input').removeAttr('disabled');
$(elem).find("select option[value='office']").attr("selected", true);
$(elem).find("input:checkbox").attr('checked', false);
$(elem).find("input[name$=-start], input[name$=-end]").each(function() {
$(this).removeClass('hasDatepicker');
$(this).datepicker({ dateFormat: 'd M yy' });
});
};
SN_EXTENDED.addRow = function() {
var div = $(this).closest('div');
var id = div.attr('id');
var cls = div.attr('class');
var index = id.match(/\d+/);
var newIndex = parseInt(index) + 1;
var newtr = $(div).closest('tr').removeClass('supersizeme').clone();
SN_EXTENDED.replaceIndex(newtr, index, newIndex);
SN_EXTENDED.resetRow(newtr);
$(div).closest('tr').after(newtr);
SN_EXTENDED.reorder(cls);
};
SN_EXTENDED.removeRow = function() {
var div = $(this).closest('div');
var id = $(div).attr('id');
var cls = $(div).attr('class');
var that = this;
$("#confirm-dialog").dialog({
buttons : {
"Confirm" : function() {
$(this).dialog("close");
var target = $(that).closest('tr');
target.fadeOut("slow", function() {
$(target).remove();
SN_EXTENDED.reorder(cls);
});
},
"Cancel" : function() {
$(this).dialog("close");
}
}
});
var cnt = SN_EXTENDED.rowCount(cls);
if (cnt > 1) {
$("#confirm-dialog").dialog("open");
}
};
$(document).ready(function() {
$("#confirm-dialog").dialog({
autoOpen: false,
modal: true
});
$("input#extprofile-manager").autocomplete({
source: 'finduser',
minLength: 2 });
$("input[name$=-start], input[name$=-end], #extprofile-birthday").datepicker({ dateFormat: 'd M yy' });
var multifields = ["phone-item", "experience-item", "education-item", "im-item", 'website-item'];
for (f in multifields) {
SN_EXTENDED.reorder(multifields[f]);
}
$("input#extprofile-manager").autocomplete({
source: 'finduser',
minLength: 2 });
$('.add_row').live('click', SN_EXTENDED.addRow);
$('.remove_row').live('click', SN_EXTENDED.removeRow);
$('input:checkbox[name$=current]').each(function() {
var input = $(this).parent().siblings('input[id$=-end]');
if ($(this).is(':checked')) {
$(input).attr('disabled', 'true');
}
});
$('input:checkbox[name$=current]').live('click', function() {
var input = $(this).parent().siblings('input[id$=-end]');
if ($(this).is(':checked')) {
$(input).val('');
$(input).attr('disabled', 'true');
} else {
$(input).removeAttr('disabled');
}
});
});

View File

@ -1,22 +0,0 @@
/* Note the #content is only needed to override weird crap in default styles */
#content table.extended-profile {
width: 100%;
border-collapse: separate;
border-spacing: 8px;
}
#content table.extended-profile th {
color: #777;
background-color: #eee;
width: 150px;
padding-top: 0; /* override bizarre theme defaults */
text-align: right;
padding-right: 8px;
}
#content table.extended-profile td {
padding: 0; /* override bizarre theme defaults */
padding-left: 8px;
}

View File

@ -21,8 +21,9 @@ if (!defined('STATUSNET')) {
exit(1);
}
class ProfileDetailAction extends ProfileAction
class ProfileDetailAction extends ShowstreamAction
{
function isReadOnly($args)
{
return true;
@ -33,28 +34,18 @@ class ProfileDetailAction extends ProfileAction
return $this->profile->getFancyName();
}
function showLocalNav()
{
$nav = new PersonalGroupNav($this);
$nav->show();
}
function showStylesheets() {
parent::showStylesheets();
$this->cssLink('plugins/ExtendedProfile/profiledetail.css');
$this->cssLink('plugins/ExtendedProfile/css/profiledetail.css');
return true;
}
function handle($args)
{
$this->showPage();
}
function showContent()
{
$cur = common_current_user();
if ($cur && $cur->id == $this->profile->id) { // your own page
$this->elementStart('div', 'entity_actions');
$this->elementStart('ul');
$this->elementStart('li', 'entity_edit');
$this->element('a', array('href' => common_local_url('profiledetailsettings'),
// TRANS: Link title for link on user profile.
@ -62,6 +53,7 @@ class ProfileDetailAction extends ProfileAction
// TRANS: Link text for link on user profile.
_m('Edit'));
$this->elementEnd('li');
$this->elementEnd('ul');
$this->elementEnd('div');
}

View File

@ -21,7 +21,7 @@ if (!defined('STATUSNET')) {
exit(1);
}
class ProfileDetailSettingsAction extends AccountSettingsAction
class ProfileDetailSettingsAction extends ProfileSettingsAction
{
function title()
@ -43,13 +43,38 @@ class ProfileDetailSettingsAction extends AccountSettingsAction
function showStylesheets() {
parent::showStylesheets();
$this->cssLink('plugins/ExtendedProfile/profiledetail.css');
$this->cssLink('plugins/ExtendedProfile/css/profiledetail.css');
$this->cssLink('http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css');
return true;
}
function handle($args)
function showScripts() {
parent::showScripts();
$this->script('plugins/ExtendedProfile/js/profiledetail.js');
$this->script('http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js');
return true;
}
function handlePost()
{
$this->showPage();
// CSRF protection
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
$this->showForm(
_m(
'There was a problem with your session token. '
. 'Try again, please.'
)
);
return;
}
if ($this->arg('save')) {
$this->saveDetails();
} else {
// TRANS: Message given submitting a form with an unknown action
$this->showForm(_m('Unexpected form submission.'));
}
}
function showContent()
@ -57,7 +82,554 @@ class ProfileDetailSettingsAction extends AccountSettingsAction
$cur = common_current_user();
$profile = $cur->getProfile();
$widget = new ExtendedProfileWidget($this, $profile, ExtendedProfileWidget::EDITABLE);
$widget = new ExtendedProfileWidget(
$this,
$profile,
ExtendedProfileWidget::EDITABLE
);
$widget->show();
}
function saveDetails()
{
common_debug(var_export($_POST, true));
$user = common_current_user();
try {
$this->saveStandardProfileDetails($user);
$profile = $user->getProfile();
$simpleFieldNames = array('title', 'spouse', 'kids', 'manager');
$dateFieldNames = array('birthday');
foreach ($simpleFieldNames as $name) {
$value = $this->trimmed('extprofile-' . $name);
if (!empty($value)) {
$this->saveField($user, $name, $value);
}
}
foreach ($dateFieldNames as $name) {
$value = $this->trimmed('extprofile-' . $name);
$dateVal = $this->parseDate($name, $value);
$this->saveField(
$user,
$name,
null,
null,
null,
$dateVal
);
}
$this->savePhoneNumbers($user);
$this->saveIms($user);
$this->saveWebsites($user);
$this->saveExperiences($user);
$this->saveEducations($user);
} catch (Exception $e) {
$this->showForm($e->getMessage(), false);
return;
}
$this->showForm(_('Details saved.'), true);
}
function parseDate($fieldname, $datestr, $required = false)
{
if (empty($datestr) && $required) {
$msg = sprintf(
_m('You must supply a date for "%s".'),
$fieldname
);
throw new Exception($msg);
} else {
$ts = strtotime($datestr);
if ($ts === false) {
throw new Exception(
sprintf(
_m('Invalid date entered for "%s": %s'),
$fieldname,
$ts
)
);
}
return common_sql_date($ts);
}
return null;
}
function savePhoneNumbers($user) {
$phones = $this->findPhoneNumbers();
$this->removeAll($user, 'phone');
$i = 0;
foreach($phones as $phone) {
if (!empty($phone['value'])) {
++$i;
$this->saveField(
$user,
'phone',
$phone['value'],
$phone['rel'],
$i
);
}
}
}
function findPhoneNumbers() {
// Form vals look like this:
// 'extprofile-phone-1' => '11332',
// 'extprofile-phone-1-rel' => 'mobile',
$phones = $this->sliceParams('phone', 2);
$phoneArray = array();
foreach ($phones as $phone) {
list($number, $rel) = array_values($phone);
$phoneArray[] = array(
'value' => $number,
'rel' => $rel
);
}
return $phoneArray;
}
function findIms() {
// Form vals look like this:
// 'extprofile-im-0' => 'jed',
// 'extprofile-im-0-rel' => 'yahoo',
$ims = $this->sliceParams('im', 2);
$imArray = array();
foreach ($ims as $im) {
list($id, $rel) = array_values($im);
$imArray[] = array(
'value' => $id,
'rel' => $rel
);
}
return $imArray;
}
function saveIms($user) {
$ims = $this->findIms();
$this->removeAll($user, 'im');
$i = 0;
foreach($ims as $im) {
if (!empty($im['value'])) {
++$i;
$this->saveField(
$user,
'im',
$im['value'],
$im['rel'],
$i
);
}
}
}
function findWebsites() {
// Form vals look like this:
$sites = $this->sliceParams('website', 2);
$wsArray = array();
foreach ($sites as $site) {
list($id, $rel) = array_values($site);
$wsArray[] = array(
'value' => $id,
'rel' => $rel
);
}
return $wsArray;
}
function saveWebsites($user) {
$sites = $this->findWebsites();
$this->removeAll($user, 'website');
$i = 0;
foreach($sites as $site) {
if (!Validate::uri(
$site['value'],
array('allowed_schemes' => array('http', 'https')))
) {
throw new Exception(sprintf(_m('Invalid URL: %s'), $site['value']));
}
if (!empty($site['value'])) {
++$i;
$this->saveField(
$user,
'website',
$site['value'],
$site['rel'],
$i
);
}
}
}
function findExperiences() {
// Form vals look like this:
// 'extprofile-experience-0' => 'Bozotronix',
// 'extprofile-experience-0-current' => 'true'
// 'extprofile-experience-0-start' => '1/5/10',
// 'extprofile-experience-0-end' => '2/3/11',
$experiences = $this->sliceParams('experience', 4);
$expArray = array();
foreach ($experiences as $exp) {
if (sizeof($experiences) == 4) {
list($company, $current, $end, $start) = array_values($exp);
} else {
$end = null;
list($company, $current, $start) = array_values($exp);
}
if (!empty($company)) {
$expArray[] = array(
'company' => $company,
'start' => $this->parseDate('Start', $start, true),
'end' => ($current == 'false') ? $this->parseDate('End', $end, true) : null,
'current' => ($current == 'false') ? false : true
);
}
}
return $expArray;
}
function saveExperiences($user) {
common_debug('save experiences');
$experiences = $this->findExperiences();
$this->removeAll($user, 'company');
$this->removeAll($user, 'start');
$this->removeAll($user, 'end'); // also stores 'current'
$i = 0;
foreach($experiences as $experience) {
if (!empty($experience['company'])) {
++$i;
$this->saveField(
$user,
'company',
$experience['company'],
null,
$i
);
$this->saveField(
$user,
'start',
null,
null,
$i,
$experience['start']
);
// Save "current" employer indicator in rel
if ($experience['current']) {
$this->saveField(
$user,
'end',
null,
'current', // rel
$i
);
} else {
$this->saveField(
$user,
'end',
null,
null,
$i,
$experience['end']
);
}
}
}
}
function findEducations() {
// Form vals look like this:
// 'extprofile-education-0-school' => 'Pigdog',
// 'extprofile-education-0-degree' => 'BA',
// 'extprofile-education-0-description' => 'Blar',
// 'extprofile-education-0-start' => '05/22/99',
// 'extprofile-education-0-end' => '05/22/05',
$edus = $this->sliceParams('education', 5);
$eduArray = array();
foreach ($edus as $edu) {
list($school, $degree, $description, $end, $start) = array_values($edu);
if (!empty($school)) {
$eduArray[] = array(
'school' => $school,
'degree' => $degree,
'description' => $description,
'start' => $this->parseDate('Start', $start, true),
'end' => $this->parseDate('End', $end, true)
);
}
}
return $eduArray;
}
function saveEducations($user) {
common_debug('save education');
$edus = $this->findEducations();
common_debug(var_export($edus, true));
$this->removeAll($user, 'school');
$this->removeAll($user, 'degree');
$this->removeAll($user, 'degree_descr');
$this->removeAll($user, 'school_start');
$this->removeAll($user, 'school_end');
$i = 0;
foreach($edus as $edu) {
if (!empty($edu['school'])) {
++$i;
$this->saveField(
$user,
'school',
$edu['school'],
null,
$i
);
$this->saveField(
$user,
'degree',
$edu['degree'],
null,
$i
);
$this->saveField(
$user,
'degree_descr',
$edu['description'],
null,
$i
);
$this->saveField(
$user,
'school_start',
null,
null,
$i,
$edu['start']
);
$this->saveField(
$user,
'school_end',
null,
null,
$i,
$edu['end']
);
}
}
}
function arraySplit($array, $pieces)
{
if ($pieces < 2) {
return array($array);
}
$newCount = ceil(count($array) / $pieces);
$a = array_slice($array, 0, $newCount);
$b = $this->arraySplit(array_slice($array, $newCount), $pieces - 1);
return array_merge(array($a), $b);
}
function findMultiParams($type) {
$formVals = array();
$target = $type;
foreach ($_POST as $key => $val) {
if (strrpos('extprofile-' . $key, $target) !== false) {
$formVals[$key] = $val;
}
}
return $formVals;
}
function sliceParams($key, $size) {
$slice = array();
$params = $this->findMultiParams($key);
ksort($params);
$slice = $this->arraySplit($params, sizeof($params) / $size);
return $slice;
}
/**
* Save an extended profile field as a Profile_detail
*
* @param User $user the current user
* @param string $name field name
* @param string $value field value
* @param string $rel field rel (type)
* @param int $index index (fields can have multiple values)
* @param date $date related date
*/
function saveField($user, $name, $value, $rel = null, $index = null, $date = null)
{
$profile = $user->getProfile();
$detail = new Profile_detail();
$detail->profile_id = $profile->id;
$detail->field_name = $name;
$detail->value_index = $index;
$result = $detail->find(true);
if (empty($result)) {
$detial->value_index = $index;
$detail->rel = $rel;
$detail->field_value = $value;
$detail->date = $date;
$detail->created = common_sql_now();
$result = $detail->insert();
if (empty($result)) {
common_log_db_error($detail, 'INSERT', __FILE__);
$this->serverError(_m('Could not save profile details.'));
}
} else {
$orig = clone($detail);
$detail->field_value = $value;
$detail->rel = $rel;
$detail->date = $date;
$result = $detail->update($orig);
if (empty($result)) {
common_log_db_error($detail, 'UPDATE', __FILE__);
$this->serverError(_m('Could not save profile details.'));
}
}
$detail->free();
}
function removeAll($user, $name)
{
$profile = $user->getProfile();
$detail = new Profile_detail();
$detail->profile_id = $profile->id;
$detail->field_name = $name;
$detail->delete();
$detail->free();
}
/**
* Save fields that should be stored in the main profile object
*
* XXX: There's a lot of dupe code here from ProfileSettingsAction.
* Do not want.
*
* @param User $user the current user
*/
function saveStandardProfileDetails($user)
{
$fullname = $this->trimmed('extprofile-fullname');
$location = $this->trimmed('extprofile-location');
$tagstring = $this->trimmed('extprofile-tags');
$bio = $this->trimmed('extprofile-bio');
if ($tagstring) {
$tags = array_map(
'common_canonical_tag',
preg_split('/[\s,]+/', $tagstring)
);
} else {
$tags = array();
}
foreach ($tags as $tag) {
if (!common_valid_profile_tag($tag)) {
// TRANS: Validation error in form for profile settings.
// TRANS: %s is an invalid tag.
throw new Exception(sprintf(_m('Invalid tag: "%s".'), $tag));
}
}
$profile = $user->getProfile();
$oldTags = $user->getSelfTags();
$newTags = array_diff($tags, $oldTags);
if ($fullname != $profile->fullname
|| $location != $profile->location
|| !empty($newTags)
|| $bio != $profile->bio) {
$orig = clone($profile);
$profile->nickname = $user->nickname;
$profile->fullname = $fullname;
$profile->bio = $bio;
$profile->location = $location;
$loc = Location::fromName($location);
if (empty($loc)) {
$profile->lat = null;
$profile->lon = null;
$profile->location_id = null;
$profile->location_ns = null;
} else {
$profile->lat = $loc->lat;
$profile->lon = $loc->lon;
$profile->location_id = $loc->location_id;
$profile->location_ns = $loc->location_ns;
}
$profile->profileurl = common_profile_url($user->nickname);
$result = $profile->update($orig);
if ($result === false) {
common_log_db_error($profile, 'UPDATE', __FILE__);
// TRANS: Server error thrown when user profile settings could not be saved.
$this->serverError(_('Could not save profile.'));
return;
}
// Set the user tags
$result = $user->setSelfTags($tags);
if (!$result) {
// TRANS: Server error thrown when user profile settings tags could not be saved.
$this->serverError(_('Could not save tags.'));
return;
}
Event::handle('EndProfileSaveForm', array($this));
common_broadcast_profile($profile);
}
}
}

View File

@ -1,9 +1,12 @@
As of StatusNet 1.0.x, actual formatting of the notices is done server-side,
loaded by AJAX after the real-time notification comes in. This has the drawback
that we may make extra HTTP requests and delay incoming notices a little, but
means that formatting and internationalization is consistent.
== TODO ==
* i18n
* Update mark behaviour (on notice send)
* Pause, Send a notice ~ should not update counter
* Pause ~ retain up to 50-100 most recent notices
* Add geo data
* Make it work for Conversation page (perhaps a little tricky)
* IE is updating the counter in document title all the time (Not sure if this
is still an issue)

View File

@ -45,9 +45,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
*/
class RealtimePlugin extends Plugin
{
protected $replyurl = null;
protected $favorurl = null;
protected $deleteurl = null;
protected $showurl = null;
/**
* When it's time to initialize the plugin, calculate and
@ -56,11 +54,8 @@ class RealtimePlugin extends Plugin
function onInitializePlugin()
{
$this->replyurl = common_local_url('newnotice');
$this->favorurl = common_local_url('favor');
$this->repeaturl = common_local_url('repeat');
// FIXME: need to find a better way to pass this pattern in
$this->deleteurl = common_local_url('deletenotice',
$this->showurl = common_local_url('shownotice',
array('notice' => '0000000000'));
return true;
}
@ -323,7 +318,12 @@ class RealtimePlugin extends Plugin
function _getScripts()
{
return array(Plugin::staticPath('Realtime', 'realtimeupdate.min.js'));
if (common_config('site', 'minify')) {
$js = 'realtimeupdate.min.js';
} else {
$js = 'realtimeupdate.js';
}
return array(Plugin::staticPath('Realtime', $js));
}
/**
@ -354,7 +354,7 @@ class RealtimePlugin extends Plugin
function _updateInitialize($timeline, $user_id)
{
return "RealtimeUpdate.init($user_id, \"$this->replyurl\", \"$this->favorurl\", \"$this->repeaturl\", \"$this->deleteurl\"); ";
return "RealtimeUpdate.init($user_id, \"$this->showurl\"); ";
}
function _connect()

View File

@ -44,10 +44,7 @@
*/
RealtimeUpdate = {
_userid: 0,
_replyurl: '',
_favorurl: '',
_repeaturl: '',
_deleteurl: '',
_showurl: '',
_updatecounter: 0,
_maxnotices: 50,
_windowhasfocus: true,
@ -66,21 +63,15 @@ RealtimeUpdate = {
* feed data into the RealtimeUpdate object!
*
* @param {int} userid: local profile ID of the currently logged-in user
* @param {String} replyurl: URL for newnotice action, used when generating reply buttons
* @param {String} favorurl: URL for favor action, used when generating fave buttons
* @param {String} repeaturl: URL for repeat action, used when generating repeat buttons
* @param {String} deleteurl: URL template for deletenotice action, used when generating delete buttons.
* @param {String} showurl: URL for shownotice action, used when fetching formatting notices.
* This URL contains a stub value of 0000000000 which will be replaced with the notice ID.
*
* @access public
*/
init: function(userid, replyurl, favorurl, repeaturl, deleteurl)
init: function(userid, showurl)
{
RealtimeUpdate._userid = userid;
RealtimeUpdate._replyurl = replyurl;
RealtimeUpdate._favorurl = favorurl;
RealtimeUpdate._repeaturl = repeaturl;
RealtimeUpdate._deleteurl = deleteurl;
RealtimeUpdate._showurl = showurl;
RealtimeUpdate._documenttitle = document.title;
@ -163,50 +154,51 @@ RealtimeUpdate = {
return;
}
var noticeItem = RealtimeUpdate.makeNoticeItem(data);
var noticeItemID = $(noticeItem).attr('id');
RealtimeUpdate.makeNoticeItem(data, function(noticeItem) {
var noticeItemID = $(noticeItem).attr('id');
var list = $("#notices_primary .notices:first")
var prepend = true;
var list = $("#notices_primary .notices:first")
var prepend = true;
var threaded = list.hasClass('threaded-notices');
if (threaded && data.in_reply_to_status_id) {
// aho!
var parent = $('#notice-' + data.in_reply_to_status_id);
if (parent.length == 0) {
// @todo fetch the original, insert it, and finish the rest
} else {
// Check the parent notice to make sure it's not a reply itself.
// If so, use it's parent as the parent.
var parentList = parent.closest('.notices');
if (parentList.hasClass('threaded-replies')) {
parent = parentList.closest('.notice');
var threaded = list.hasClass('threaded-notices');
if (threaded && data.in_reply_to_status_id) {
// aho!
var parent = $('#notice-' + data.in_reply_to_status_id);
if (parent.length == 0) {
// @todo fetch the original, insert it, and finish the rest
} else {
// Check the parent notice to make sure it's not a reply itself.
// If so, use it's parent as the parent.
var parentList = parent.closest('.notices');
if (parentList.hasClass('threaded-replies')) {
parent = parentList.closest('.notice');
}
list = parent.find('.threaded-replies');
if (list.length == 0) {
list = $('<ul class="notices threaded-replies xoxo"></ul>');
parent.append(list);
}
prepend = false;
}
list = parent.find('.threaded-replies');
if (list.length == 0) {
list = $('<ul class="notices threaded-replies xoxo"></ul>');
parent.append(list);
}
prepend = false;
}
}
var newNotice = $(noticeItem);
if (prepend) {
list.prepend(newNotice);
} else {
var placeholder = list.find('li.notice-reply-placeholder')
if (placeholder.length > 0) {
newNotice.insertBefore(placeholder)
var newNotice = $(noticeItem);
if (prepend) {
list.prepend(newNotice);
} else {
newNotice.appendTo(list);
SN.U.NoticeInlineReplyPlaceholder(parent);
var placeholder = list.find('li.notice-reply-placeholder')
if (placeholder.length > 0) {
newNotice.insertBefore(placeholder)
} else {
newNotice.appendTo(list);
SN.U.NoticeInlineReplyPlaceholder(parent);
}
}
}
newNotice.css({display:"none"}).fadeIn(1000);
newNotice.css({display:"none"}).fadeIn(1000);
SN.U.NoticeReplyTo($('#'+noticeItemID));
SN.U.NoticeWithAttachment($('#'+noticeItemID));
SN.U.NoticeReplyTo($('#'+noticeItemID));
SN.U.NoticeWithAttachment($('#'+noticeItemID));
});
},
/**
@ -263,86 +255,24 @@ RealtimeUpdate = {
},
/**
* Builds a notice HTML block from JSON API-style data.
* Builds a notice HTML block from JSON API-style data;
* loads data from server, so runs async.
*
* @param {Object} data: extended JSON API-formatted notice
* @return {String} HTML fragment
*
* @fixme this replicates core StatusNet code, making maintenance harder
* @fixme sloppy HTML building (raw concat without escaping)
* @fixme no i18n support
* @fixme local variables pollute global namespace
* @param {function} callback: function(DOMNode) to receive new code
*
* @access private
*/
makeNoticeItem: function(data)
makeNoticeItem: function(data, callback)
{
if (data.hasOwnProperty('retweeted_status')) {
original = data['retweeted_status'];
repeat = data;
data = original;
unique = repeat['id'];
responsible = repeat['user'];
} else {
original = null;
repeat = null;
unique = data['id'];
responsible = data['user'];
}
user = data['user'];
html = data['html'].replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&amp;/g,'&');
source = data['source'].replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&amp;/g,'&');
ni = "<li class=\"hentry notice\" id=\"notice-"+unique+"\">"+
"<div class=\"entry-title\">"+
"<span class=\"vcard author\">"+
"<a href=\""+user['profile_url']+"\" class=\"url\" title=\""+user['name']+"\">"+
"<img src=\""+user['profile_image_url']+"\" class=\"avatar photo\" width=\"48\" height=\"48\" alt=\""+user['screen_name']+"\"/>"+
"<span class=\"nickname fn\">"+user['screen_name']+"</span>"+
"</a>"+
"</span>"+
"<p class=\"entry-content\">"+html+"</p>"+
"</div>"+
"<div class=\"entry-content\">"+
"<a class=\"timestamp\" rel=\"bookmark\" href=\""+data['url']+"\" >"+
"<abbr class=\"published\" title=\""+data['created_at']+"\">a few seconds ago</abbr>"+
"</a> "+
"<span class=\"source\">"+
"from "+
"<span class=\"device\">"+source+"</span>"+ // may have a link
"</span>";
if (data['conversation_url']) {
ni = ni+" <a class=\"response\" href=\""+data['conversation_url']+"\">in context</a>";
}
if (repeat) {
ru = repeat['user'];
ni = ni + "<span class=\"repeat vcard\">Repeated by " +
"<a href=\"" + ru['profile_url'] + "\" class=\"url\">" +
"<span class=\"nickname\">"+ ru['screen_name'] + "</span></a></span>";
}
ni = ni+"</div>";
ni = ni + "<div class=\"notice-options\">";
if (RealtimeUpdate._userid != 0) {
var input = $("form#form_notice fieldset input#token");
var session_key = input.val();
ni = ni+RealtimeUpdate.makeFavoriteForm(data['id'], session_key);
ni = ni+RealtimeUpdate.makeReplyLink(data['id'], data['user']['screen_name']);
if (RealtimeUpdate._userid == responsible['id']) {
ni = ni+RealtimeUpdate.makeDeleteLink(data['id']);
} else if (RealtimeUpdate._userid != user['id']) {
ni = ni+RealtimeUpdate.makeRepeatForm(data['id'], session_key);
}
}
ni = ni+"</div>";
ni = ni+"</li>";
return ni;
var url = RealtimeUpdate._showurl.replace('0000000000', data.id);
$.get(url, {ajax: 1}, function(data, textStatus, xhr) {
var notice = $('li.notice:first', data);
if (notice.length) {
var node = document._importNode(notice[0], true);
callback(node);
}
});
},
/**

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,140 @@
<?php
/**
* Data class to store local search subscriptions
*
* PHP version 5
*
* @category SearchSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* For storing the search subscriptions
*
* @category PollPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* @see DB_DataObject
*/
class SearchSub extends Managed_DataObject
{
public $__table = 'searchsub'; // table name
public $search; // text
public $profile_id; // int -> profile.id
public $created; // datetime
/**
* Get an instance by key
*
* This is a utility method to get a single instance with a given key value.
*
* @param string $k Key to use to lookup (usually 'user_id' for this class)
* @param mixed $v Value to lookup
*
* @return SearchSub object found, or null for no hits
*
*/
function staticGet($k, $v=null)
{
return Memcached_DataObject::staticGet('SearchSub', $k, $v);
}
/**
* Get an instance by compound key
*
* This is a utility method to get a single instance with a given set of
* key-value pairs. Usually used for the primary key for a compound key; thus
* the name.
*
* @param array $kv array of key-value mappings
*
* @return SearchSub object found, or null for no hits
*
*/
function pkeyGet($kv)
{
return Memcached_DataObject::pkeyGet('SearchSub', $kv);
}
/**
* The One True Thingy that must be defined and declared.
*/
public static function schemaDef()
{
return array(
'description' => 'SearchSubPlugin search subscription records',
'fields' => array(
'search' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'hash search associated with this subscription'),
'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'profile ID of subscribing user'),
'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
),
'primary key' => array('search', 'profile_id'),
'foreign keys' => array(
'searchsub_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
),
'indexes' => array(
'searchsub_created_idx' => array('created'),
'searchsub_profile_id_tag_idx' => array('profile_id', 'search'),
),
);
}
/**
* Start a search subscription!
*
* @param profile $profile subscriber
* @param string $search subscribee
* @return SearchSub
*/
static function start(Profile $profile, $search)
{
$ts = new SearchSub();
$ts->search = $search;
$ts->profile_id = $profile->id;
$ts->created = common_sql_now();
$ts->insert();
return $ts;
}
/**
* End a search subscription!
*
* @param profile $profile subscriber
* @param string $search subscribee
*/
static function cancel(Profile $profile, $search)
{
$ts = SearchSub::pkeyGet(array('search' => $search,
'profile_id' => $profile->id));
if ($ts) {
$ts->delete();
}
}
}

View File

@ -0,0 +1,212 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* A plugin to enable local tab subscription
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category SearchSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* SearchSub plugin main class
*
* @category SearchSubPlugin
* @package StatusNet
* @author Brion Vibber <brionv@status.net>
* @copyright 2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class SearchSubPlugin extends Plugin
{
const VERSION = '0.1';
/**
* Database schema setup
*
* @see Schema
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onCheckSchema()
{
$schema = Schema::get();
$schema->ensureTable('searchsub', SearchSub::schemaDef());
return true;
}
/**
* Load related modules when needed
*
* @param string $cls Name of the class to be loaded
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onAutoload($cls)
{
$dir = dirname(__FILE__);
switch ($cls)
{
case 'SearchSub':
include_once $dir.'/'.$cls.'.php';
return false;
case 'SearchsubAction':
case 'SearchunsubAction':
case 'SearchSubForm':
case 'SearchUnsubForm':
include_once $dir.'/'.strtolower($cls).'.php';
return false;
default:
return true;
}
}
/**
* Map URLs to actions
*
* @param Net_URL_Mapper $m path-to-action mapper
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onRouterInitialized($m)
{
$m->connect('search/:search/subscribe',
array('action' => 'searchsub'),
array('search' => Router::REGEX_TAG));
$m->connect('search/:search/unsubscribe',
array('action' => 'searchunsub'),
array('search' => Router::REGEX_TAG));
return true;
}
/**
* Plugin version data
*
* @param array &$versions array of version data
*
* @return value
*/
function onPluginVersion(&$versions)
{
$versions[] = array('name' => 'SearchSub',
'version' => self::VERSION,
'author' => 'Brion Vibber',
'homepage' => 'http://status.net/wiki/Plugin:SearchSub',
'rawdescription' =>
// TRANS: Plugin description.
_m('Plugin to allow following all messages with a given search.'));
return true;
}
/**
* Hook inbox delivery setup so search subscribers receive all
* notices with that search in their inbox.
*
* Currently makes no distinction between local messages and
* remote ones which happen to come in to the system. Remote
* notices that don't come in at all won't ever reach this.
*
* @param Notice $notice
* @param array $ni in/out map of profile IDs to inbox constants
* @return boolean hook result
*/
function onStartNoticeWhoGets(Notice $notice, array &$ni)
{
// Warning: this is potentially very slow
// with a lot of searches!
$sub = new SearchSub();
$sub->groupBy('search');
$sub->find();
while ($sub->fetch()) {
$search = $sub->search;
if ($this->matchSearch($notice, $search)) {
// Match? Find all those who subscribed to this
// search term and get our delivery on...
$searchsub = new SearchSub();
$searchsub->search = $search;
$searchsub->find();
while ($searchsub->fetch()) {
// These constants are currently not actually used, iirc
$ni[$searchsub->profile_id] = NOTICE_INBOX_SOURCE_SUB;
}
}
}
return true;
}
/**
* Does the given notice match the given fulltext search query?
*
* Warning: not guaranteed to match other search engine behavior, etc.
* Currently using a basic case-insensitive substring match, which
* probably fits with the 'LIKE' search but not the default MySQL
* or Sphinx search backends.
*
* @param Notice $notice
* @param string $search
* @return boolean
*/
function matchSearch(Notice $notice, $search)
{
return (mb_stripos($notice->content, $search) !== false);
}
/**
*
* @param NoticeSearchAction $action
* @param string $q
* @param Notice $notice
* @return boolean hook result
*/
function onStartNoticeSearchShowResults($action, $q, $notice)
{
$user = common_current_user();
if ($user) {
$search = $q;
$searchsub = SearchSub::pkeyGet(array('search' => $search,
'profile_id' => $user->id));
if ($searchsub) {
$form = new SearchUnsubForm($action, $search);
} else {
$form = new SearchSubForm($action, $search);
}
$action->elementStart('div', 'entity_actions');
$action->elementStart('ul');
$action->elementStart('li', 'entity_subscribe');
$form->show();
$action->elementEnd('li');
$action->elementEnd('ul');
$action->elementEnd('div');
}
return true;
}
}

View File

@ -0,0 +1,149 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008-2011, StatusNet, Inc.
*
* Search subscription action.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @copyright 2008-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Search subscription action
*
* Takes parameters:
*
* - token: session token to prevent CSRF attacks
* - ajax: boolean; whether to return Ajax or full-browser results
*
* Only works if the current user is logged in.
*
* @category Action
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Brion Vibber <brion@status.net>
* @copyright 2008-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class SearchsubAction extends Action
{
var $user;
var $search;
/**
* Check pre-requisites and instantiate attributes
*
* @param Array $args array of arguments (URL, GET, POST)
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
if ($this->boolean('ajax')) {
StatusNet::setApi(true);
}
// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
// TRANS: Client error displayed trying to perform any request method other than POST.
// TRANS: Do not translate POST.
$this->clientError(_('This action only accepts POST requests.'));
return false;
}
// CSRF protection
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
// TRANS: Client error displayed when the session token is not okay.
$this->clientError(_('There was a problem with your session token.'.
' Try again, please.'));
return false;
}
// Only for logged-in users
$this->user = common_current_user();
if (empty($this->user)) {
// TRANS: Client error displayed trying to subscribe when not logged in.
$this->clientError(_('Not logged in.'));
return false;
}
// Profile to subscribe to
$this->search = $this->arg('search');
if (empty($this->search)) {
// TRANS: Client error displayed trying to subscribe to a non-existing profile.
$this->clientError(_('No such profile.'));
return false;
}
return true;
}
/**
* Handle request
*
* Does the subscription and returns results.
*
* @param Array $args unused.
*
* @return void
*/
function handle($args)
{
// Throws exception on error
SearchSub::start($this->user->getProfile(),
$this->search);
if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
// TRANS: Page title when search subscription succeeded.
$this->element('title', null, _m('Subscribed'));
$this->elementEnd('head');
$this->elementStart('body');
$unsubscribe = new SearchUnsubForm($this, $this->search);
$unsubscribe->show();
$this->elementEnd('body');
$this->elementEnd('html');
} else {
$url = common_local_url('search',
array('search' => $this->search));
common_redirect($url, 303);
}
}
}

View File

@ -0,0 +1,142 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Form for subscribing to a search
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category SearchSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @copyright 2009-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
/**
* Form for subscribing to a user
*
* @category SearchSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*
* @see UnsubscribeForm
*/
class SearchSubForm extends Form
{
/**
* Name of search to subscribe to
*/
var $search = '';
/**
* Constructor
*
* @param HTMLOutputter $out output channel
* @param string $search name of search to subscribe to
*/
function __construct($out=null, $search=null)
{
parent::__construct($out);
$this->search = $search;
}
/**
* ID of the form
*
* @return int ID of the form
*/
function id()
{
return 'search-subscribe-' . $this->search;
}
/**
* class of the form
*
* @return string of the form class
*/
function formClass()
{
// class to match existing styles...
return 'form_user_subscribe ajax';
}
/**
* Action of the form
*
* @return string URL of the action
*/
function action()
{
return common_local_url('searchsub', array('search' => $this->search));
}
/**
* Legend of the Form
*
* @return void
*/
function formLegend()
{
$this->out->element('legend', null, _m('Subscribe to this search'));
}
/**
* Data elements of the form
*
* @return void
*/
function formData()
{
$this->out->hidden('subscribeto-' . $this->search,
$this->search,
'subscribeto');
}
/**
* Action elements
*
* @return void
*/
function formActions()
{
$this->out->submit('submit', _('Subscribe'), 'submit', null, _m('Subscribe to this search'));
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008-2011, StatusNet, Inc.
*
* Search subscription action.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @copyright 2008-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Search unsubscription action
*
* Takes parameters:
*
* - token: session token to prevent CSRF attacks
* - ajax: boolean; whether to return Ajax or full-browser results
*
* Only works if the current user is logged in.
*
* @category Action
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Brion Vibber <brion@status.net>
* @copyright 2008-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class SearchunsubAction extends SearchsubAction
{
/**
* Handle request
*
* Does the subscription and returns results.
*
* @param Array $args unused.
*
* @return void
*/
function handle($args)
{
// Throws exception on error
SearchSub::cancel($this->user->getProfile(),
$this->search);
if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
// TRANS: Page title when search unsubscription succeeded.
$this->element('title', null, _m('Unsubscribed'));
$this->elementEnd('head');
$this->elementStart('body');
$subscribe = new SearchSubForm($this, $this->search);
$subscribe->show();
$this->elementEnd('body');
$this->elementEnd('html');
} else {
$url = common_local_url('search',
array('search' => $this->search));
common_redirect($url, 303);
}
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Form for subscribing to a search
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category SearchSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @copyright 2009-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
/**
* Form for subscribing to a user
*
* @category SearchSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*
* @see UnsubscribeForm
*/
class SearchUnsubForm extends SearchSubForm
{
/**
* ID of the form
*
* @return int ID of the form
*/
function id()
{
return 'search-unsubscribe-' . $this->search;
}
/**
* class of the form
*
* @return string of the form class
*/
function formClass()
{
// class to match existing styles...
return 'form_user_unsubscribe ajax';
}
/**
* Action of the form
*
* @return string URL of the action
*/
function action()
{
return common_local_url('searchunsub', array('search' => $this->search));
}
/**
* Legend of the Form
*
* @return void
*/
function formLegend()
{
$this->out->element('legend', null, _m('Unsubscribe from this search'));
}
/**
* Action elements
*
* @return void
*/
function formActions()
{
$this->out->submit('submit', _('Unsubscribe'), 'submit', null, _m('Unsubscribe from this search'));
}
}

View File

@ -35,6 +35,9 @@ class SubMirrorPlugin extends Plugin
{
$m->connect('settings/mirror',
array('action' => 'mirrorsettings'));
$m->connect('settings/mirror/add/:provider',
array('action' => 'mirrorsettings'),
array('provider' => '[A-Za-z0-9_-]+'));
$m->connect('settings/mirror/add',
array('action' => 'addmirror'));
$m->connect('settings/mirror/edit',

View File

@ -59,11 +59,27 @@ class AddMirrorAction extends BaseMirrorAction
function prepare($args)
{
parent::prepare($args);
$this->feedurl = $this->validateFeedUrl($this->trimmed('feedurl'));
$feedurl = $this->getFeedUrl();
$this->feedurl = $this->validateFeedUrl($feedurl);
$this->profile = $this->profileForFeed($this->feedurl);
return true;
}
function getFeedUrl()
{
$provider = $this->trimmed('provider');
switch ($provider) {
case 'feed':
return $this->trimmed('feedurl');
case 'twitter':
$screenie = $this->trimmed('screen_name');
$base = 'http://api.twitter.com/1/statuses/user_timeline.atom?screen_name=';
return $base . urlencode($screenie);
default:
throw new Exception('Internal form error: unrecognized feed provider.');
}
}
function saveMirror()
{
if ($this->oprofile->subscribe()) {

View File

@ -68,7 +68,7 @@ abstract class BaseMirrorAction extends Action
if (common_valid_http_url($url)) {
return $url;
} else {
$this->clientError(_m("Invalid feed URL."));
$this->clientError(sprintf(_m("Invalid feed URL: %s"), $url));
}
}

View File

@ -65,18 +65,30 @@ class MirrorSettingsAction extends SettingsAction
function showContent()
{
$user = common_current_user();
$provider = $this->trimmed('provider');
if ($provider) {
$this->showAddFeedForm($provider);
} else {
$this->elementStart('div', array('id' => 'add-mirror'));
$this->showAddWizard();
$this->elementEnd('div');
$this->showAddFeedForm();
$mirror = new SubMirror();
$mirror->subscriber = $user->id;
if ($mirror->find()) {
while ($mirror->fetch()) {
$this->showFeedForm($mirror);
$mirror = new SubMirror();
$mirror->subscriber = $user->id;
if ($mirror->find()) {
while ($mirror->fetch()) {
$this->showFeedForm($mirror);
}
}
}
}
function showAddWizard()
{
$form = new AddMirrorWizard($this);
$form->show();
}
function showFeedForm($mirror)
{
$profile = Profile::staticGet('id', $mirror->subscribed);
@ -88,10 +100,47 @@ class MirrorSettingsAction extends SettingsAction
function showAddFeedForm()
{
$form = new AddMirrorForm($this);
switch ($this->arg('provider')) {
case 'statusnet':
break;
case 'twitter':
$form = new AddTwitterMirrorForm($this);
break;
case 'wordpress':
break;
case 'linkedin':
break;
case 'feed':
default:
$form = new AddMirrorForm($this);
}
$form->show();
}
/**
*
* @param array $args
*
* @todo move the ajax display handling to common code
*/
function handle($args)
{
if ($this->boolean('ajax')) {
header('Content-Type: text/html;charset=utf-8');
$this->elementStart('html');
$this->elementStart('head');
$this->element('title', null, _('Provider add'));
$this->elementEnd('head');
$this->elementStart('body');
$this->showAddFeedForm();
$this->elementEnd('body');
$this->elementEnd('html');
} else {
return parent::handle($args);
}
}
/**
* Handle a POST request
*
@ -108,4 +157,16 @@ class MirrorSettingsAction extends SettingsAction
$nav = new SubGroupNav($this, common_current_user());
$nav->show();
}
function showScripts()
{
parent::showScripts();
$this->script('plugins/SubMirror/js/mirrorsettings.js');
}
function showStylesheets()
{
parent::showStylesheets();
$this->cssLink('plugins/SubMirror/css/mirrorsettings.css');
}
}

View File

@ -0,0 +1,26 @@
/* undo insane stuff from core styles */
#add-mirror-wizard img {
display: inline;
}
/* we need #something to override most of the #content crap */
#add-mirror-wizard {
margin-left: 20px;
margin-right: 20px;
}
#add-mirror-wizard .provider-list table {
width: 100%;
}
#add-mirror-wizard .provider-heading img {
vertical-align: middle;
}
#add-mirror-wizard .provider-heading {
cursor: pointer;
}
#add-mirror-wizard .provider-detail fieldset {
margin-top: 8px; /* hack */
margin-bottom: 8px; /* hack */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,47 @@
$(function() {
/**
* Append 'ajax=1' parameter onto URL.
*/
function ajaxize(url) {
if (url.indexOf('?') == '-1') {
return url + '?ajax=1';
} else {
return url + '&ajax=1';
}
}
var addMirror = $('#add-mirror');
var wizard = $('#add-mirror-wizard');
if (wizard.length > 0) {
var list = wizard.find('.provider-list');
var providers = list.find('.provider-heading');
providers.click(function(event) {
console.log(this);
var targetUrl = $(this).find('a').attr('href');
if (targetUrl) {
// Make sure we don't accidentally follow the direct link
event.preventDefault();
var node = this;
function showNew() {
var detail = $('<div class="provider-detail" style="display: none"></div>').insertAfter(node);
detail.load(ajaxize(targetUrl), function(responseText, testStatus, xhr) {
detail.slideDown('fast', function() {
detail.find('input[type="text"]').focus();
});
});
}
var old = addMirror.find('.provider-detail');
if (old.length) {
old.slideUp('fast', function() {
old.remove();
showNew();
});
} else {
showNew();
}
}
});
}
});

View File

@ -49,6 +49,7 @@ class AddMirrorForm extends Form
*/
function formData()
{
$this->out->hidden('provider', 'feed');
$this->out->elementStart('fieldset');
$this->out->elementStart('ul');
@ -67,7 +68,7 @@ class AddMirrorForm extends Form
$this->out->elementEnd('fieldset');
}
private function doInput($id, $name, $label, $value=null, $instructions=null)
protected function doInput($id, $name, $label, $value=null, $instructions=null)
{
$this->out->element('label', array('for' => $id), $label);
$attrs = array('name' => $name,

View File

@ -0,0 +1,187 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package StatusNet
* @copyright 2010-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
class AddMirrorWizard extends Widget
{
/**
* Name of the form
*
* Sub-classes should overload this with the name of their form.
*
* @return void
*/
function formLegend()
{
}
/**
* Visible or invisible data elements
*
* Display the form fields that make up the data of the form.
* Sub-classes should overload this to show their data.
*
* @return void
*/
function show()
{
$this->out->elementStart('div', array('id' => 'add-mirror-wizard'));
$providers = $this->providers();
$this->showProviders($providers);
$this->out->elementEnd('div');
}
function providers()
{
return array(
/*
// We could accept hostname & username combos here, or
// webfingery combinations as for remote users.
array(
'id' => 'statusnet',
'name' => _m('StatusNet'),
),
*/
// Accepts a Twitter username and pulls their user timeline as a
// public Atom feed. Requires a working alternate hub which, one
// hopes, is getting timely updates.
array(
'id' => 'twitter',
'name' => _m('Twitter'),
),
/*
// WordPress was on our list some whiles ago, but not sure
// what we can actually do here. Search on Wordpress.com hosted
// sites, or ?
array(
'id' => 'wordpress',
'name' => _m('WordPress'),
),
*/
/*
// In theory, Facebook lets you pull public updates over RSS,
// but the URLs for your own update feed that I can find from
// 2009-era websites no longer seem to work and there's no
// good current documentation. May not still be available...
// Mirroring from an FB account is probably better done with
// the dedicated plugin. (As of March 2011)
array(
'id' => 'facebook',
'name' => _m('Facebook'),
),
*/
/*
// LinkedIn doesn't currently seem to have public feeds
// for users or groups (March 2011)
array(
'id' => 'linkedin',
'name' => _m('LinkedIn'),
),
*/
array(
'id' => 'feed',
'name' => _m('RSS or Atom feed'),
),
);
}
function showProviders(array $providers)
{
$out = $this->out;
$out->elementStart('div', 'provider-list');
$out->element('h2', null, _m('Select a feed provider'));
$out->elementStart('table');
foreach ($providers as $provider) {
$icon = common_path('plugins/SubMirror/images/providers/' . $provider['id'] . '.png');
$targetUrl = common_local_url('mirrorsettings', array('provider' => $provider['id']));
$out->elementStart('tr', array('class' => 'provider'));
$out->elementStart('td');
$out->elementStart('div', 'provider-heading');
$out->element('img', array('src' => $icon));
$out->element('a', array('href' => $targetUrl), $provider['name']);
$out->elementEnd('div');
$out->elementEnd('td');
$out->elementEnd('tr');
}
$out->elementEnd('table');
$out->elementEnd('div');
}
/**
* Buttons for form actions
*
* Submit and cancel buttons (or whatever)
* Sub-classes should overload this to show their own buttons.
*
* @return void
*/
function formActions()
{
}
/**
* ID of the form
*
* Should be unique on the page. Sub-classes should overload this
* to show their own IDs.
*
* @return string ID of the form
*/
function id()
{
return 'add-mirror-wizard';
}
/**
* Action of the form.
*
* URL to post to. Should be overloaded by subclasses to give
* somewhere to post to.
*
* @return string URL to post to
*/
function action()
{
return common_local_url('addmirror');
}
/**
* Class of the form.
*
* @return string the form's class
*/
function formClass()
{
return 'form_settings';
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package StatusNet
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
class AddTwitterMirrorForm extends AddMirrorForm
{
/**
* Visible or invisible data elements
*
* Display the form fields that make up the data of the form.
* Sub-classes should overload this to show their data.
*
* @return void
*/
function formData()
{
$this->out->hidden('provider', 'twitter');
$this->out->elementStart('fieldset');
$this->out->elementStart('ul');
$this->li();
$this->doInput('addmirror-feedurl',
'screen_name',
_m('Twitter username:'),
$this->out->trimmed('screen_name'));
$this->unli();
$this->li();
$this->out->submit('addmirror-save', _m('BUTTON','Add feed'));
$this->unli();
$this->out->elementEnd('ul');
$this->out->elementEnd('fieldset');
}
}

140
plugins/TagSub/TagSub.php Normal file
View File

@ -0,0 +1,140 @@
<?php
/**
* Data class to store local tag subscriptions
*
* PHP version 5
*
* @category TagSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* For storing the tag subscriptions
*
* @category PollPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* @see DB_DataObject
*/
class TagSub extends Managed_DataObject
{
public $__table = 'tagsub'; // table name
public $tag; // text
public $profile_id; // int -> profile.id
public $created; // datetime
/**
* Get an instance by key
*
* This is a utility method to get a single instance with a given key value.
*
* @param string $k Key to use to lookup (usually 'user_id' for this class)
* @param mixed $v Value to lookup
*
* @return TagSub object found, or null for no hits
*
*/
function staticGet($k, $v=null)
{
return Memcached_DataObject::staticGet('TagSub', $k, $v);
}
/**
* Get an instance by compound key
*
* This is a utility method to get a single instance with a given set of
* key-value pairs. Usually used for the primary key for a compound key; thus
* the name.
*
* @param array $kv array of key-value mappings
*
* @return TagSub object found, or null for no hits
*
*/
function pkeyGet($kv)
{
return Memcached_DataObject::pkeyGet('TagSub', $kv);
}
/**
* The One True Thingy that must be defined and declared.
*/
public static function schemaDef()
{
return array(
'description' => 'TagSubPlugin tag subscription records',
'fields' => array(
'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'hash tag associated with this subscription'),
'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'profile ID of subscribing user'),
'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
),
'primary key' => array('tag', 'profile_id'),
'foreign keys' => array(
'tagsub_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
),
'indexes' => array(
'tagsub_created_idx' => array('created'),
'tagsub_profile_id_tag_idx' => array('profile_id', 'tag'),
),
);
}
/**
* Start a tag subscription!
*
* @param profile $profile subscriber
* @param string $tag subscribee
* @return TagSub
*/
static function start(Profile $profile, $tag)
{
$ts = new TagSub();
$ts->tag = $tag;
$ts->profile_id = $profile->id;
$ts->created = common_sql_now();
$ts->insert();
return $ts;
}
/**
* End a tag subscription!
*
* @param profile $profile subscriber
* @param string $tag subscribee
*/
static function cancel(Profile $profile, $tag)
{
$ts = TagSub::pkeyGet(array('tag' => $tag,
'profile_id' => $profile->id));
if ($ts) {
$ts->delete();
}
}
}

View File

@ -0,0 +1,182 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* A plugin to enable local tab subscription
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category TagSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* TagSub plugin main class
*
* @category TagSubPlugin
* @package StatusNet
* @author Brion Vibber <brionv@status.net>
* @copyright 2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class TagSubPlugin extends Plugin
{
const VERSION = '0.1';
/**
* Database schema setup
*
* @see Schema
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onCheckSchema()
{
$schema = Schema::get();
$schema->ensureTable('tagsub', TagSub::schemaDef());
return true;
}
/**
* Load related modules when needed
*
* @param string $cls Name of the class to be loaded
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onAutoload($cls)
{
$dir = dirname(__FILE__);
switch ($cls)
{
case 'TagSub':
include_once $dir.'/'.$cls.'.php';
return false;
case 'TagsubAction':
case 'TagunsubAction':
case 'TagSubForm':
case 'TagUnsubForm':
include_once $dir.'/'.strtolower($cls).'.php';
return false;
default:
return true;
}
}
/**
* Map URLs to actions
*
* @param Net_URL_Mapper $m path-to-action mapper
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onRouterInitialized($m)
{
$m->connect('tag/:tag/subscribe',
array('action' => 'tagsub'),
array('tag' => Router::REGEX_TAG));
$m->connect('tag/:tag/unsubscribe',
array('action' => 'tagunsub'),
array('tag' => Router::REGEX_TAG));
return true;
}
/**
* Plugin version data
*
* @param array &$versions array of version data
*
* @return value
*/
function onPluginVersion(&$versions)
{
$versions[] = array('name' => 'TagSub',
'version' => self::VERSION,
'author' => 'Brion Vibber',
'homepage' => 'http://status.net/wiki/Plugin:TagSub',
'rawdescription' =>
// TRANS: Plugin description.
_m('Plugin to allow following all messages with a given tag.'));
return true;
}
/**
* Hook inbox delivery setup so tag subscribers receive all
* notices with that tag in their inbox.
*
* Currently makes no distinction between local messages and
* remote ones which happen to come in to the system. Remote
* notices that don't come in at all won't ever reach this.
*
* @param Notice $notice
* @param array $ni in/out map of profile IDs to inbox constants
* @return boolean hook result
*/
function onStartNoticeWhoGets(Notice $notice, array &$ni)
{
foreach ($notice->getTags() as $tag) {
$tagsub = new TagSub();
$tagsub->tag = $tag;
$tagsub->find();
while ($tagsub->fetch()) {
// These constants are currently not actually used, iirc
$ni[$tagsub->profile_id] = NOTICE_INBOX_SOURCE_SUB;
}
}
return true;
}
/**
*
* @param TagAction $action
* @return boolean hook result
*/
function onStartTagShowContent(TagAction $action)
{
$user = common_current_user();
if ($user) {
$tag = $action->trimmed('tag');
$tagsub = TagSub::pkeyGet(array('tag' => $tag,
'profile_id' => $user->id));
if ($tagsub) {
$form = new TagUnsubForm($action, $tag);
} else {
$form = new TagSubForm($action, $tag);
}
$action->elementStart('div', 'entity_actions');
$action->elementStart('ul');
$action->elementStart('li', 'entity_subscribe');
$form->show();
$action->elementEnd('li');
$action->elementEnd('ul');
$action->elementEnd('div');
}
return true;
}
}

View File

@ -0,0 +1,149 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008-2011, StatusNet, Inc.
*
* Tag subscription action.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @copyright 2008-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Tag subscription action
*
* Takes parameters:
*
* - token: session token to prevent CSRF attacks
* - ajax: boolean; whether to return Ajax or full-browser results
*
* Only works if the current user is logged in.
*
* @category Action
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Brion Vibber <brion@status.net>
* @copyright 2008-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class TagsubAction extends Action
{
var $user;
var $tag;
/**
* Check pre-requisites and instantiate attributes
*
* @param Array $args array of arguments (URL, GET, POST)
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
if ($this->boolean('ajax')) {
StatusNet::setApi(true);
}
// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
// TRANS: Client error displayed trying to perform any request method other than POST.
// TRANS: Do not translate POST.
$this->clientError(_('This action only accepts POST requests.'));
return false;
}
// CSRF protection
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
// TRANS: Client error displayed when the session token is not okay.
$this->clientError(_('There was a problem with your session token.'.
' Try again, please.'));
return false;
}
// Only for logged-in users
$this->user = common_current_user();
if (empty($this->user)) {
// TRANS: Client error displayed trying to subscribe when not logged in.
$this->clientError(_('Not logged in.'));
return false;
}
// Profile to subscribe to
$this->tag = $this->arg('tag');
if (empty($this->tag)) {
// TRANS: Client error displayed trying to subscribe to a non-existing profile.
$this->clientError(_('No such profile.'));
return false;
}
return true;
}
/**
* Handle request
*
* Does the subscription and returns results.
*
* @param Array $args unused.
*
* @return void
*/
function handle($args)
{
// Throws exception on error
TagSub::start($this->user->getProfile(),
$this->tag);
if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
// TRANS: Page title when tag subscription succeeded.
$this->element('title', null, _m('Subscribed'));
$this->elementEnd('head');
$this->elementStart('body');
$unsubscribe = new TagUnsubForm($this, $this->tag);
$unsubscribe->show();
$this->elementEnd('body');
$this->elementEnd('html');
} else {
$url = common_local_url('tag',
array('tag' => $this->tag));
common_redirect($url, 303);
}
}
}

View File

@ -0,0 +1,142 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Form for subscribing to a tag
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category TagSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @copyright 2009-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
/**
* Form for subscribing to a user
*
* @category TagSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*
* @see UnsubscribeForm
*/
class TagSubForm extends Form
{
/**
* Name of tag to subscribe to
*/
var $tag = '';
/**
* Constructor
*
* @param HTMLOutputter $out output channel
* @param string $tag name of tag to subscribe to
*/
function __construct($out=null, $tag=null)
{
parent::__construct($out);
$this->tag = $tag;
}
/**
* ID of the form
*
* @return int ID of the form
*/
function id()
{
return 'tag-subscribe-' . $this->tag;
}
/**
* class of the form
*
* @return string of the form class
*/
function formClass()
{
// class to match existing styles...
return 'form_user_subscribe ajax';
}
/**
* Action of the form
*
* @return string URL of the action
*/
function action()
{
return common_local_url('tagsub', array('tag' => $this->tag));
}
/**
* Legend of the Form
*
* @return void
*/
function formLegend()
{
$this->out->element('legend', null, _m('Subscribe to this tag'));
}
/**
* Data elements of the form
*
* @return void
*/
function formData()
{
$this->out->hidden('subscribeto-' . $this->tag,
$this->tag,
'subscribeto');
}
/**
* Action elements
*
* @return void
*/
function formActions()
{
$this->out->submit('submit', _('Subscribe'), 'submit', null, _m('Subscribe to this tag'));
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008-2011, StatusNet, Inc.
*
* Tag subscription action.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @copyright 2008-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Tag unsubscription action
*
* Takes parameters:
*
* - token: session token to prevent CSRF attacks
* - ajax: boolean; whether to return Ajax or full-browser results
*
* Only works if the current user is logged in.
*
* @category Action
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Brion Vibber <brion@status.net>
* @copyright 2008-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class TagunsubAction extends TagsubAction
{
/**
* Handle request
*
* Does the subscription and returns results.
*
* @param Array $args unused.
*
* @return void
*/
function handle($args)
{
// Throws exception on error
TagSub::cancel($this->user->getProfile(),
$this->tag);
if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
// TRANS: Page title when tag unsubscription succeeded.
$this->element('title', null, _m('Unsubscribed'));
$this->elementEnd('head');
$this->elementStart('body');
$subscribe = new TagSubForm($this, $this->tag);
$subscribe->show();
$this->elementEnd('body');
$this->elementEnd('html');
} else {
$url = common_local_url('tag',
array('tag' => $this->tag));
common_redirect($url, 303);
}
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Form for subscribing to a tag
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category TagSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @copyright 2009-2011 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
/**
* Form for subscribing to a user
*
* @category TagSubPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*
* @see UnsubscribeForm
*/
class TagUnsubForm extends TagSubForm
{
/**
* ID of the form
*
* @return int ID of the form
*/
function id()
{
return 'tag-unsubscribe-' . $this->tag;
}
/**
* class of the form
*
* @return string of the form class
*/
function formClass()
{
// class to match existing styles...
return 'form_user_unsubscribe ajax';
}
/**
* Action of the form
*
* @return string URL of the action
*/
function action()
{
return common_local_url('tagunsub', array('tag' => $this->tag));
}
/**
* Legend of the Form
*
* @return void
*/
function formLegend()
{
$this->out->element('legend', null, _m('Unsubscribe from this tag'));
}
/**
* Action elements
*
* @return void
*/
function formActions()
{
$this->out->submit('submit', _('Unsubscribe'), 'submit', null, _m('Unsubscribe from this tag'));
}
}

View File

@ -137,21 +137,46 @@ address {
#core {
clear: both;
margin: 0px;
width: 960px;
width: 958px;
border-top: 5px solid #FB6104;
}
#site_nav_local_views {
display: block;
float: left;
width: 138px;
margin-top: 0px;
padding: 10px;
padding-top: 22px;
background-color: #ececf2;
border-left: 1px solid #d8dae6;
border-right: 1px solid #d8dae6;
min-height: 800px; /* XXX set up equal column heights! */
}
#aside_primary_wrapper {
width: 100%;
float: left;
overflow: hidden;
position: relative;
background-color: #ececf2;
}
#content_wrapper {
width: 100%;
float: left;
position: relative;
right: 239px;
background-color: #fff;
border-right: 1px solid #d8dae6;
}
#site_nav_local_views_wrapper {
width: 100%;
float: left;
position: relative;
right: 561px;
background-color: #ececf2;
border-right: 1px solid #d8dae6;
}
#site_nav_local_views {
width: 138px;
float: left;
overflow: hidden;
position: relative;
left: 800px;
margin-top: 0px;
padding: 22px 10px 40px 10px;
}
#site_nav_local_views H3 {
@ -196,8 +221,12 @@ address {
#content {
width: 520px;
margin-right: 0px;
padding: 20px;
float: left;
overflow: hidden;
position: relative;
left: 801px;
margin: 0px;
padding: 20px 20px 40px 20px;
}
/* Input forms */
@ -357,13 +386,13 @@ address {
#aside_primary {
width: 218px;
padding: 10px;
padding-top: 22px;
float: left;
overflow: hidden;
position: relative;
left: 802px;
padding: 22px 10px 40px 10px;
margin-top: 0px;
background-color: #ececf2;
border-left: 1px solid #d8dae6;
border-right: 1px solid #d8dae6;
min-height: 800px; /* XXX set up equal column heights! */
background: none;
}
#aside_primary .section {
@ -665,6 +694,8 @@ div.entry-content a.response:after {
}
#footer {
position: relative;
top: -6px;
color: #000;
margin-left: 0px;
margin-right: 0px;