diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Shorturl_api.php | 24 | ||||
-rw-r--r-- | lib/api.php | 17 | ||||
-rw-r--r-- | lib/apiprivateauth.php | 82 | ||||
-rw-r--r-- | lib/command.php | 27 | ||||
-rw-r--r-- | lib/commandinterpreter.php | 6 | ||||
-rw-r--r-- | lib/common.php | 3 | ||||
-rw-r--r-- | lib/curlclient.php | 179 | ||||
-rw-r--r-- | lib/default.php | 2 | ||||
-rw-r--r-- | lib/designsettings.php | 13 | ||||
-rw-r--r-- | lib/error.php | 4 | ||||
-rw-r--r-- | lib/grouplist.php | 28 | ||||
-rw-r--r-- | lib/httpclient.php | 213 | ||||
-rw-r--r-- | lib/imagefile.php | 7 | ||||
-rw-r--r-- | lib/language.php | 69 | ||||
-rw-r--r-- | lib/mediafile.php | 5 | ||||
-rw-r--r-- | lib/oauthclient.php | 65 | ||||
-rw-r--r-- | lib/ping.php | 12 | ||||
-rw-r--r-- | lib/profilelist.php | 29 | ||||
-rw-r--r-- | lib/router.php | 71 | ||||
-rw-r--r-- | lib/snapshot.php | 21 | ||||
-rw-r--r-- | lib/userprofile.php | 323 | ||||
-rw-r--r-- | lib/util.php | 73 |
22 files changed, 838 insertions, 435 deletions
diff --git a/lib/Shorturl_api.php b/lib/Shorturl_api.php index 18ae7719b..de4d55012 100644 --- a/lib/Shorturl_api.php +++ b/lib/Shorturl_api.php @@ -41,22 +41,18 @@ abstract class ShortUrlApi return strlen($url) >= common_config('site', 'shorturllength'); } - protected function http_post($data) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $this->service_url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - $response = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - if (($code < 200) || ($code >= 400)) return false; - return $response; + protected function http_post($data) + { + $request = HTTPClient::start(); + $response = $request->post($this->service_url, null, $data); + return $response->getBody(); } - protected function http_get($url) { - $encoded_url = urlencode($url); - return file_get_contents("{$this->service_url}$encoded_url"); + protected function http_get($url) + { + $request = HTTPClient::start(); + $response = $request->get($this->service_url . urlencode($url)); + return $response->getBody(); } protected function tidy($response) { diff --git a/lib/api.php b/lib/api.php index 9bd2083de..a1236ab7e 100644 --- a/lib/api.php +++ b/lib/api.php @@ -134,11 +134,20 @@ class ApiAction extends Action $twitter_user['protected'] = false; # not supported by StatusNet yet $twitter_user['followers_count'] = $profile->subscriberCount(); - // Need to pull up the user for some of this - $user = $profile->getUser(); - $design = $user->getDesign(); $defaultDesign = Design::siteDesign(); - if (!$design) $design = $defaultDesign; + $design = null; + $user = $profile->getUser(); + + // Note: some profiles don't have an associated user + + if (!empty($user)) { + $design = $user->getDesign(); + } + + if (empty($design)) { + $design = $defaultDesign; + } + $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor); $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue(); $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor); diff --git a/lib/apiprivateauth.php b/lib/apiprivateauth.php new file mode 100644 index 000000000..5d0033005 --- /dev/null +++ b/lib/apiprivateauth.php @@ -0,0 +1,82 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Base class for API actions that only require auth when a site + * is configured to be private + * + * 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 API + * @package StatusNet + * @author Adrian Lang <mail@adrianlang.de> + * @author Brenda Wallace <shiny@cpan.org> + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author mEDI <medi@milaro.net> + * @author Sarven Capadisli <csarven@status.net> + * @author Zach Copley <zach@status.net> + * @copyright 2009 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); +} + +require_once INSTALLDIR.'/lib/apiauth.php'; + +/** + * Actions extending this class will require auth only if a site is private + * + * @category API + * @package StatusNet + * @author Adrian Lang <mail@adrianlang.de> + * @author Brenda Wallace <shiny@cpan.org> + * @author Craig Andrews <candrews@integralblue.com> + * @author Dan Moore <dan@moore.cx> + * @author Evan Prodromou <evan@status.net> + * @author mEDI <medi@milaro.net> + * @author Sarven Capadisli <csarven@status.net> + * @author Zach Copley <zach@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/ + */ + +class ApiPrivateAuthAction extends ApiAuthAction +{ + + /** + * Does this API resource require authentication? + * + * @return boolean true or false + */ + + function requiresAuth() + { + // If the site is "private", all API methods except statusnet/config + // need authentication + + if (common_config('site', 'private')) { + return true; + } + + return false; + } + +} diff --git a/lib/command.php b/lib/command.php index 9efa40696..2ec3320de 100644 --- a/lib/command.php +++ b/lib/command.php @@ -579,6 +579,32 @@ class OnCommand extends Command } } +class LoginCommand extends Command +{ + function execute($channel) + { + $login_token = Login_token::staticGet('user_id',$this->user->id); + if($login_token){ + $login_token->delete(); + } + $login_token = new Login_token(); + $login_token->user_id = $this->user->id; + $login_token->token = common_good_rand(16); + $login_token->created = common_sql_now(); + $result = $login_token->insert(); + if (!$result) { + common_log_db_error($login_token, 'INSERT', __FILE__); + $channel->error($this->user, sprintf(_('Could not create login token for %s'), + $this->user->nickname)); + return; + } + $channel->output($this->user, + sprintf(_('This link is useable only once, and is good for only 2 minutes: %s'), + common_local_url('login', + array('user_id'=>$login_token->user_id, 'token'=>$login_token->token)))); + } +} + class HelpCommand extends Command { function execute($channel) @@ -598,6 +624,7 @@ class HelpCommand extends Command "reply #<notice_id> - reply to notice with a given id\n". "reply <nickname> - reply to the last notice from user\n". "join <group> - join group\n". + "login - Get a link to login to the web interface\n". "drop <group> - leave group\n". "stats - get your stats\n". "stop - same as 'off'\n". diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php index b921a17cc..d878fe268 100644 --- a/lib/commandinterpreter.php +++ b/lib/commandinterpreter.php @@ -41,6 +41,12 @@ class CommandInterpreter return null; } return new HelpCommand($user); + case 'login': + if ($arg) { + return null; + } else { + return new LoginCommand($user); + } case 'on': if ($arg) { list($other, $extra) = $this->split_arg($arg); diff --git a/lib/common.php b/lib/common.php index 2c2f6869e..68bdbf229 100644 --- a/lib/common.php +++ b/lib/common.php @@ -169,6 +169,7 @@ if (isset($conffile)) { $_config_files[] = INSTALLDIR.'/config.php'; } +global $_have_a_config; $_have_a_config = false; foreach ($_config_files as $_config_file) { @@ -187,7 +188,7 @@ function _have_config() // XXX: Throw a conniption if database not installed // XXX: Find a way to use htmlwriter for this instead of handcoded markup if (!_have_config()) { - echo '<p>'. _('No configuation file found. ') .'</p>'; + echo '<p>'. _('No configuration file found. ') .'</p>'; echo '<p>'. _('I looked for configuration files in the following places: ') .'<br/> '. implode($_config_files, '<br/>'); echo '<p>'. _('You may wish to run the installer to fix this.') .'</p>'; echo '<a href="install.php">'. _('Go to the installer.') .'</a>'; diff --git a/lib/curlclient.php b/lib/curlclient.php deleted file mode 100644 index c307c2984..000000000 --- a/lib/curlclient.php +++ /dev/null @@ -1,179 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Utility class for wrapping Curl - * - * 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 HTTP - * @package StatusNet - * @author Evan Prodromou <evan@status.net> - * @copyright 2009 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); -} - -define(CURLCLIENT_VERSION, "0.1"); - -/** - * Wrapper for Curl - * - * Makes Curl HTTP client calls within our HTTPClient framework - * - * @category HTTP - * @package StatusNet - * @author Evan Prodromou <evan@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/ - */ - -class CurlClient extends HTTPClient -{ - function __construct() - { - } - - function head($url, $headers=null) - { - $ch = curl_init($url); - - $this->setup($ch); - - curl_setopt_array($ch, - array(CURLOPT_NOBODY => true)); - - if (!is_null($headers)) { - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - } - - $result = curl_exec($ch); - - curl_close($ch); - - return $this->parseResults($result); - } - - function get($url, $headers=null) - { - $ch = curl_init($url); - - $this->setup($ch); - - if (!is_null($headers)) { - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - } - - $result = curl_exec($ch); - - curl_close($ch); - - return $this->parseResults($result); - } - - function post($url, $headers=null, $body=null) - { - $ch = curl_init($url); - - $this->setup($ch); - - curl_setopt($ch, CURLOPT_POST, true); - - if (!is_null($body)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - - if (!is_null($headers)) { - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - } - - $result = curl_exec($ch); - - curl_close($ch); - - return $this->parseResults($result); - } - - function setup($ch) - { - curl_setopt_array($ch, - array(CURLOPT_USERAGENT => $this->userAgent(), - CURLOPT_HEADER => true, - CURLOPT_RETURNTRANSFER => true)); - } - - function userAgent() - { - $version = curl_version(); - return parent::userAgent() . " CurlClient/".CURLCLIENT_VERSION . " cURL/" . $version['version']; - } - - function parseResults($results) - { - $resp = new HTTPResponse(); - - $lines = explode("\r\n", $results); - - if (preg_match("#^HTTP/1.[01] (\d\d\d) .+$#", $lines[0], $match)) { - $resp->code = $match[1]; - } else { - throw Exception("Bad format: initial line is not HTTP status line"); - } - - $lastk = null; - - for ($i = 1; $i < count($lines); $i++) { - $l =& $lines[$i]; - if (mb_strlen($l) == 0) { - $resp->body = implode("\r\n", array_slice($lines, $i + 1)); - break; - } - if (preg_match("#^(\S+):\s+(.*)$#", $l, $match)) { - $k = $match[1]; - $v = $match[2]; - - if (array_key_exists($k, $resp->headers)) { - if (is_array($resp->headers[$k])) { - $resp->headers[$k][] = $v; - } else { - $resp->headers[$k] = array($resp->headers[$k], $v); - } - } else { - $resp->headers[$k] = $v; - } - $lastk = $k; - } else if (preg_match("#^\s+(.*)$#", $l, $match)) { - // continuation line - if (is_null($lastk)) { - throw Exception("Bad format: initial whitespace in headers"); - } - $h =& $resp->headers[$lastk]; - if (is_array($h)) { - $n = count($h); - $h[$n-1] .= $match[1]; - } else { - $h .= $match[1]; - } - } - } - - return $resp; - } -} diff --git a/lib/default.php b/lib/default.php index 7ec8558b0..f6cc4b725 100644 --- a/lib/default.php +++ b/lib/default.php @@ -228,8 +228,6 @@ $default = array('contentlimit' => null), 'message' => array('contentlimit' => null), - 'http' => - array('client' => 'curl'), // XXX: should this be the default? 'location' => array('namespace' => 1), // 1 = geonames, 2 = Yahoo Where on Earth ); diff --git a/lib/designsettings.php b/lib/designsettings.php index 820d534f2..5ce9ddeda 100644 --- a/lib/designsettings.php +++ b/lib/designsettings.php @@ -271,17 +271,20 @@ class DesignSettingsAction extends AccountSettingsAction function handlePost() { - // XXX: Robin's workaround for a bug in PHP where $_POST - // and $_FILE are empty in the case that the uploaded - // file is bigger than PHP is configured to handle. - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - if (empty($_POST) && $_SERVER['CONTENT_LENGTH']) { + // Workaround for PHP returning empty $_POST and $_FILES when POST + // length > post_max_size in php.ini + + if (empty($_FILES) + && empty($_POST) + && ($_SERVER['CONTENT_LENGTH'] > 0) + ) { $msg = _('The server was unable to handle that much POST ' . 'data (%s bytes) due to its current configuration.'); $this->showForm(sprintf($msg, $_SERVER['CONTENT_LENGTH'])); + return; } } diff --git a/lib/error.php b/lib/error.php index 6a9b76be1..3162cfe65 100644 --- a/lib/error.php +++ b/lib/error.php @@ -70,7 +70,7 @@ class ErrorAction extends Action */ function extraHeaders() { - $status_string = $this->status[$this->code]; + $status_string = @self::$status[$this->code]; header('HTTP/1.1 '.$this->code.' '.$status_string); } @@ -92,7 +92,7 @@ class ErrorAction extends Action function title() { - return self::$status[$this->code]; + return @self::$status[$this->code]; } function isReadOnly($args) diff --git a/lib/grouplist.php b/lib/grouplist.php index b41c5b5f8..cc734bdd0 100644 --- a/lib/grouplist.php +++ b/lib/grouplist.php @@ -97,7 +97,7 @@ class GroupList extends Widget $this->out->elementStart('a', array('href' => $this->group->homeUrl(), 'class' => 'url', - 'rel' => 'group')); + 'rel' => 'contact group')); $this->out->element('img', array('src' => $logo, 'class' => 'photo avatar', 'width' => AVATAR_STREAM_SIZE, @@ -105,48 +105,32 @@ class GroupList extends Widget 'alt' => ($this->group->fullname) ? $this->group->fullname : $this->group->nickname)); - $hasFN = ($this->group->fullname) ? 'nickname url uid' : 'fn org nickname url uid'; + $hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname'; $this->out->elementStart('span', $hasFN); $this->out->raw($this->highlight($this->group->nickname)); $this->out->elementEnd('span'); $this->out->elementEnd('a'); if ($this->group->fullname) { - $this->out->elementStart('dl', 'entity_fn'); - $this->out->element('dt', null, 'Full name'); - $this->out->elementStart('dd'); $this->out->elementStart('span', 'fn org'); $this->out->raw($this->highlight($this->group->fullname)); $this->out->elementEnd('span'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); } if ($this->group->location) { - $this->out->elementStart('dl', 'entity_location'); - $this->out->element('dt', null, _('Location')); - $this->out->elementStart('dd', 'label'); + $this->out->elementStart('span', 'label'); $this->out->raw($this->highlight($this->group->location)); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); + $this->out->elementEnd('span'); } if ($this->group->homepage) { - $this->out->elementStart('dl', 'entity_url'); - $this->out->element('dt', null, _('URL')); - $this->out->elementStart('dd'); $this->out->elementStart('a', array('href' => $this->group->homepage, 'class' => 'url')); $this->out->raw($this->highlight($this->group->homepage)); $this->out->elementEnd('a'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); } if ($this->group->description) { - $this->out->elementStart('dl', 'entity_note'); - $this->out->element('dt', null, _('Note')); - $this->out->elementStart('dd', 'note'); + $this->out->elementStart('p', 'note'); $this->out->raw($this->highlight($this->group->description)); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); + $this->out->elementEnd('p'); } # If we're on a list with an owner (subscriptions or subscribers)... diff --git a/lib/httpclient.php b/lib/httpclient.php index f16e31e10..3f8262076 100644 --- a/lib/httpclient.php +++ b/lib/httpclient.php @@ -31,6 +31,9 @@ if (!defined('STATUSNET')) { exit(1); } +require_once 'HTTP/Request2.php'; +require_once 'HTTP/Request2/Response.php'; + /** * Useful structure for HTTP responses * @@ -38,18 +41,53 @@ if (!defined('STATUSNET')) { * ways of doing them. This class hides the specifics of what underlying * library (curl or PHP-HTTP or whatever) that's used. * + * This extends the HTTP_Request2_Response class with methods to get info + * about any followed redirects. + * * @category HTTP - * @package StatusNet - * @author Evan Prodromou <evan@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/ + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @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://status.net/ */ - -class HTTPResponse +class HTTPResponse extends HTTP_Request2_Response { - public $code = null; - public $headers = array(); - public $body = null; + function __construct(HTTP_Request2_Response $response, $url, $redirects=0) + { + foreach (get_object_vars($response) as $key => $val) { + $this->$key = $val; + } + $this->url = strval($url); + $this->redirectCount = intval($redirects); + } + + /** + * Get the count of redirects that have been followed, if any. + * @return int + */ + function getRedirectCount() + { + return $this->redirectCount; + } + + /** + * Gets the final target URL, after any redirects have been followed. + * @return string URL + */ + function getUrl() + { + return $this->url; + } + + /** + * Check if the response is OK, generally a 200 status code. + * @return bool + */ + function isOk() + { + return ($this->getStatus() == 200); + } } /** @@ -59,64 +97,163 @@ class HTTPResponse * ways of doing them. This class hides the specifics of what underlying * library (curl or PHP-HTTP or whatever) that's used. * + * This extends the PEAR HTTP_Request2 package: + * - sends StatusNet-specific User-Agent header + * - 'follow_redirects' config option, defaulting off + * - 'max_redirs' config option, defaulting to 10 + * - extended response class adds getRedirectCount() and getUrl() methods + * - get() and post() convenience functions return body content directly + * * @category HTTP * @package StatusNet * @author Evan Prodromou <evan@status.net> + * @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://status.net/ */ -class HTTPClient +class HTTPClient extends HTTP_Request2 { - static $_client = null; - static function start() + function __construct($url=null, $method=self::METHOD_GET, $config=array()) { - if (!is_null(self::$_client)) { - return self::$_client; - } - - $type = common_config('http', 'client'); - - switch ($type) { - case 'curl': - self::$_client = new CurlClient(); - break; - default: - throw new Exception("Unknown HTTP client type '$type'"); - break; - } - - return self::$_client; + $this->config['max_redirs'] = 10; + $this->config['follow_redirects'] = true; + parent::__construct($url, $method, $config); + $this->setHeader('User-Agent', $this->userAgent()); } - function head($url, $headers) + /** + * Convenience/back-compat instantiator + * @return HTTPClient + */ + public static function start() { - throw new Exception("HEAD method unimplemented"); + return new HTTPClient(); } - function get($url, $headers) + /** + * Convenience function to run a GET request. + * + * @return HTTPResponse + * @throws HTTP_Request2_Exception + */ + public function get($url, $headers=array()) { - throw new Exception("GET method unimplemented"); + return $this->doRequest($url, self::METHOD_GET, $headers); } - function post($url, $headers, $body) + /** + * Convenience function to run a HEAD request. + * + * @return HTTPResponse + * @throws HTTP_Request2_Exception + */ + public function head($url, $headers=array()) { - throw new Exception("POST method unimplemented"); + return $this->doRequest($url, self::METHOD_HEAD, $headers); } - function put($url, $headers, $body) + /** + * Convenience function to POST form data. + * + * @param string $url + * @param array $headers optional associative array of HTTP headers + * @param array $data optional associative array or blob of form data to submit + * @return HTTPResponse + * @throws HTTP_Request2_Exception + */ + public function post($url, $headers=array(), $data=array()) { - throw new Exception("PUT method unimplemented"); + if ($data) { + $this->addPostParameter($data); + } + return $this->doRequest($url, self::METHOD_POST, $headers); } - function delete($url, $headers) + /** + * @return HTTPResponse + * @throws HTTP_Request2_Exception + */ + protected function doRequest($url, $method, $headers) { - throw new Exception("DELETE method unimplemented"); + $this->setUrl($url); + $this->setMethod($method); + if ($headers) { + foreach ($headers as $header) { + $this->setHeader($header); + } + } + $response = $this->send(); + return $response; + } + + protected function log($level, $detail) { + $method = $this->getMethod(); + $url = $this->getUrl(); + common_log($level, __CLASS__ . ": HTTP $method $url - $detail"); } + /** + * Pulls up StatusNet's customized user-agent string, so services + * we hit can track down the responsible software. + * + * @return string + */ function userAgent() { return "StatusNet/".STATUSNET_VERSION." (".STATUSNET_CODENAME.")"; } + + /** + * Actually performs the HTTP request and returns an HTTPResponse object + * with response body and header info. + * + * Wraps around parent send() to add logging and redirection processing. + * + * @return HTTPResponse + * @throw HTTP_Request2_Exception + */ + public function send() + { + $maxRedirs = intval($this->config['max_redirs']); + if (empty($this->config['follow_redirects'])) { + $maxRedirs = 0; + } + $redirs = 0; + do { + try { + $response = parent::send(); + } catch (HTTP_Request2_Exception $e) { + $this->log(LOG_ERR, $e->getMessage()); + throw $e; + } + $code = $response->getStatus(); + if ($code >= 200 && $code < 300) { + $reason = $response->getReasonPhrase(); + $this->log(LOG_INFO, "$code $reason"); + } elseif ($code >= 300 && $code < 400) { + $url = $this->getUrl(); + $target = $response->getHeader('Location'); + + if (++$redirs >= $maxRedirs) { + common_log(LOG_ERR, __CLASS__ . ": Too many redirects: skipping $code redirect from $url to $target"); + break; + } + try { + $this->setUrl($target); + $this->setHeader('Referer', $url); + common_log(LOG_INFO, __CLASS__ . ": Following $code redirect from $url to $target"); + continue; + } catch (HTTP_Request2_Exception $e) { + common_log(LOG_ERR, __CLASS__ . ": Invalid $code redirect from $url to $target"); + } + } else { + $reason = $response->getReasonPhrase(); + $this->log(LOG_ERR, "$code $reason"); + } + break; + } while ($maxRedirs); + return new HTTPResponse($response, $this->getUrl(), $redirs); + } } diff --git a/lib/imagefile.php b/lib/imagefile.php index 88f461481..cf1668f20 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -72,14 +72,19 @@ class ImageFile break; case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: - throw new Exception(sprintf(_('That file is too big. The maximum file size is %d.'), + throw new Exception(sprintf(_('That file is too big. The maximum file size is %s.'), ImageFile::maxFileSize())); return; case UPLOAD_ERR_PARTIAL: @unlink($_FILES[$param]['tmp_name']); throw new Exception(_('Partial upload.')); return; + case UPLOAD_ERR_NO_FILE: + // No file; probably just a non-AJAX submission. + return; default: + common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . + $_FILES[$param]['error']); throw new Exception(_('System error uploading file.')); return; } diff --git a/lib/language.php b/lib/language.php index 7dcb808c9..bec5620fd 100644 --- a/lib/language.php +++ b/lib/language.php @@ -100,38 +100,39 @@ function get_nice_language_list() * @return array mapping of language codes to language info */ function get_all_languages() { - return array( - 'bg' => array('q' => 0.8, 'lang' => 'bg', 'name' => 'Bulgarian', 'direction' => 'ltr'), - 'ca' => array('q' => 0.5, 'lang' => 'ca', 'name' => 'Catalan', 'direction' => 'ltr'), - 'cs' => array('q' => 0.5, 'lang' => 'cs', 'name' => 'Czech', 'direction' => 'ltr'), - 'de' => array('q' => 0.8, 'lang' => 'de', 'name' => 'German', 'direction' => 'ltr'), - 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'), - 'en-us' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'), - 'en-gb' => array('q' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'), - 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'), - 'es' => array('q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'), - 'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'), - 'fr-fr' => array('q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'), - 'ga' => array('q' => 0.5, 'lang' => 'ga', 'name' => 'Galician', 'direction' => 'ltr'), - 'he' => array('q' => 0.5, 'lang' => 'he', 'name' => 'Hebrew', 'direction' => 'rtl'), - 'it' => array('q' => 1, 'lang' => 'it', 'name' => 'Italian', 'direction' => 'ltr'), - 'jp' => array('q' => 0.5, 'lang' => 'ja', 'name' => 'Japanese', 'direction' => 'ltr'), - 'ko' => array('q' => 0.9, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'), - 'mk' => array('q' => 0.5, 'lang' => 'mk', 'name' => 'Macedonian', 'direction' => 'ltr'), - 'nb' => array('q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'), - 'no' => array('q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'), - 'nn' => array('q' => 1, 'lang' => 'nn', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'), - 'nl' => array('q' => 0.5, 'lang' => 'nl', 'name' => 'Dutch', 'direction' => 'ltr'), - 'pl' => array('q' => 0.5, 'lang' => 'pl', 'name' => 'Polish', 'direction' => 'ltr'), - 'pt' => array('q' => 0.1, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'), - 'pt-br' => array('q' => 0.9, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'), - 'ru' => array('q' => 0.9, 'lang' => 'ru', 'name' => 'Russian', 'direction' => 'ltr'), - 'sv' => array('q' => 0.8, 'lang' => 'sv', 'name' => 'Swedish', 'direction' => 'ltr'), - 'te' => array('q' => 0.3, 'lang' => 'te', 'name' => 'Telugu', 'direction' => 'ltr'), - 'tr' => array('q' => 0.5, 'lang' => 'tr', 'name' => 'Turkish', 'direction' => 'ltr'), - 'uk' => array('q' => 1, 'lang' => 'uk', 'name' => 'Ukrainian', 'direction' => 'ltr'), - 'vi' => array('q' => 0.8, 'lang' => 'vi', 'name' => 'Vietnamese', 'direction' => 'ltr'), - 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'), - 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_TW', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'), - ); + return array( + 'bg' => array('q' => 0.8, 'lang' => 'bg', 'name' => 'Bulgarian', 'direction' => 'ltr'), + 'ca' => array('q' => 0.5, 'lang' => 'ca', 'name' => 'Catalan', 'direction' => 'ltr'), + 'cs' => array('q' => 0.5, 'lang' => 'cs', 'name' => 'Czech', 'direction' => 'ltr'), + 'de' => array('q' => 0.8, 'lang' => 'de', 'name' => 'German', 'direction' => 'ltr'), + 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'), + 'en-us' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'), + 'en-gb' => array('q' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'), + 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'), + 'es' => array('q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'), + 'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'), + 'fr-fr' => array('q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'), + 'ga' => array('q' => 0.5, 'lang' => 'ga', 'name' => 'Galician', 'direction' => 'ltr'), + 'he' => array('q' => 0.5, 'lang' => 'he', 'name' => 'Hebrew', 'direction' => 'rtl'), + 'is' => array('q' => 0.1, 'lang' => 'is', 'name' => 'Icelandic', 'direction' => 'ltr'), + 'it' => array('q' => 1, 'lang' => 'it', 'name' => 'Italian', 'direction' => 'ltr'), + 'jp' => array('q' => 0.5, 'lang' => 'ja', 'name' => 'Japanese', 'direction' => 'ltr'), + 'ko' => array('q' => 0.9, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'), + 'mk' => array('q' => 0.5, 'lang' => 'mk', 'name' => 'Macedonian', 'direction' => 'ltr'), + 'nb' => array('q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'), + 'no' => array('q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'), + 'nn' => array('q' => 1, 'lang' => 'nn', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'), + 'nl' => array('q' => 0.5, 'lang' => 'nl', 'name' => 'Dutch', 'direction' => 'ltr'), + 'pl' => array('q' => 0.5, 'lang' => 'pl', 'name' => 'Polish', 'direction' => 'ltr'), + 'pt' => array('q' => 0.1, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'), + 'pt-br' => array('q' => 0.9, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'), + 'ru' => array('q' => 0.9, 'lang' => 'ru', 'name' => 'Russian', 'direction' => 'ltr'), + 'sv' => array('q' => 0.8, 'lang' => 'sv', 'name' => 'Swedish', 'direction' => 'ltr'), + 'te' => array('q' => 0.3, 'lang' => 'te', 'name' => 'Telugu', 'direction' => 'ltr'), + 'tr' => array('q' => 0.5, 'lang' => 'tr', 'name' => 'Turkish', 'direction' => 'ltr'), + 'uk' => array('q' => 1, 'lang' => 'uk', 'name' => 'Ukrainian', 'direction' => 'ltr'), + 'vi' => array('q' => 0.8, 'lang' => 'vi', 'name' => 'Vietnamese', 'direction' => 'ltr'), + 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'), + 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_TW', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'), + ); } diff --git a/lib/mediafile.php b/lib/mediafile.php index d4d184dd0..29d752f0c 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -152,6 +152,9 @@ class MediaFile throw new ClientException(_('The uploaded file was only' . ' partially uploaded.')); return; + case UPLOAD_ERR_NO_FILE: + // No file; probably just a non-AJAX submission. + return; case UPLOAD_ERR_NO_TMP_DIR: throw new ClientException(_('Missing a temporary folder.')); return; @@ -162,6 +165,8 @@ class MediaFile throw new ClientException(_('File upload stopped by extension.')); return; default: + common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . + $_FILES[$param]['error']); throw new ClientException(_('System error uploading file.')); return; } diff --git a/lib/oauthclient.php b/lib/oauthclient.php index f1827726e..1a86e2460 100644 --- a/lib/oauthclient.php +++ b/lib/oauthclient.php @@ -43,7 +43,7 @@ require_once 'OAuth.php'; * @link http://status.net/ * */ -class OAuthClientCurlException extends Exception +class OAuthClientException extends Exception { } @@ -97,9 +97,14 @@ class OAuthClient function getRequestToken($url) { $response = $this->oAuthGet($url); - parse_str($response); - $token = new OAuthToken($oauth_token, $oauth_token_secret); - return $token; + $arr = array(); + parse_str($response, $arr); + if (isset($arr['oauth_token']) && isset($arr['oauth_token_secret'])) { + $token = new OAuthToken($arr['oauth_token'], @$arr['oauth_token_secret']); + return $token; + } else { + throw new OAuthClientException(); + } } /** @@ -177,7 +182,7 @@ class OAuthClient } /** - * Make a HTTP request using cURL. + * Make a HTTP request. * * @param string $url Where to make the * @param array $params post parameters @@ -186,40 +191,32 @@ class OAuthClient */ function httpRequest($url, $params = null) { - $options = array( - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_HEADER => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_USERAGENT => 'StatusNet', - CURLOPT_CONNECTTIMEOUT => 120, - CURLOPT_TIMEOUT => 120, - CURLOPT_HTTPAUTH => CURLAUTH_ANY, - CURLOPT_SSL_VERIFYPEER => false, - - // Twitter is strict about accepting invalid "Expect" headers - - CURLOPT_HTTPHEADER => array('Expect:') - ); + $request = new HTTPClient($url); + $request->setConfig(array( + 'connect_timeout' => 120, + 'timeout' => 120, + 'follow_redirects' => true, + 'ssl_verify_peer' => false, + )); + + // Twitter is strict about accepting invalid "Expect" headers + $request->setHeader('Expect', ''); if (isset($params)) { - $options[CURLOPT_POST] = true; - $options[CURLOPT_POSTFIELDS] = $params; + $request->setMethod(HTTP_Request2::METHOD_POST); + $request->setBody($params); } - $ch = curl_init($url); - curl_setopt_array($ch, $options); - $response = curl_exec($ch); - - if ($response === false) { - $msg = curl_error($ch); - $code = curl_errno($ch); - throw new OAuthClientCurlException($msg, $code); + try { + $response = $request->send(); + $code = $response->getStatus(); + if ($code < 200 || $code >= 400) { + throw new OAuthClientException($response->getBody(), $code); + } + return $response->getBody(); + } catch (Exception $e) { + throw new OAuthClientException($e->getMessage(), $e->getCode()); } - - curl_close($ch); - - return $response; } } diff --git a/lib/ping.php b/lib/ping.php index 175bf8440..5698c4038 100644 --- a/lib/ping.php +++ b/lib/ping.php @@ -44,20 +44,16 @@ function ping_broadcast_notice($notice) { array('nickname' => $profile->nickname)), $tags)); - $context = stream_context_create(array('http' => array('method' => "POST", - 'header' => - "Content-Type: text/xml\r\n". - "User-Agent: StatusNet/".STATUSNET_VERSION."\r\n", - 'content' => $req))); - $file = file_get_contents($notify_url, false, $context); + $request = HTTPClient::start(); + $httpResponse = $request->post($notify_url, array('Content-Type: text/xml'), $req); - if ($file === false || mb_strlen($file) == 0) { + if (!$httpResponse || mb_strlen($httpResponse->getBody()) == 0) { common_log(LOG_WARNING, "XML-RPC empty results for ping ($notify_url, $notice->id) "); continue; } - $response = xmlrpc_decode($file); + $response = xmlrpc_decode($httpResponse->getBody()); if (is_array($response) && xmlrpc_is_fault($response)) { common_log(LOG_WARNING, diff --git a/lib/profilelist.php b/lib/profilelist.php index 5cc211e36..bbb722701 100644 --- a/lib/profilelist.php +++ b/lib/profilelist.php @@ -182,7 +182,8 @@ class ProfileListItem extends Widget { $avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE); $this->out->elementStart('a', array('href' => $this->profile->profileurl, - 'class' => 'url')); + 'class' => 'url', + 'rel' => 'contact')); $this->out->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE), 'class' => 'photo avatar', 'width' => AVATAR_STREAM_SIZE, @@ -190,7 +191,7 @@ class ProfileListItem extends Widget 'alt' => ($this->profile->fullname) ? $this->profile->fullname : $this->profile->nickname)); - $hasFN = ($this->profile->fullname !== '') ? 'nickname' : 'fn nickname'; + $hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname'; $this->out->elementStart('span', $hasFN); $this->out->raw($this->highlight($this->profile->nickname)); $this->out->elementEnd('span'); @@ -200,53 +201,37 @@ class ProfileListItem extends Widget function showFullName() { if (!empty($this->profile->fullname)) { - $this->out->elementStart('dl', 'entity_fn'); - $this->out->element('dt', null, 'Full name'); - $this->out->elementStart('dd'); $this->out->elementStart('span', 'fn'); $this->out->raw($this->highlight($this->profile->fullname)); $this->out->elementEnd('span'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); } } function showLocation() { if (!empty($this->profile->location)) { - $this->out->elementStart('dl', 'entity_location'); - $this->out->element('dt', null, _('Location')); - $this->out->elementStart('dd', 'label'); + $this->out->elementStart('span', 'location'); $this->out->raw($this->highlight($this->profile->location)); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); + $this->out->elementEnd('span'); } } function showHomepage() { if (!empty($this->profile->homepage)) { - $this->out->elementStart('dl', 'entity_url'); - $this->out->element('dt', null, _('URL')); - $this->out->elementStart('dd'); $this->out->elementStart('a', array('href' => $this->profile->homepage, 'class' => 'url')); $this->out->raw($this->highlight($this->profile->homepage)); $this->out->elementEnd('a'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); } } function showBio() { if (!empty($this->profile->bio)) { - $this->out->elementStart('dl', 'entity_note'); - $this->out->element('dt', null, _('Note')); - $this->out->elementStart('dd', 'note'); + $this->out->elementStart('p', 'note'); $this->out->raw($this->highlight($this->profile->bio)); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); + $this->out->elementEnd('p'); } } diff --git a/lib/router.php b/lib/router.php index 64853e419..db9fdb470 100644 --- a/lib/router.php +++ b/lib/router.php @@ -88,6 +88,8 @@ class Router $m->connect('doc/:title', array('action' => 'doc')); + $m->connect('main/login?user_id=:user_id&token=:token', array('action'=>'login'), array('user_id'=> '[0-9]+', 'token'=>'.+')); + // main stuff is repetitive $main = array('login', 'logout', 'register', 'subscribe', @@ -120,7 +122,7 @@ class Router // exceptional $m->connect('main/remote', array('action' => 'remotesubscribe')); - $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '[A-Za-z0-9_-]+')); + $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '['.NICKNAME_FMT.']+')); foreach (Router::$bare as $action) { $m->connect('index.php?action=' . $action, array('action' => $action)); @@ -164,10 +166,10 @@ class Router $m->connect('notice/new', array('action' => 'newnotice')); $m->connect('notice/new?replyto=:replyto', array('action' => 'newnotice'), - array('replyto' => '[A-Za-z0-9_-]+')); + array('replyto' => '['.NICKNAME_FMT.']+')); $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto', array('action' => 'newnotice'), - array('replyto' => '[A-Za-z0-9_-]+'), + array('replyto' => '['.NICKNAME_FMT.']+'), array('inreplyto' => '[0-9]+')); $m->connect('notice/:notice/file', @@ -191,7 +193,7 @@ class Router array('id' => '[0-9]+')); $m->connect('message/new', array('action' => 'newmessage')); - $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => '[A-Za-z0-9_-]+')); + $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => '['.NICKNAME_FMT.']+')); $m->connect('message/:message', array('action' => 'showmessage'), array('message' => '[0-9]+')); @@ -275,7 +277,7 @@ class Router $m->connect('api/statuses/friends_timeline/:id.:format', array('action' => 'ApiTimelineFriends', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json|rss|atom)')); $m->connect('api/statuses/home_timeline.:format', array('action' => 'ApiTimelineFriends', @@ -283,7 +285,7 @@ class Router $m->connect('api/statuses/home_timeline/:id.:format', array('action' => 'ApiTimelineFriends', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json|rss|atom)')); $m->connect('api/statuses/user_timeline.:format', @@ -292,7 +294,7 @@ class Router $m->connect('api/statuses/user_timeline/:id.:format', array('action' => 'ApiTimelineUser', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json|rss|atom)')); $m->connect('api/statuses/mentions.:format', @@ -301,7 +303,7 @@ class Router $m->connect('api/statuses/mentions/:id.:format', array('action' => 'ApiTimelineMentions', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json|rss|atom)')); $m->connect('api/statuses/replies.:format', @@ -310,7 +312,7 @@ class Router $m->connect('api/statuses/replies/:id.:format', array('action' => 'ApiTimelineMentions', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json|rss|atom)')); $m->connect('api/statuses/friends.:format', @@ -319,7 +321,7 @@ class Router $m->connect('api/statuses/friends/:id.:format', array('action' => 'ApiUserFriends', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); $m->connect('api/statuses/followers.:format', @@ -328,7 +330,7 @@ class Router $m->connect('api/statuses/followers/:id.:format', array('action' => 'ApiUserFollowers', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); $m->connect('api/statuses/show.:format', @@ -357,14 +359,9 @@ class Router $m->connect('api/users/show/:id.:format', array('action' => 'ApiUserShow', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); - $m->connect('api/users/:method', - array('action' => 'api', - 'apiaction' => 'users'), - array('method' => 'show(\.(xml|json))?')); - // direct messages $m->connect('api/direct_messages.:format', @@ -400,12 +397,12 @@ class Router $m->connect('api/friendships/create/:id.:format', array('action' => 'ApiFriendshipsCreate', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); $m->connect('api/friendships/destroy/:id.:format', array('action' => 'ApiFriendshipsDestroy', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); // Social graph @@ -431,6 +428,9 @@ class Router $m->connect('api/account/verify_credentials.:format', array('action' => 'ApiAccountVerifyCredentials')); + $m->connect('api/account/update_profile_image.:format', + array('action' => 'ApiAccountUpdateProfileImage')); + // special case where verify_credentials is called w/out a format $m->connect('api/account/verify_credentials', @@ -447,35 +447,28 @@ class Router $m->connect('api/favorites/:id.:format', array('action' => 'ApiTimelineFavorites', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xmljson|rss|atom)')); $m->connect('api/favorites/create/:id.:format', array('action' => 'ApiFavoriteCreate', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); $m->connect('api/favorites/destroy/:id.:format', array('action' => 'ApiFavoriteDestroy', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); - - // notifications - - $m->connect('api/notifications/:method/:argument', - array('action' => 'api', - 'apiaction' => 'favorites')); - // blocks $m->connect('api/blocks/create/:id.:format', array('action' => 'ApiBlockCreate', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); $m->connect('api/blocks/destroy/:id.:format', array('action' => 'ApiBlockDestroy', - 'id' => '[a-zA-Z0-9]+', + 'id' => '['.NICKNAME_FMT.']+', 'format' => '(xml|json)')); // help @@ -591,14 +584,14 @@ class Router 'replies', 'inbox', 'outbox', 'microsummary') as $a) { $m->connect(':nickname/'.$a, array('action' => $a), - array('nickname' => '[a-zA-Z0-9]{1,64}')); + array('nickname' => '['.NICKNAME_FMT.']{1,64}')); } foreach (array('subscriptions', 'subscribers') as $a) { $m->connect(':nickname/'.$a.'/:tag', array('action' => $a), array('tag' => '[a-zA-Z0-9]+', - 'nickname' => '[a-zA-Z0-9]{1,64}')); + 'nickname' => '['.NICKNAME_FMT.']{1,64}')); } foreach (array('rss', 'groups') as $a) { @@ -610,31 +603,31 @@ class Router foreach (array('all', 'replies', 'favorites') as $a) { $m->connect(':nickname/'.$a.'/rss', array('action' => $a.'rss'), - array('nickname' => '[a-zA-Z0-9]{1,64}')); + array('nickname' => '['.NICKNAME_FMT.']{1,64}')); } $m->connect(':nickname/favorites', array('action' => 'showfavorites'), - array('nickname' => '[a-zA-Z0-9]{1,64}')); + array('nickname' => '['.NICKNAME_FMT.']{1,64}')); $m->connect(':nickname/avatar/:size', array('action' => 'avatarbynickname'), array('size' => '(original|96|48|24)', - 'nickname' => '[a-zA-Z0-9]{1,64}')); + 'nickname' => '['.NICKNAME_FMT.']{1,64}')); $m->connect(':nickname/tag/:tag/rss', array('action' => 'userrss'), - array('nickname' => '[a-zA-Z0-9]{1,64}'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}'), array('tag' => '[a-zA-Z0-9]+')); $m->connect(':nickname/tag/:tag', array('action' => 'showstream'), - array('nickname' => '[a-zA-Z0-9]{1,64}'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}'), array('tag' => '[a-zA-Z0-9]+')); $m->connect(':nickname', array('action' => 'showstream'), - array('nickname' => '[a-zA-Z0-9]{1,64}')); + array('nickname' => '['.NICKNAME_FMT.']{1,64}')); Event::handle('RouterInitialized', array($m)); } diff --git a/lib/snapshot.php b/lib/snapshot.php index ede846e5b..2a10c6b93 100644 --- a/lib/snapshot.php +++ b/lib/snapshot.php @@ -172,26 +172,9 @@ class Snapshot { // XXX: Use OICU2 and OAuth to make authorized requests - $postdata = http_build_query($this->stats); - - $opts = - array('http' => - array( - 'method' => 'POST', - 'header' => 'Content-type: '. - 'application/x-www-form-urlencoded', - 'content' => $postdata, - 'user_agent' => 'StatusNet/'.STATUSNET_VERSION - ) - ); - - $context = stream_context_create($opts); - $reporturl = common_config('snapshot', 'reporturl'); - - $result = @file_get_contents($reporturl, false, $context); - - return $result; + $request = HTTPClient::start(); + $request->post($reporturl, null, $this->stats); } /** diff --git a/lib/userprofile.php b/lib/userprofile.php new file mode 100644 index 000000000..ca1b38c8b --- /dev/null +++ b/lib/userprofile.php @@ -0,0 +1,323 @@ +<?php +/** + * StatusNet, the distributed open-source microblogging tool + * + * Profile for a particular user + * + * 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 Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @author Sarven Capadisli <csarven@status.net> + * @copyright 2008 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); +} + +require_once INSTALLDIR.'/lib/widget.php'; + +/** + * Profile of a user + * + * Shows profile information about a particular user + * + * @category Output + * @package StatusNet + * @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 HTMLOutputter + */ + +class UserProfile extends Widget +{ + var $user = null; + var $profile = null; + + function __construct($action=null, $user=null, $profile=null) + { + parent::__construct($action); + $this->user = $user; + $this->profile = $profile; + } + + function show() + { + $this->showProfileData(); + $this->showEntityActions(); + } + + function showProfileData() + { + if (Event::handle('StartProfilePageProfileSection', array(&$this, $this->profile))) { + + $this->out->elementStart('div', 'entity_profile vcard author'); + $this->out->element('h2', null, _('User profile')); + + if (Event::handle('StartProfilePageProfileElements', array(&$this, $this->profile))) { + + $this->showAvatar(); + $this->showNickname(); + $this->showFullName(); + $this->showLocation(); + $this->showHomepage(); + $this->showBio(); + $this->showProfileTags(); + + Event::handle('EndProfilePageProfileElements', array(&$this, $this->profile)); + } + + $this->out->elementEnd('div'); + Event::handle('EndProfilePageProfileSection', array(&$this, $this->profile)); + } + } + + function showAvatar() + { + if (Event::handle('StartProfilePageAvatar', array($this, $this->profile))) { + + $avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + + $this->out->elementStart('dl', 'entity_depiction'); + $this->out->element('dt', null, _('Photo')); + $this->out->elementStart('dd'); + $this->out->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE), + 'class' => 'photo avatar', + 'width' => AVATAR_PROFILE_SIZE, + 'height' => AVATAR_PROFILE_SIZE, + 'alt' => $this->profile->nickname)); + $this->out->elementEnd('dd'); + + $user = User::staticGet('id', $this->profile->id); + + $cur = common_current_user(); + if ($cur && $cur->id == $user->id) { + $this->out->elementStart('dd'); + $this->out->element('a', array('href' => common_local_url('avatarsettings')), _('Edit Avatar')); + $this->out->elementEnd('dd'); + } + + $this->out->elementEnd('dl'); + + Event::handle('EndProfilePageAvatar', array($this, $this->profile)); + } + } + + function showNickname() + { + if (Event::handle('StartProfilePageNickname', array($this, $this->profile))) { + + $this->out->elementStart('dl', 'entity_nickname'); + $this->out->element('dt', null, _('Nickname')); + $this->out->elementStart('dd'); + $hasFN = ($this->profile->fullname) ? 'nickname url uid' : 'fn nickname url uid'; + $this->out->element('a', array('href' => $this->profile->profileurl, + 'rel' => 'me', 'class' => $hasFN), + $this->profile->nickname); + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + + Event::handle('EndProfilePageNickname', array($this, $this->profile)); + } + } + + function showFullName() + { + if (Event::handle('StartProfilePageFullName', array($this, $this->profile))) { + if ($this->profile->fullname) { + $this->out->elementStart('dl', 'entity_fn'); + $this->out->element('dt', null, _('Full name')); + $this->out->elementStart('dd'); + $this->out->element('span', 'fn', $this->profile->fullname); + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageFullName', array($this, $this->profile)); + } + } + + function showLocation() + { + if (Event::handle('StartProfilePageLocation', array($this, $this->profile))) { + if ($this->profile->location) { + $this->out->elementStart('dl', 'entity_location'); + $this->out->element('dt', null, _('Location')); + $this->out->element('dd', 'label', $this->profile->location); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageLocation', array($this, $this->profile)); + } + } + + function showHomepage() + { + if (Event::handle('StartProfilePageHomepage', array($this, $this->profile))) { + if ($this->profile->homepage) { + $this->out->elementStart('dl', 'entity_url'); + $this->out->element('dt', null, _('URL')); + $this->out->elementStart('dd'); + $this->out->element('a', array('href' => $this->profile->homepage, + 'rel' => 'me', 'class' => 'url'), + $this->profile->homepage); + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageHomepage', array($this, $this->profile)); + } + } + + function showBio() + { + if (Event::handle('StartProfilePageBio', array($this, $this->profile))) { + if ($this->profile->bio) { + $this->out->elementStart('dl', 'entity_note'); + $this->out->element('dt', null, _('Note')); + $this->out->element('dd', 'note', $this->profile->bio); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageBio', array($this, $this->profile)); + } + } + + function showProfileTags() + { + if (Event::handle('StartProfilePageProfileTags', array($this, $this->profile))) { + $tags = Profile_tag::getTags($this->profile->id, $this->profile->id); + + if (count($tags) > 0) { + $this->out->elementStart('dl', 'entity_tags'); + $this->out->element('dt', null, _('Tags')); + $this->out->elementStart('dd'); + $this->out->elementStart('ul', 'tags xoxo'); + foreach ($tags as $tag) { + $this->out->elementStart('li'); + // Avoid space by using raw output. + $pt = '<span class="mark_hash">#</span><a rel="tag" href="' . + common_local_url('peopletag', array('tag' => $tag)) . + '">' . $tag . '</a>'; + $this->out->raw($pt); + $this->out->elementEnd('li'); + } + $this->out->elementEnd('ul'); + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } + Event::handle('EndProfilePageProfileTags', array($this, $this->profile)); + } + } + + function showEntityActions() + { + if (Event::handle('StartProfilePageActionsSection', array(&$this, $this->profile))) { + + $cur = common_current_user(); + + $this->out->elementStart('div', 'entity_actions'); + $this->out->element('h2', null, _('User actions')); + $this->out->elementStart('ul'); + + if (Event::handle('StartProfilePageActionsElements', array(&$this, $this->profile))) { + if (empty($cur)) { // not logged in + $this->out->elementStart('li', 'entity_subscribe'); + $this->showRemoteSubscribeLink(); + $this->out->elementEnd('li'); + } else { + if ($cur->id == $this->profile->id) { // your own page + $this->out->elementStart('li', 'entity_edit'); + $this->out->element('a', array('href' => common_local_url('profilesettings'), + 'title' => _('Edit profile settings')), + _('Edit')); + $this->out->elementEnd('li'); + } else { // someone else's page + + // subscribe/unsubscribe button + + $this->out->elementStart('li', 'entity_subscribe'); + + if ($cur->isSubscribed($this->profile)) { + $usf = new UnsubscribeForm($this->out, $this->profile); + $usf->show(); + } else { + $sf = new SubscribeForm($this->out, $this->profile); + $sf->show(); + } + $this->out->elementEnd('li'); + + if ($cur->mutuallySubscribed($this->user)) { + + // message + + $this->out->elementStart('li', 'entity_send-a-message'); + $this->out->element('a', array('href' => common_local_url('newmessage', array('to' => $this->user->id)), + 'title' => _('Send a direct message to this user')), + _('Message')); + $this->out->elementEnd('li'); + + // nudge + + if ($this->user->email && $this->user->emailnotifynudge) { + $this->out->elementStart('li', 'entity_nudge'); + $nf = new NudgeForm($this->out, $this->user); + $nf->show(); + $this->out->elementEnd('li'); + } + } + + // block/unblock + + $blocked = $cur->hasBlocked($this->profile); + $this->out->elementStart('li', 'entity_block'); + if ($blocked) { + $ubf = new UnblockForm($this->out, $this->profile, + array('action' => 'showstream', + 'nickname' => $this->profile->nickname)); + $ubf->show(); + } else { + $bf = new BlockForm($this->out, $this->profile, + array('action' => 'showstream', + 'nickname' => $this->profile->nickname)); + $bf->show(); + } + $this->out->elementEnd('li'); + } + } + + Event::handle('EndProfilePageActionsElements', array(&$this, $this->profile)); + } + + $this->out->elementEnd('ul'); + $this->out->elementEnd('div'); + + Event::handle('EndProfilePageActionsSection', array(&$this, $this->profile)); + } + } + + function showRemoteSubscribeLink() + { + $url = common_local_url('remotesubscribe', + array('nickname' => $this->profile->nickname)); + $this->out->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _('Subscribe')); + } +} diff --git a/lib/util.php b/lib/util.php index d159c583e..a4865c46c 100644 --- a/lib/util.php +++ b/lib/util.php @@ -119,16 +119,44 @@ function common_munge_password($password, $id) // check if a username exists and has matching password function common_check_user($nickname, $password) { - // NEVER allow blank passwords, even if they match the DB - if (mb_strlen($password) == 0) { - return false; - } + $authenticated = false; + $eventResult = Event::handle('CheckPassword', array($nickname, $password, &$authenticated)); $user = User::staticGet('nickname', $nickname); if (is_null($user) || $user === false) { - return false; + //user does not exist + if($authenticated){ + //a handler said these are valid credentials, so see if a plugin wants to auto register the user + if(Event::handle('AutoRegister', array($nickname))){ + //no handler registered the user + return false; + }else{ + $user = User::staticGet('nickname', $nickname); + if (is_null($user) || $user === false) { + common_log(LOG_WARNING, "A plugin handled the AutoRegister event, but did not actually register the user, nickname: $nickname"); + return false; + }else{ + return $user; + } + } + }else{ + //no handler indicated the credentials were valid, and we know their not valid because the user isn't in the database + return false; + } } else { - if (0 == strcmp(common_munge_password($password, $user->id), - $user->password)) { + if($eventResult && ! $authenticated){ + //no handler was authoritative + if (mb_strlen($password) == 0) { + // NEVER allow blank passwords, even if they match the DB + return false; + }else{ + if (0 == strcmp(common_munge_password($password, $user->id), + $user->password)) { + //internal checking passed + $authenticated = true; + } + } + } + if($authenticated){ return $user; } else { return false; @@ -422,7 +450,7 @@ function common_render_text($text) function common_replace_urls_callback($text, $callback, $notice_id = null) { // Start off with a regex $regex = '#'. - '(?:^|[\s\(\)\[\]\{\}\\\'\\\";]+)(?![\@\!\#])'. + '(?:^|[\s\<\>\(\)\[\]\{\}\\\'\\\";]+)(?![\@\!\#])'. '('. '(?:'. '(?:'. //Known protocols @@ -452,9 +480,9 @@ function common_replace_urls_callback($text, $callback, $notice_id = null) { ')'. '(?:'. '(?:\:\d+)?'. //:port - '(?:/[\pN\pL$\[\]\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\"@]*)?'. // /path - '(?:\?[\pN\pL\$\[\]\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\"@\/]*)?'. // ?query string - '(?:\#[\pN\pL$\[\]\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\"\@/\?\#]*)?'. // #fragment + '(?:/[\pN\pL$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'@]*)?'. // /path + '(?:\?[\pN\pL\$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'@\/]*)?'. // ?query string + '(?:\#[\pN\pL$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\@/\?\#]*)?'. // #fragment ')(?<![\?\.\,\#\,])'. ')'. '#ixu'; @@ -480,6 +508,10 @@ function callback_helper($matches, $callback, $notice_id) { array( 'left'=>'{', 'right'=>'}' + ), + array( + 'left'=>'<', + 'right'=>'>' ) ); $cannotEndWith=array('.','?',',','#'); @@ -1366,9 +1398,28 @@ function common_memcache() } } +function common_license_terms($uri) +{ + if(preg_match('/creativecommons.org\/licenses\/([^\/]+)/', $uri, $matches)) { + return explode('-',$matches[1]); + } + return array($uri); +} + function common_compatible_license($from, $to) { + $from_terms = common_license_terms($from); + // public domain and cc-by are compatible with everything + if(count($from_terms) == 1 && ($from_terms[0] == 'publicdomain' || $from_terms[0] == 'by')) { + return true; + } + $to_terms = common_license_terms($to); + // sa is compatible across versions. IANAL + if(in_array('sa',$from_terms) || in_array('sa',$to_terms)) { + return count(array_diff($from_terms, $to_terms)) == 0; + } // XXX: better compatibility check needed here! + // Should at least normalise URIs return ($from == $to); } |