apisearchatom.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * Action for showing Twitter-like Atom search results
  6. *
  7. * PHP version 5
  8. *
  9. * LICENCE: This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. * @category Search
  23. * @package StatusNet
  24. * @author Zach Copley <zach@status.net>
  25. * @copyright 2008-2010 StatusNet, Inc.
  26. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  27. * @link http://status.net/
  28. */
  29. if (!defined('STATUSNET') && !defined('LACONICA')) {
  30. exit(1);
  31. }
  32. /**
  33. * Action for outputting search results in Twitter compatible Atom
  34. * format.
  35. *
  36. * TODO: abstract Atom stuff into a ruseable base class like
  37. * RSS10Action.
  38. *
  39. * @category Search
  40. * @package StatusNet
  41. * @author Zach Copley <zach@status.net>
  42. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  43. * @link http://status.net/
  44. *
  45. * @see ApiPrivateAuthAction
  46. */
  47. class ApiSearchAtomAction extends ApiPrivateAuthAction
  48. {
  49. var $cnt;
  50. var $query;
  51. var $lang;
  52. var $rpp;
  53. var $page;
  54. var $since_id;
  55. var $geocode;
  56. /**
  57. * Constructor
  58. *
  59. * Just wraps the Action constructor.
  60. *
  61. * @param string $output URI to output to, default = stdout
  62. * @param boolean $indent Whether to indent output, default true
  63. *
  64. * @see Action::__construct
  65. */
  66. function __construct($output='php://output', $indent=null)
  67. {
  68. parent::__construct($output, $indent);
  69. }
  70. /**
  71. * Do we need to write to the database?
  72. *
  73. * @return boolean true
  74. */
  75. function isReadonly()
  76. {
  77. return true;
  78. }
  79. /**
  80. * Read arguments and initialize members
  81. *
  82. * @param array $args Arguments from $_REQUEST
  83. *
  84. * @return boolean success
  85. */
  86. function prepare($args)
  87. {
  88. parent::prepare($args);
  89. $this->query = $this->trimmed('q');
  90. $this->lang = $this->trimmed('lang');
  91. $this->rpp = $this->trimmed('rpp');
  92. if (!$this->rpp) {
  93. $this->rpp = 15;
  94. }
  95. if ($this->rpp > 100) {
  96. $this->rpp = 100;
  97. }
  98. $this->page = $this->trimmed('page');
  99. if (!$this->page) {
  100. $this->page = 1;
  101. }
  102. // TODO: Suppport max_id -- we need to tweak the backend
  103. // Search classes to support it.
  104. $this->since_id = $this->trimmed('since_id');
  105. $this->geocode = $this->trimmed('geocode');
  106. // TODO: Also, language and geocode
  107. return true;
  108. }
  109. /**
  110. * Handle a request
  111. *
  112. * @param array $args Arguments from $_REQUEST
  113. *
  114. * @return void
  115. */
  116. function handle($args)
  117. {
  118. parent::handle($args);
  119. common_debug("In apisearchatom handle()");
  120. $this->showAtom();
  121. }
  122. /**
  123. * Get the notices to output as results. This also sets some class
  124. * attrs so we can use them to calculate pagination, and output
  125. * since_id and max_id.
  126. *
  127. * @return array an array of Notice objects sorted in reverse chron
  128. */
  129. function getNotices()
  130. {
  131. // TODO: Support search operators like from: and to:, boolean, etc.
  132. $notices = array();
  133. $notice = new Notice();
  134. // lcase it for comparison
  135. $q = strtolower($this->query);
  136. $search_engine = $notice->getSearchEngine('notice');
  137. $search_engine->set_sort_mode('chron');
  138. $search_engine->limit(($this->page - 1) * $this->rpp,
  139. $this->rpp + 1, true);
  140. if (false === $search_engine->query($q)) {
  141. $this->cnt = 0;
  142. } else {
  143. $this->cnt = $notice->find();
  144. }
  145. $cnt = 0;
  146. $this->max_id = 0;
  147. if ($this->cnt > 0) {
  148. while ($notice->fetch()) {
  149. ++$cnt;
  150. if (!$this->max_id) {
  151. $this->max_id = $notice->id;
  152. }
  153. if ($this->since_id && $notice->id <= $this->since_id) {
  154. break;
  155. }
  156. if ($cnt > $this->rpp) {
  157. break;
  158. }
  159. $notices[] = clone($notice);
  160. }
  161. }
  162. return $notices;
  163. }
  164. /**
  165. * Output search results as an Atom feed
  166. *
  167. * @return void
  168. */
  169. function showAtom()
  170. {
  171. $notices = $this->getNotices();
  172. $this->initAtom();
  173. $this->showFeed();
  174. foreach ($notices as $n) {
  175. $profile = $n->getProfile();
  176. // Don't show notices from deleted users
  177. if (!empty($profile)) {
  178. $this->showEntry($n);
  179. }
  180. }
  181. $this->endAtom();
  182. }
  183. /**
  184. * Show feed specific Atom elements
  185. *
  186. * @return void
  187. */
  188. function showFeed()
  189. {
  190. // TODO: A9 OpenSearch stuff like search.twitter.com?
  191. $server = common_config('site', 'server');
  192. $sitename = common_config('site', 'name');
  193. // XXX: Use xmlns:statusnet instead?
  194. $this->elementStart('feed',
  195. array('xmlns' => 'http://www.w3.org/2005/Atom',
  196. // XXX: xmlns:twitter causes Atom validation to fail
  197. // It's used for the source attr on notices
  198. 'xmlns:twitter' => 'http://api.twitter.com/',
  199. 'xml:lang' => 'en-US')); // XXX Other locales ?
  200. $taguribase = TagURI::base();
  201. $this->element('id', null, "tag:$taguribase:search/$server");
  202. $site_uri = common_path(false);
  203. $search_uri = $site_uri . 'api/search.atom?q=' . urlencode($this->query);
  204. if ($this->rpp != 15) {
  205. $search_uri .= '&rpp=' . $this->rpp;
  206. }
  207. // FIXME: this alternate link is not quite right because our
  208. // web-based notice search doesn't support a rpp (responses per
  209. // page) param yet
  210. $this->element('link', array('type' => 'text/html',
  211. 'rel' => 'alternate',
  212. 'href' => $site_uri . 'search/notice?q=' .
  213. urlencode($this->query)));
  214. // self link
  215. $self_uri = $search_uri;
  216. $self_uri .= ($this->page > 1) ? '&page=' . $this->page : '';
  217. $this->element('link', array('type' => 'application/atom+xml',
  218. 'rel' => 'self',
  219. 'href' => $self_uri));
  220. // @todo Needs i18n?
  221. $this->element('title', null, "$this->query - $sitename Search");
  222. $this->element('updated', null, common_date_iso8601('now'));
  223. // XXX: The below "rel" links are not valid Atom, but it's what
  224. // Twitter does...
  225. // refresh link
  226. $refresh_uri = $search_uri . "&since_id=" . $this->max_id;
  227. $this->element('link', array('type' => 'application/atom+xml',
  228. 'rel' => 'refresh',
  229. 'href' => $refresh_uri));
  230. // pagination links
  231. if ($this->cnt > $this->rpp) {
  232. $next_uri = $search_uri . "&max_id=" . $this->max_id .
  233. '&page=' . ($this->page + 1);
  234. $this->element('link', array('type' => 'application/atom+xml',
  235. 'rel' => 'next',
  236. 'href' => $next_uri));
  237. }
  238. if ($this->page > 1) {
  239. $previous_uri = $search_uri . "&max_id=" . $this->max_id .
  240. '&page=' . ($this->page - 1);
  241. $this->element('link', array('type' => 'application/atom+xml',
  242. 'rel' => 'previous',
  243. 'href' => $previous_uri));
  244. }
  245. }
  246. /**
  247. * Build an Atom entry similar to search.twitter.com's based on
  248. * a given notice
  249. *
  250. * @param Notice $notice the notice to use
  251. *
  252. * @return void
  253. */
  254. function showEntry($notice)
  255. {
  256. $server = common_config('site', 'server');
  257. $profile = $notice->getProfile();
  258. $nurl = common_local_url('shownotice', array('notice' => $notice->id));
  259. $this->elementStart('entry');
  260. $taguribase = TagURI::base();
  261. $this->element('id', null, "tag:$taguribase:$notice->id");
  262. $this->element('published', null, common_date_w3dtf($notice->created));
  263. $this->element('link', array('type' => 'text/html',
  264. 'rel' => 'alternate',
  265. 'href' => $nurl));
  266. $this->element('title', null, common_xml_safe_str(trim($notice->content)));
  267. $this->element('content', array('type' => 'html'), $notice->getRendered());
  268. $this->element('updated', null, common_date_w3dtf($notice->created));
  269. $this->element('link', array('type' => 'image/png',
  270. // XXX: Twitter uses rel="image" (not valid)
  271. 'rel' => 'related',
  272. 'href' => $profile->avatarUrl()));
  273. // @todo: Here is where we'd put in a link to an atom feed for threads
  274. $source = null;
  275. $source_link = null;
  276. $ns = $notice->getSource();
  277. if ($ns instanceof Notice_source) {
  278. $source = $ns->code;
  279. if (!empty($ns->url)) {
  280. $source_link = $ns->url;
  281. if (!empty($ns->name)) {
  282. $source = $ns->name;
  283. }
  284. }
  285. }
  286. $this->element("twitter:source", null, $source);
  287. $this->element("twitter:source_link", null, $source_link);
  288. $this->elementStart('author');
  289. $name = $profile->nickname;
  290. if ($profile->fullname) {
  291. // @todo Needs proper i18n?
  292. $name .= ' (' . $profile->fullname . ')';
  293. }
  294. $this->element('name', null, $name);
  295. $this->element('uri', null, common_profile_uri($profile));
  296. $this->elementEnd('author');
  297. $this->elementEnd('entry');
  298. }
  299. /**
  300. * Initialize the Atom output, send headers
  301. *
  302. * @return void
  303. */
  304. function initAtom()
  305. {
  306. header('Content-Type: application/atom+xml; charset=utf-8');
  307. $this->startXml();
  308. }
  309. /**
  310. * End the Atom feed
  311. *
  312. * @return void
  313. */
  314. function endAtom()
  315. {
  316. $this->elementEnd('feed');
  317. }
  318. }