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

This commit is contained in:
Evan Prodromou 2009-11-10 17:08:59 -05:00
commit 1455d60b06
26 changed files with 821 additions and 339 deletions

View File

@ -491,11 +491,13 @@ EndCheckPassword: After checking a username/password pair
- $password: The password that was checked
- $authenticatedUser: User object if credentials match a user, else null.
ChangePassword: Handle a password change request
StartChangePassword: Before changing a password
- $nickname: user's nickname
- $oldpassword: the user's old password
- $newpassword: the desired new password
- &$errormsg: set this to an error message if the password could not be changed. If the password was changed, leave this as false
EndChangePassword: After changing a password
- $nickname: user's nickname
CanUserChangeField: Determines if a user is allowed to change a specific profile field
- $nickname: nickname of the user who would like to know which of their profile fields are mutable

31
README
View File

@ -389,20 +389,16 @@ the server first.
Sphinx
------
To use a Sphinx server to search users and notices, you also need
to install, compile and enable the sphinx pecl extension for php on the
client side, which itself depends on the sphinx development files.
"pecl install sphinx" should take care of that. Add "extension=sphinx.so"
to your php.ini and reload apache to enable it.
To use a Sphinx server to search users and notices, you'll need to
enable the SphinxSearch plugin. Add to your config.php:
You can update your MySQL or Postgresql databases to drop their fulltext
search indexes, since they're now provided by sphinx.
addPlugin('SphinxSearch');
$config['sphinx']['server'] = 'searchhost.local';
On the sphinx server side, a script reads the main database and build
the keyword index. A cron job reads the database and keeps the sphinx
indexes up to date. scripts/sphinx-cron.sh should be called by cron
every 5 minutes, for example. scripts/sphinx.sh is an init.d script
to start and stop the sphinx search daemon.
You also need to install, compile and enable the sphinx pecl extension for
php on the client side, which itself depends on the sphinx development files.
See plugins/SphinxSearch/README for more details and server setup.
SMS
---
@ -1168,17 +1164,6 @@ base: memcached uses key-value pairs to store data. We build long,
StatusNet site using your memcached server.
port: Port to connect to; defaults to 11211.
sphinx
------
You can get a significant boost in performance using Sphinx Search
instead of your database server to search for users and notices.
<http://sphinxsearch.com/>.
enabled: Set to true to enable. Default false.
server: a string with the hostname of the sphinx server.
port: an integer with the port number of the sphinx server.
emailpost
---------

View File

@ -104,7 +104,7 @@ class NoticesearchAction extends SearchAction
{
$notice = new Notice();
$search_engine = $notice->getSearchEngine('identica_notices');
$search_engine = $notice->getSearchEngine('notice');
$search_engine->set_sort_mode('chron');
// Ask for an extra to see if there's more.
$search_engine->limit((($page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1);

View File

@ -62,7 +62,7 @@ class NoticesearchrssAction extends Rss10Action
$notice = new Notice();
$search_engine = $notice->getSearchEngine('identica_notices');
$search_engine = $notice->getSearchEngine('notice');
$search_engine->set_sort_mode('chron');
if (!$limit) $limit = 20;

View File

@ -58,19 +58,6 @@ class PasswordsettingsAction extends AccountSettingsAction
return _('Change password');
}
function prepare($args){
parent::prepare($args);
$user = common_current_user();
Event::handle('CanUserChangeField', array($user->nickname, 'password'));
if(! $fields['password']){
//user is not allowed to change his password
$this->clientError(_('You are not allowed to change your password'));
}
}
/**
* Instructions for use
*
@ -182,8 +169,8 @@ class PasswordsettingsAction extends AccountSettingsAction
$oldpassword = null;
}
$errormsg = false;
if(! Event::handle('ChangePassword', array($user->nickname, $oldpassword, $newpassword, &$errormsg))){
$success = false;
if(! Event::handle('StartChangePassword', array($user->nickname, $oldpassword, $newpassword))){
//no handler changed the password, so change the password internally
$original = clone($user);
@ -199,11 +186,9 @@ class PasswordsettingsAction extends AccountSettingsAction
$this->serverError(_('Can\'t save new password.'));
return;
}
Event::handle('EndChangePassword', array($nickname));
}
if($errormsg === false)
$this->showForm(_('Password saved.'), true);
else
$this->showForm($errormsg);
$this->showForm(_('Password saved.'), true);
}
}

View File

@ -61,7 +61,7 @@ class PeoplesearchAction extends SearchAction
function showResults($q, $page)
{
$profile = new Profile();
$search_engine = $profile->getSearchEngine('identica_people');
$search_engine = $profile->getSearchEngine('profile');
$search_engine->set_sort_mode('chron');
// Ask for an extra to see if there's more.
$search_engine->limit((($page-1)*PROFILES_PER_PAGE), PROFILES_PER_PAGE + 1);

View File

@ -161,7 +161,7 @@ class TwitapisearchatomAction extends ApiAction
// lcase it for comparison
$q = strtolower($this->query);
$search_engine = $notice->getSearchEngine('identica_notices');
$search_engine = $notice->getSearchEngine('notice');
$search_engine->set_sort_mode('chron');
$search_engine->limit(($this->page - 1) * $this->rpp,
$this->rpp + 1, true);

View File

@ -121,7 +121,7 @@ class TwitapisearchjsonAction extends ApiAction
// lcase it for comparison
$q = strtolower($this->query);
$search_engine = $notice->getSearchEngine('identica_notices');
$search_engine = $notice->getSearchEngine('notice');
$search_engine->set_sort_mode('chron');
$search_engine->limit(($this->page - 1) * $this->rpp, $this->rpp + 1, true);
if (false === $search_engine->query($q)) {

View File

@ -184,27 +184,20 @@ class Memcached_DataObject extends DB_DataObject
require_once INSTALLDIR.'/lib/search_engines.php';
static $search_engine;
if (!isset($search_engine)) {
$connected = false;
if (common_config('sphinx', 'enabled')) {
$search_engine = new SphinxSearch($this, $table);
$connected = $search_engine->is_connected();
}
// unable to connect to sphinx' search daemon
if (!$connected) {
if ('mysql' === common_config('db', 'type')) {
$type = common_config('search', 'type');
if ($type == 'like') {
$search_engine = new MySQLLikeSearch($this, $table);
} else if ($type == 'fulltext') {
$search_engine = new MySQLSearch($this, $table);
} else {
throw new ServerException('Unknown search type: ' . $type);
}
if (Event::handle('GetSearchEngine', array($this, $table, &$search_engine))) {
if ('mysql' === common_config('db', 'type')) {
$type = common_config('search', 'type');
if ($type == 'like') {
$search_engine = new MySQLLikeSearch($this, $table);
} else if ($type == 'fulltext') {
$search_engine = new MySQLSearch($this, $table);
} else {
$search_engine = new PGSearch($this, $table);
throw new ServerException('Unknown search type: ' . $type);
}
} else {
$search_engine = new PGSearch($this, $table);
}
}
}
return $search_engine;
}

View File

@ -57,14 +57,16 @@ class Status_network extends DB_DataObject
$config['db']['ini_'.$dbname] = INSTALLDIR.'/classes/status_network.ini';
$config['db']['table_status_network'] = $dbname;
self::$cache = new Memcache();
if (class_exists('Memcache')) {
self::$cache = new Memcache();
if (is_array($servers)) {
foreach($servers as $server) {
self::$cache->addServer($server);
if (is_array($servers)) {
foreach($servers as $server) {
self::$cache->addServer($server);
}
} else {
self::$cache->addServer($servers);
}
} else {
self::$cache->addServer($servers);
}
self::$base = $dbname;
@ -76,6 +78,10 @@ class Status_network extends DB_DataObject
static function memGet($k, $v)
{
if (!self::$cache) {
return self::staticGet($k, $v);
}
$ck = self::cacheKey($k, $v);
$sn = self::$cache->get($ck);
@ -92,10 +98,12 @@ class Status_network extends DB_DataObject
function decache()
{
$keys = array('nickname', 'hostname', 'pathname');
foreach ($keys as $k) {
$ck = self::cacheKey($k, $this->$k);
self::$cache->delete($ck);
if (self::$cache) {
$keys = array('nickname', 'hostname', 'pathname');
foreach ($keys as $k) {
$ck = self::cacheKey($k, $this->$k);
self::$cache->delete($ck);
}
}
}

View File

@ -125,10 +125,6 @@ $default =
'public' => array()), # JIDs of users who want to receive the public stream
'invite' =>
array('enabled' => true),
'sphinx' =>
array('enabled' => false,
'server' => 'localhost',
'port' => 3312),
'tag' =>
array('dropoff' => 864000.0),
'popular' =>

View File

@ -46,70 +46,11 @@ class SearchEngine
}
}
class SphinxSearch extends SearchEngine
{
private $sphinx;
private $connected;
function __construct($target, $table)
{
$fp = @fsockopen(common_config('sphinx', 'server'), common_config('sphinx', 'port'));
if (!$fp) {
$this->connected = false;
return;
}
fclose($fp);
parent::__construct($target, $table);
$this->sphinx = new SphinxClient;
$this->sphinx->setServer(common_config('sphinx', 'server'), common_config('sphinx', 'port'));
$this->connected = true;
}
function is_connected()
{
return $this->connected;
}
function limit($offset, $count, $rss = false)
{
//FIXME without LARGEST_POSSIBLE, the most recent results aren't returned
// this probably has a large impact on performance
$LARGEST_POSSIBLE = 1e6;
if ($rss) {
$this->sphinx->setLimits($offset, $count, $count, $LARGEST_POSSIBLE);
}
else {
// return at most 50 pages of results
$this->sphinx->setLimits($offset, $count, 50 * ($count - 1), $LARGEST_POSSIBLE);
}
return $this->target->limit(0, $count);
}
function query($q)
{
$result = $this->sphinx->query($q, $this->table);
if (!isset($result['matches'])) return false;
$id_set = join(', ', array_keys($result['matches']));
$this->target->whereAdd("id in ($id_set)");
return true;
}
function set_sort_mode($mode)
{
if ('chron' === $mode) {
$this->sphinx->SetSortMode(SPH_SORT_ATTR_DESC, 'created_ts');
return $this->target->orderBy('created desc');
}
}
}
class MySQLSearch extends SearchEngine
{
function query($q)
{
if ('identica_people' === $this->table) {
if ('profile' === $this->table) {
$this->target->whereAdd('MATCH(nickname, fullname, location, bio, homepage) ' .
'AGAINST (\''.addslashes($q).'\' IN BOOLEAN MODE)');
if (strtolower($q) != $q) {
@ -117,7 +58,7 @@ class MySQLSearch extends SearchEngine
'AGAINST (\''.addslashes(strtolower($q)).'\' IN BOOLEAN MODE)', 'OR');
}
return true;
} else if ('identica_notices' === $this->table) {
} else if ('notice' === $this->table) {
// Don't show imported notices
$this->target->whereAdd('notice.is_local != ' . Notice::GATEWAY);
@ -143,13 +84,13 @@ class MySQLLikeSearch extends SearchEngine
{
function query($q)
{
if ('identica_people' === $this->table) {
if ('profile' === $this->table) {
$qry = sprintf('(nickname LIKE "%%%1$s%%" OR '.
' fullname LIKE "%%%1$s%%" OR '.
' location LIKE "%%%1$s%%" OR '.
' bio LIKE "%%%1$s%%" OR '.
' homepage LIKE "%%%1$s%%")', addslashes($q));
} else if ('identica_notices' === $this->table) {
} else if ('notice' === $this->table) {
$qry = sprintf('content LIKE "%%%1$s%%"', addslashes($q));
} else {
throw new ServerException('Unknown table: ' . $this->table);
@ -165,9 +106,9 @@ class PGSearch extends SearchEngine
{
function query($q)
{
if ('identica_people' === $this->table) {
if ('profile' === $this->table) {
return $this->target->whereAdd('textsearch @@ plainto_tsquery(\''.addslashes($q).'\')');
} else if ('identica_notices' === $this->table) {
} else if ('notice' === $this->table) {
// XXX: We need to filter out gateway notices (notice.is_local = -2) --Zach

145
plugins/Auth/AuthPlugin.php Normal file
View File

@ -0,0 +1,145 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Superclass for plugins that do authentication and/or authorization
*
* 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 Plugin
* @package StatusNet
* @author Craig Andrews <candrews@integralblue.com>
* @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);
}
/**
* Superclass for plugins that do authentication
*
* @category Plugin
* @package StatusNet
* @author Craig Andrews <candrews@integralblue.com>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
abstract class AuthPlugin extends Plugin
{
//is this plugin authoritative for authentication?
protected $authn_authoritative = false;
//should accounts be automatically created after a successful login attempt?
protected $autoregistration = false;
//------------Auth plugin should implement some (or all) of these methods------------\\
/**
* Check if a nickname/password combination is valid
* @param nickname
* @param password
* @return boolean true if the credentials are valid, false if they are invalid.
*/
function checkPassword($nickname, $password)
{
return false;
}
/**
* Automatically register a user when they attempt to login with valid credentials.
* User::register($data) is a very useful method for this implementation
* @param nickname
* @return boolean true if the user was created, false if autoregistration is not allowed, null if this plugin is not responsible for this nickname
*/
function autoRegister($nickname)
{
return null;
}
/**
* Change a user's password
* The old password has been verified to be valid by this plugin before this call is made
* @param nickname
* @param oldpassword
* @param newpassword
* @return boolean true if the password was changed, false if password changing failed for some reason, null if this plugin is not responsible for this nickname
*/
function changePassword($nickname,$oldpassword,$newpassword)
{
return null;
}
/**
* Can a user change this field in his own profile?
* @param nickname
* @param field
* @return boolean true if the field can be changed, false if not allowed to change it, null if this plugin is not responsible for this nickname
*/
function canUserChangeField($nickname, $field)
{
return null;
}
//------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\
function __construct()
{
parent::__construct();
}
function StartCheckPassword($nickname, $password, &$authenticatedUser){
$authenticated = $this->checkPassword($nickname, $password);
if($authenticated){
$authenticatedUser = User::staticGet('nickname', $nickname);
if(!$authenticatedUser && $this->autoregistration){
if($this->autoregister($nickname)){
$authenticatedUser = User::staticGet('nickname', $nickname);
}
}
return false;
}else{
if($this->authn_authoritative){
return false;
}
}
//we're not authoritative, so let other handlers try
}
function onStartChangePassword($nickname,$oldpassword,$newpassword)
{
$authenticated = $this->checkPassword($nickname, $oldpassword);
if($authenticated){
$result = $this->changePassword($nickname,$oldpassword,$newpassword);
if($result){
//stop handling of other handlers, because what was requested was done
return false;
}else{
throw new Exception(_('Password changing failed'));
}
}else{
if($this->authn_authoritative){
//since we're authoritative, no other plugin could do this
throw new Exception(_('Password changing failed'));
}else{
//let another handler try
return null;
}
}
}
}

View File

@ -31,38 +31,42 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
require_once INSTALLDIR.'/plugins/Ldap/ldap.php';
require_once INSTALLDIR.'/plugins/Auth/AuthPlugin.php';
require_once 'Net/LDAP2.php';
class LdapPlugin extends Plugin
class LdapPlugin extends AuthPlugin
{
private $config = array();
function __construct()
{
parent::__construct();
}
//---interface implementation---//
function onCheckPassword($nickname, $password, &$authenticated)
function checkPassword($nickname, $password)
{
if(ldap_check_password($nickname, $password)){
$authenticated = true;
//stop handling of other events, because we have an answer
$ldap = $this->ldap_get_connection();
if(!$ldap){
return false;
}
if(common_config('ldap','authoritative')){
//a false return stops handler processing
$entry = $this->ldap_get_user($nickname);
if(!$entry){
return false;
}else{
$config = $this->ldap_get_config();
$config['binddn']=$entry->dn();
$config['bindpw']=$password;
if($this->ldap_get_connection($config)){
return true;
}else{
return false;
}
}
}
function onAutoRegister($nickname)
function autoRegister($nickname)
{
$user = User::staticGet('nickname', $nickname);
if (! is_null($user) && $user !== false) {
common_log(LOG_WARNING, "An attempt was made to autoregister an existing user with nickname: $nickname");
return;
}
$attributes=array();
$config_attributes = array('nickname','email','fullname','homepage','location');
foreach($config_attributes as $config_attribute){
@ -71,7 +75,7 @@ class LdapPlugin extends Plugin
array_push($attributes,$value);
}
}
$entry = ldap_get_user($nickname,$attributes);
$entry = $this->ldap_get_user($nickname,$attributes);
if($entry){
$registration_data = array();
foreach($config_attributes as $config_attribute){
@ -89,21 +93,22 @@ class LdapPlugin extends Plugin
//set the database saved password to a random string.
$registration_data['password']=common_good_rand(16);
$user = User::register($registration_data);
//prevent other handlers from running, as we have registered the user
return false;
return true;
}else{
//user isn't in ldap, so we cannot register him
return null;
}
}
function onChangePassword($nickname,$oldpassword,$newpassword,&$errormsg)
function changePassword($nickname,$oldpassword,$newpassword)
{
//TODO implement this
$errormsg = _('Sorry, changing LDAP passwords is not supported at this time');
throw new Exception(_('Sorry, changing LDAP passwords is not supported at this time'));
//return false, indicating that the event has been handled
return false;
}
function onCanUserChangeField($nickname, $field)
function canUserChangeField($nickname, $field)
{
switch($field)
{
@ -113,4 +118,67 @@ class LdapPlugin extends Plugin
return false;
}
}
//---utility functions---//
function ldap_get_config(){
$config = array();
$keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','filter','scope');
foreach($keys as $key){
$value = $this->$key;
if($value!==false){
$config[$key]=$value;
}
}
return $config;
}
function ldap_get_connection($config = null){
if($config == null){
$config = $this->ldap_get_config();
}
//cannot use Net_LDAP2::connect() as StatusNet uses
//PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
//PEAR handling can be overridden on instance objects, so we do that.
$ldap = new Net_LDAP2($config);
$ldap->setErrorHandling(PEAR_ERROR_RETURN);
$err=$ldap->bind();
if (Net_LDAP2::isError($err)) {
common_log(LOG_WARNING, 'Could not connect to LDAP server: '.$err->getMessage());
return false;
}
return $ldap;
}
/**
* get an LDAP entry for a user with a given username
*
* @param string $username
* $param array $attributes LDAP attributes to retrieve
* @return string DN
*/
function ldap_get_user($username,$attributes=array()){
$ldap = $this->ldap_get_connection();
$filter = Net_LDAP2_Filter::create(common_config('ldap','nickname_attribute'), 'equals', $username);
$options = array(
'scope' => 'sub',
'attributes' => $attributes
);
$search = $ldap->search(null,$filter,$options);
if (PEAR::isError($search)) {
common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage());
return false;
}
if($search->count()==0){
return false;
}else if($search->count()==1){
$entry = $search->shiftEntry();
return $entry;
}else{
common_log(LOG_WARNING, 'Found ' . $search->count() . ' ldap user with the username: ' . $username);
return false;
}
}
}

View File

@ -2,22 +2,46 @@ The LDAP plugin allows for StatusNet to handle authentication, authorization, an
Installation
============
Add configuration entries to config.php. These entries are:
add "addPlugin('ldap', array('setting'=>'value', 'setting2'=>'value2', ...);" to the bottom of your config.php
The following are documented at http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
$config['ldap']['binddn']
$config['ldap']['bindpw']
$config['ldap']['basedn']
$config['ldap']['host']
$config['ldap']['nickname_attribute'] Set this to the name of the ldap attribute that holds the username. For example, on Microsoft's Active Directory, this should be set to 'sAMAccountName'
$config['ldap']['nickname_email'] Set this to the name of the ldap attribute that holds the user's email address. For example, on Microsoft's Active Directory, this should be set to 'mail'
$config['ldap']['nickname_fullname'] Set this to the name of the ldap attribute that holds the user's full name. For example, on Microsoft's Active Directory, this should be set to 'displayName'
$config['ldap']['nickname_homepage'] Set this to the name of the ldap attribute that holds the the url of the user's home page.
$config['ldap']['nickname_location'] Set this to the name of the ldap attribute that holds the user's location.
$config['ldap']['authoritative'] Set to true if LDAP's responses are authoritative (meaning if LDAP fails, do check the any other plugins or the internal password database)
$config['ldap']['autoregister'] Set to true if users should be automatically created when they attempt to login
Settings
========
authn_authoritative: Set to true if LDAP's responses are authoritative (meaning if LDAP fails, do check the any other plugins or the internal password database).
autoregistration: Set to true if users should be automatically created when they attempt to login.
Finally, add "addPlugin('ldap');" to the bottom of your config.php
host*: LDAP server name to connect to. You can provide several hosts in an array in which case the hosts are tried from left to right.. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
port: Port on the server. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
version: LDAP version. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
starttls: TLS is started after connecting. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
binddn: The distinguished name to bind as (username). See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
bindpw: Password for the binddn. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
basedn*: LDAP base name (root directory). See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
options: See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
filter: Default search filter. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
scope: Default search scope. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php
attributes: an array with the key being the StatusNet user attribute name, and the value the LDAP attribute name
nickname*
email
fullname
homepage
location
* required
Example
=======
Here's an example of an LDAP plugin configuration that connects to Microsoft Active Directory.
addPlugin('ldap', array(
'binddn'=>'username',
'bindpw'=>'password',
'basedn'=>'OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc',
'host'=>array('server1', 'server2'),
'attributes'=>array(
'nickname'=>'sAMAccountName',
'email'=>'mail',
'fullname'=>'displayName')
));

View File

@ -1,108 +0,0 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008, 2009, 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') && !defined('LACONICA')) { exit(1); }
require_once 'Net/LDAP2.php';
function ldap_get_config(){
static $config = null;
if($config == null){
$config = array();
$keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','scope');
foreach($keys as $key){
$value = common_config('ldap', $key);
if($value!==false){
$config[$key]=$value;
}
}
}
return $config;
}
function ldap_get_connection($config = null){
if($config == null){
$config = ldap_get_config();
}
//cannot use Net_LDAP2::connect() as StatusNet uses
//PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError');
//PEAR handling can be overridden on instance objects, so we do that.
$ldap = new Net_LDAP2($config);
$ldap->setErrorHandling(PEAR_ERROR_RETURN);
$err=$ldap->bind();
if (Net_LDAP2::isError($err)) {
common_log(LOG_WARNING, 'Could not connect to LDAP server: '.$err->getMessage());
return false;
}
return $ldap;
}
function ldap_check_password($username, $password){
$ldap = ldap_get_connection();
if(!$ldap){
return false;
}
$entry = ldap_get_user($username);
if(!$entry){
return false;
}else{
$config = ldap_get_config();
$config['binddn']=$entry->dn();
$config['bindpw']=$password;
if(ldap_get_connection($config)){
return true;
}else{
return false;
}
}
}
/**
* get an LDAP entry for a user with a given username
*
* @param string $username
* $param array $attributes LDAP attributes to retrieve
* @return string DN
*/
function ldap_get_user($username,$attributes=array()){
$ldap = ldap_get_connection();
$filter = Net_LDAP2_Filter::create(common_config('ldap','nickname_attribute'), 'equals', $username);
$options = array(
'scope' => 'sub',
'attributes' => $attributes
);
$search = $ldap->search(null,$filter,$options);
if (PEAR::isError($search)) {
common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage());
return false;
}
if($search->count()==0){
return false;
}else if($search->count()==1){
$entry = $search->shiftEntry();
return $entry;
}else{
common_log(LOG_WARNING, 'Found ' . $search->count() . ' ldap user with the username: ' . $username);
return false;
}
}

View File

@ -0,0 +1,45 @@
You can get a significant boost in performance using Sphinx Search
instead of your database server to search for users and notices.
<http://sphinxsearch.com/>.
Configuration
-------------
In StatusNet's configuration, you can adjust the following settings
under 'sphinx':
enabled: Set to true to enable. Default false.
server: a string with the hostname of the sphinx server.
port: an integer with the port number of the sphinx server.
Requirements
------------
To use a Sphinx server to search users and notices, you also need
to install, compile and enable the sphinx pecl extension for php on the
client side, which itself depends on the sphinx development files.
"pecl install sphinx" should take care of that. Add "extension=sphinx.so"
to your php.ini and reload apache to enable it.
You can update your MySQL or Postgresql databases to drop their fulltext
search indexes, since they're now provided by sphinx.
You will also need a Sphinx server to serve the search queries.
On the sphinx server side, a script reads the main database and build
the keyword index. A cron job reads the database and keeps the sphinx
indexes up to date. scripts/sphinx-cron.sh should be called by cron
every 5 minutes, for example. scripts/sphinx.sh is an init.d script
to start and stop the sphinx search daemon.
Server configuration
--------------------
scripts/gen_config.php can generate a sphinx.conf file listing MySQL
data sources for your databases. You may need to tweak paths afterwards.
$ plugins/SphinxSearch/scripts/gen_config.php > sphinx.conf
If you wish, you can build a full config yourself based on sphinx.conf.sample

View File

@ -0,0 +1,100 @@
<?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/>.
*
* @category Plugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('STATUSNET')) {
exit(1);
}
// Set defaults if not already set in the config array...
global $config;
$sphinxDefaults =
array('enabled' => true,
'server' => 'localhost',
'port' => 3312);
foreach($sphinxDefaults as $key => $val) {
if (!isset($config['sphinx'][$key])) {
$config['sphinx'][$key] = $val;
}
}
/**
* Plugin for Sphinx search backend.
*
* @category Plugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
* @link http://twitter.com/
*/
class SphinxSearchPlugin extends Plugin
{
/**
* Automatically load any classes used
*
* @param string $cls the class
* @return boolean hook return
*/
function onAutoload($cls)
{
switch ($cls) {
case 'SphinxSearch':
include_once INSTALLDIR . '/plugins/SphinxSearch/' .
strtolower($cls) . '.php';
return false;
default:
return true;
}
}
/**
* Create sphinx search engine object for the given table type.
*
* @param Memcached_DataObject $target
* @param string $table
* @param out &$search_engine SearchEngine object on output if successful
* @ return boolean hook return
*/
function onGetSearchEngine(Memcached_DataObject $target, $table, &$search_engine)
{
if (common_config('sphinx', 'enabled')) {
if (!class_exists('SphinxClient')) {
throw new ServerException('Sphinx PHP extension must be installed.');
}
$engine = new SphinxSearch($target, $table);
if ($engine->is_connected()) {
$search_engine = $engine;
return false;
}
}
// Sphinx disabled or disconnected
return true;
}
}

View File

@ -0,0 +1,126 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2009, 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/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$longoptions = array('base=', 'network');
$helptext = <<<END_OF_TRIM_HELP
Generates sphinx.conf file based on StatusNet configuration.
--base Base dir to Sphinx install
(default /usr/local)
--network Use status_network global config table
(non-functional at present)
END_OF_TRIM_HELP;
require_once INSTALLDIR . '/scripts/commandline.inc';
require dirname(__FILE__) . '/sphinx-utils.php';
$timestamp = date('r');
print <<<END
#
# Sphinx configuration for StatusNet
# Generated {$timestamp}
#
END;
sphinx_iterate_sites('sphinx_site_template');
print <<<END
indexer
{
mem_limit = 300M
}
searchd
{
port = 3312
log = {$base}/log/searchd.log
query_log = {$base}/log/query.log
read_timeout = 5
max_children = 30
pid_file = {$base}/log/searchd.pid
max_matches = 1000
seamless_rotate = 1
preopen_indexes = 0
unlink_old = 1
}
END;
/**
* Build config entries for a single site
* @fixme we only seem to have master DB currently available...
*/
function sphinx_site_template($sn)
{
return
sphinx_template($sn,
'profile',
'SELECT id, UNIX_TIMESTAMP(created) as created_ts, nickname, fullname, location, bio, homepage FROM profile',
'SELECT * FROM profile where id = $id') .
sphinx_template($sn,
'notice',
'SELECT id, UNIX_TIMESTAMP(created) as created_ts, content FROM notice',
'SELECT * FROM notice where notice.id = $id AND notice.is_local != -2');
}
function sphinx_template($sn, $table, $query, $query_info)
{
$base = sphinx_base();
$dbtype = common_config('db', 'type');
print <<<END
#
# {$sn->sitename}
#
source {$sn->dbname}_src_{$table}
{
type = {$dbtype}
sql_host = {$sn->dbhost}
sql_user = {$sn->dbuser}
sql_pass = {$sn->dbpass}
sql_db = {$sn->dbname}
sql_query_pre = SET NAMES utf8;
sql_query = {$query}
sql_query_info = {$query_info}
sql_attr_timestamp = created_ts
}
index {$sn->dbname}_{$table}
{
source = {$sn->dbname}_src_{$table}
path = {$base}/data/{$sn->dbname}_{$table}
docinfo = extern
charset_type = utf-8
min_word_len = 3
}
END;
}

View File

@ -0,0 +1,61 @@
#!/usr/bin/env php
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2009, 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/>.
*/
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
$longoptions = array('base=', 'network');
$helptext = <<<END_OF_TRIM_HELP
Runs Sphinx search indexer.
--rotate Have Sphinx run index update in background and
rotate updated indexes into place as they finish.
--base Base dir to Sphinx install
(default /usr/local)
--network Use status_network global config table for site list
(non-functional at present)
END_OF_TRIM_HELP;
require_once INSTALLDIR . '/scripts/commandline.inc';
require dirname(__FILE__) . '/sphinx-utils.php';
sphinx_iterate_sites('sphinx_index_update');
function sphinx_index_update($sn)
{
$base = sphinx_base();
$baseIndexes = array('notice', 'profile');
$params = array();
if (have_option('rotate')) {
$params[] = '--rotate';
}
foreach ($baseIndexes as $index) {
$params[] = "{$sn->dbname}_{$index}";
}
$params = implode(' ', $params);
$cmd = "$base/bin/indexer --config $base/etc/sphinx.conf $params";
print "$cmd\n";
system($cmd);
}

View File

@ -0,0 +1,63 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2009, 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/>.
*/
function sphinx_use_network()
{
return have_option('network');
}
function sphinx_base()
{
if (have_option('base')) {
return get_option_value('base');
} else {
return "/usr/local/sphinx";
}
}
function sphinx_iterate_sites($callback)
{
if (sphinx_use_network()) {
// @fixme this should use, like, some kind of config
Status_network::setupDB('localhost', 'statusnet', 'statuspass', 'statusnet');
$sn = new Status_network();
if (!$sn->find()) {
die("Confused... no sites in status_network table or lookup failed.\n");
}
while ($sn->fetch()) {
$callback($sn);
}
} else {
if (preg_match('!^(mysqli?|pgsql)://(.*?):(.*?)@(.*?)/(.*?)$!',
common_config('db', 'database'), $matches)) {
list(/*all*/, $dbtype, $dbuser, $dbpass, $dbhost, $dbname) = $matches;
$sn = (object)array(
'sitename' => common_config('site', 'name'),
'dbhost' => $dbhost,
'dbuser' => $dbuser,
'dbpass' => $dbpass,
'dbname' => $dbname);
$callback($sn);
} else {
print "Unrecognized database configuration string in config.php\n";
exit(1);
}
}
}

View File

@ -0,0 +1,96 @@
<?php
/*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008, 2009, 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);
}
class SphinxSearch extends SearchEngine
{
private $sphinx;
private $connected;
function __construct($target, $table)
{
$fp = @fsockopen(common_config('sphinx', 'server'), common_config('sphinx', 'port'));
if (!$fp) {
$this->connected = false;
return;
}
fclose($fp);
parent::__construct($target, $table);
$this->sphinx = new SphinxClient;
$this->sphinx->setServer(common_config('sphinx', 'server'), common_config('sphinx', 'port'));
$this->connected = true;
}
function is_connected()
{
return $this->connected;
}
function limit($offset, $count, $rss = false)
{
//FIXME without LARGEST_POSSIBLE, the most recent results aren't returned
// this probably has a large impact on performance
$LARGEST_POSSIBLE = 1e6;
if ($rss) {
$this->sphinx->setLimits($offset, $count, $count, $LARGEST_POSSIBLE);
}
else {
// return at most 50 pages of results
$this->sphinx->setLimits($offset, $count, 50 * ($count - 1), $LARGEST_POSSIBLE);
}
return $this->target->limit(0, $count);
}
function query($q)
{
$result = $this->sphinx->query($q, $this->remote_table());
if (!isset($result['matches'])) return false;
$id_set = join(', ', array_keys($result['matches']));
$this->target->whereAdd("id in ($id_set)");
return true;
}
function set_sort_mode($mode)
{
if ('chron' === $mode) {
$this->sphinx->SetSortMode(SPH_SORT_ATTR_DESC, 'created_ts');
return $this->target->orderBy('created desc');
}
}
function remote_table()
{
return $this->dbname() . '_' . $this->table;
}
function dbname()
{
// @fixme there should be a less dreadful way to do this.
// DB objects won't give database back until they connect, it's confusing
if (preg_match('!^.*?://.*?:.*?@.*?/(.*?)$!', common_config('db', 'database'), $matches)) {
return $matches[1];
}
throw new ServerException("Sphinx search could not identify database name");
}
}

View File

@ -1,24 +0,0 @@
#!/bin/sh
# StatusNet - a distributed open-source microblogging tool
# Copyright (C) 2008, 2009, 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/>.
# This program tries to start the daemons for StatusNet.
# Note that the 'maildaemon' needs to run as a mail filter.
/usr/local/bin/indexer --config /usr/local/etc/sphinx.conf --all --rotate

View File

@ -1,24 +0,0 @@
#!/bin/sh
# StatusNet - a distributed open-source microblogging tool
# Copyright (C) 2008, 2009, 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/>.
# This program tries to start the daemons for StatusNet.
# Note that the 'maildaemon' needs to run as a mail filter.
/usr/local/bin/indexer --config /usr/local/etc/sphinx.conf --all