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
50 changed files with 4214 additions and 412 deletions

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'));
}
}