forked from GNUsocial/gnu-social
f7d488d4b2
If a user does a notice search that should return no matching notices, the RSS feed / API results for that search currently returns all notices instead of no notices. This fixes it so that an empty list is returned instead.
383 lines
10 KiB
PHP
383 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Laconica, the distributed open-source microblogging tool
|
|
*
|
|
* Action for showing Twitter-like Atom search results
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* LICENCE: This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* @category Search
|
|
* @package Laconica
|
|
* @author Zach Copley <zach@controlyourself.ca>
|
|
* @copyright 2008-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('LACONICA')) {
|
|
exit(1);
|
|
}
|
|
|
|
require_once INSTALLDIR.'/lib/twitterapi.php';
|
|
|
|
/**
|
|
* Action for outputting search results in Twitter compatible Atom
|
|
* format.
|
|
*
|
|
* TODO: abstract Atom stuff into a ruseable base class like
|
|
* RSS10Action.
|
|
*
|
|
* @category Search
|
|
* @package Laconica
|
|
* @author Zach Copley <zach@controlyourself.ca>
|
|
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
|
* @link http://laconi.ca/
|
|
*
|
|
* @see TwitterapiAction
|
|
*/
|
|
|
|
class TwitapisearchatomAction extends TwitterapiAction
|
|
{
|
|
|
|
var $cnt;
|
|
var $query;
|
|
var $lang;
|
|
var $rpp;
|
|
var $page;
|
|
var $since_id;
|
|
var $geocode;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* Just wraps the Action constructor.
|
|
*
|
|
* @param string $output URI to output to, default = stdout
|
|
* @param boolean $indent Whether to indent output, default true
|
|
*
|
|
* @see Action::__construct
|
|
*/
|
|
|
|
function __construct($output='php://output', $indent=true)
|
|
{
|
|
parent::__construct($output, $indent);
|
|
}
|
|
|
|
/**
|
|
* Do we need to write to the database?
|
|
*
|
|
* @return boolean true
|
|
*/
|
|
|
|
function isReadonly()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Read arguments and initialize members
|
|
*
|
|
* @param array $args Arguments from $_REQUEST
|
|
*
|
|
* @return boolean success
|
|
*
|
|
*/
|
|
|
|
function prepare($args)
|
|
{
|
|
parent::prepare($args);
|
|
|
|
$this->query = $this->trimmed('q');
|
|
$this->lang = $this->trimmed('lang');
|
|
$this->rpp = $this->trimmed('rpp');
|
|
|
|
if (!$this->rpp) {
|
|
$this->rpp = 15;
|
|
}
|
|
|
|
if ($this->rpp > 100) {
|
|
$this->rpp = 100;
|
|
}
|
|
|
|
$this->page = $this->trimmed('page');
|
|
|
|
if (!$this->page) {
|
|
$this->page = 1;
|
|
}
|
|
|
|
// TODO: Suppport since_id -- we need to tweak the backend
|
|
// Search classes to support it.
|
|
|
|
$this->since_id = $this->trimmed('since_id');
|
|
$this->geocode = $this->trimmed('geocode');
|
|
|
|
// TODO: Also, language and geocode
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handle a request
|
|
*
|
|
* @param array $args Arguments from $_REQUEST
|
|
*
|
|
* @return void
|
|
*/
|
|
|
|
function handle($args)
|
|
{
|
|
parent::handle($args);
|
|
$this->showAtom();
|
|
}
|
|
|
|
/**
|
|
* Get the notices to output as results. This also sets some class
|
|
* attrs so we can use them to calculate pagination, and output
|
|
* since_id and max_id.
|
|
*
|
|
* @return array an array of Notice objects sorted in reverse chron
|
|
*/
|
|
|
|
function getNotices()
|
|
{
|
|
// TODO: Support search operators like from: and to:, boolean, etc.
|
|
|
|
$notices = array();
|
|
$notice = new Notice();
|
|
|
|
// lcase it for comparison
|
|
$q = strtolower($this->query);
|
|
|
|
$search_engine = $notice->getSearchEngine('identica_notices');
|
|
$search_engine->set_sort_mode('chron');
|
|
$search_engine->limit(($this->page - 1) * $this->rpp,
|
|
$this->rpp + 1, true);
|
|
if (false === $search_engine->query($q)) {
|
|
$this->cnt = 0;
|
|
} else {
|
|
$this->cnt = $notice->find();
|
|
}
|
|
|
|
$cnt = 0;
|
|
|
|
if ($this->cnt > 0) {
|
|
while ($notice->fetch()) {
|
|
|
|
++$cnt;
|
|
|
|
if (!$this->max_id) {
|
|
$this->max_id = $notice->id;
|
|
}
|
|
|
|
if ($cnt > $this->rpp) {
|
|
break;
|
|
}
|
|
|
|
$notices[] = clone($notice);
|
|
}
|
|
}
|
|
|
|
return $notices;
|
|
}
|
|
|
|
/**
|
|
* Output search results as an Atom feed
|
|
*
|
|
* @return void
|
|
*/
|
|
|
|
function showAtom()
|
|
{
|
|
$notices = $this->getNotices();
|
|
|
|
$this->initAtom();
|
|
$this->showFeed();
|
|
|
|
foreach ($notices as $n) {
|
|
$this->showEntry($n);
|
|
}
|
|
|
|
$this->endAtom();
|
|
}
|
|
|
|
/**
|
|
* Show feed specific Atom elements
|
|
*
|
|
* @return void
|
|
*/
|
|
|
|
function showFeed()
|
|
{
|
|
// TODO: A9 OpenSearch stuff like search.twitter.com?
|
|
|
|
$server = common_config('site', 'server');
|
|
$sitename = common_config('site', 'name');
|
|
|
|
// XXX: Use xmlns:laconica instead?
|
|
|
|
$this->elementStart('feed',
|
|
array('xmlns' => 'http://www.w3.org/2005/Atom',
|
|
|
|
// XXX: xmlns:twitter causes Atom validation to fail
|
|
// It's used for the source attr on notices
|
|
|
|
'xmlns:twitter' => 'http://api.twitter.com/',
|
|
'xml:lang' => 'en-US')); // XXX Other locales ?
|
|
|
|
$taguribase = common_config('integration', 'taguri');
|
|
$this->element('id', null, "tag:$taguribase:search/$server");
|
|
|
|
$site_uri = common_path(false);
|
|
|
|
$search_uri = $site_uri . 'api/search.atom?q=' . urlencode($this->query);
|
|
|
|
if ($this->rpp != 15) {
|
|
$search_uri .= '&rpp=' . $this->rpp;
|
|
}
|
|
|
|
// FIXME: this alternate link is not quite right because our
|
|
// web-based notice search doesn't support a rpp (responses per
|
|
// page) param yet
|
|
|
|
$this->element('link', array('type' => 'text/html',
|
|
'rel' => 'alternate',
|
|
'href' => $site_uri . 'search/notice?q=' .
|
|
urlencode($this->query)));
|
|
|
|
// self link
|
|
|
|
$self_uri = $search_uri;
|
|
$self_uri .= ($this->page > 1) ? '&page=' . $this->page : '';
|
|
|
|
$this->element('link', array('type' => 'application/atom+xml',
|
|
'rel' => 'self',
|
|
'href' => $self_uri));
|
|
|
|
$this->element('title', null, "$this->query - $sitename Search");
|
|
$this->element('updated', null, common_date_iso8601('now'));
|
|
|
|
// XXX: The below "rel" links are not valid Atom, but it's what
|
|
// Twitter does...
|
|
|
|
// refresh link
|
|
|
|
$refresh_uri = $search_uri . "&since_id=" . $this->max_id;
|
|
|
|
$this->element('link', array('type' => 'application/atom+xml',
|
|
'rel' => 'refresh',
|
|
'href' => $refresh_uri));
|
|
|
|
// pagination links
|
|
|
|
if ($this->cnt > $this->rpp) {
|
|
|
|
$next_uri = $search_uri . "&max_id=" . $this->max_id .
|
|
'&page=' . ($this->page + 1);
|
|
|
|
$this->element('link', array('type' => 'application/atom+xml',
|
|
'rel' => 'next',
|
|
'href' => $next_uri));
|
|
}
|
|
|
|
if ($this->page > 1) {
|
|
|
|
$previous_uri = $search_uri . "&max_id=" . $this->max_id .
|
|
'&page=' . ($this->page - 1);
|
|
|
|
$this->element('link', array('type' => 'application/atom+xml',
|
|
'rel' => 'previous',
|
|
'href' => $previous_uri));
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Build an Atom entry similar to search.twitter.com's based on
|
|
* a given notice
|
|
*
|
|
* @param Notice $notice the notice to use
|
|
*
|
|
* @return void
|
|
*/
|
|
|
|
function showEntry($notice)
|
|
{
|
|
$server = common_config('site', 'server');
|
|
$profile = $notice->getProfile();
|
|
$nurl = common_local_url('shownotice', array('notice' => $notice->id));
|
|
|
|
$this->elementStart('entry');
|
|
|
|
$taguribase = common_config('integration', 'taguri');
|
|
|
|
$this->element('id', null, "tag:$taguribase:$notice->id");
|
|
$this->element('published', null, common_date_w3dtf($notice->created));
|
|
$this->element('link', array('type' => 'text/html',
|
|
'rel' => 'alternate',
|
|
'href' => $nurl));
|
|
$this->element('title', null, common_xml_safe_str(trim($notice->content)));
|
|
$this->element('content', array('type' => 'html'), $notice->rendered);
|
|
$this->element('updated', null, common_date_w3dtf($notice->created));
|
|
$this->element('link', array('type' => 'image/png',
|
|
// XXX: Twitter uses rel="image" (not valid)
|
|
'rel' => 'related',
|
|
'href' => $profile->avatarUrl()));
|
|
|
|
// TODO: Here is where we'd put in a link to an atom feed for threads
|
|
|
|
$this->element("twitter:source", null,
|
|
htmlentities($this->source_link($notice->source)));
|
|
|
|
$this->elementStart('author');
|
|
|
|
$name = $profile->nickname;
|
|
|
|
if ($profile->fullname) {
|
|
$name .= ' (' . $profile->fullname . ')';
|
|
}
|
|
|
|
$this->element('name', null, $name);
|
|
$this->element('uri', null, common_profile_uri($profile));
|
|
$this->elementEnd('author');
|
|
|
|
$this->elementEnd('entry');
|
|
}
|
|
|
|
/**
|
|
* Initialize the Atom output, send headers
|
|
*
|
|
* @return void
|
|
*/
|
|
|
|
function initAtom()
|
|
{
|
|
header('Content-Type: application/atom+xml; charset=utf-8');
|
|
$this->startXml();
|
|
}
|
|
|
|
/**
|
|
* End the Atom feed
|
|
*
|
|
* @return void
|
|
*/
|
|
|
|
function endAtom()
|
|
{
|
|
$this->elementEnd('feed');
|
|
}
|
|
|
|
}
|