]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/apisearchatom.php
Merge branch 'apinamespace' into 0.9.x
[quix0rs-gnu-social.git] / actions / apisearchatom.php
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
30 if (!defined('STATUSNET') && !defined('LACONICA')) {
31     exit(1);
32 }
33
34 require_once INSTALLDIR.'/lib/apiprivateauth.php';
35
36 /**
37  * Action for outputting search results in Twitter compatible Atom
38  * format.
39  *
40  * TODO: abstract Atom stuff into a ruseable base class like
41  * RSS10Action.
42  *
43  * @category Search
44  * @package  StatusNet
45  * @author   Zach Copley <zach@status.net>
46  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
47  * @link     http://status.net/
48  *
49  * @see      ApiPrivateAuthAction
50  */
51
52 class ApiSearchAtomAction extends ApiPrivateAuthAction
53 {
54
55     var $cnt;
56     var $query;
57     var $lang;
58     var $rpp;
59     var $page;
60     var $since_id;
61     var $geocode;
62
63     /**
64      * Constructor
65      *
66      * Just wraps the Action constructor.
67      *
68      * @param string  $output URI to output to, default = stdout
69      * @param boolean $indent Whether to indent output, default true
70      *
71      * @see Action::__construct
72      */
73
74     function __construct($output='php://output', $indent=null)
75     {
76         parent::__construct($output, $indent);
77     }
78
79     /**
80      * Do we need to write to the database?
81      *
82      * @return boolean true
83      */
84
85     function isReadonly()
86     {
87         return true;
88     }
89
90     /**
91      * Read arguments and initialize members
92      *
93      * @param array $args Arguments from $_REQUEST
94      *
95      * @return boolean success
96      *
97      */
98
99     function prepare($args)
100     {
101         common_debug("in apisearchatom prepare()");
102
103         parent::prepare($args);
104
105
106         $this->query = $this->trimmed('q');
107         $this->lang  = $this->trimmed('lang');
108         $this->rpp   = $this->trimmed('rpp');
109
110         if (!$this->rpp) {
111             $this->rpp = 15;
112         }
113
114         if ($this->rpp > 100) {
115             $this->rpp = 100;
116         }
117
118         $this->page = $this->trimmed('page');
119
120         if (!$this->page) {
121             $this->page = 1;
122         }
123
124         // TODO: Suppport since_id -- we need to tweak the backend
125         // Search classes to support it.
126
127         $this->since_id = $this->trimmed('since_id');
128         $this->geocode  = $this->trimmed('geocode');
129
130         // TODO: Also, language and geocode
131
132         return true;
133     }
134
135     /**
136      * Handle a request
137      *
138      * @param array $args Arguments from $_REQUEST
139      *
140      * @return void
141      */
142
143     function handle($args)
144     {
145         parent::handle($args);
146         common_debug("In apisearchatom handle()");
147         $this->showAtom();
148     }
149
150     /**
151      * Get the notices to output as results. This also sets some class
152      * attrs so we can use them to calculate pagination, and output
153      * since_id and max_id.
154      *
155      * @return array an array of Notice objects sorted in reverse chron
156      */
157
158     function getNotices()
159     {
160         // TODO: Support search operators like from: and to:, boolean, etc.
161
162         $notices = array();
163         $notice = new Notice();
164
165         // lcase it for comparison
166         $q = strtolower($this->query);
167
168         $search_engine = $notice->getSearchEngine('notice');
169         $search_engine->set_sort_mode('chron');
170         $search_engine->limit(($this->page - 1) * $this->rpp,
171             $this->rpp + 1, true);
172         if (false === $search_engine->query($q)) {
173             $this->cnt = 0;
174         } else {
175             $this->cnt = $notice->find();
176         }
177
178         $cnt = 0;
179         $this->max_id = 0;
180
181         if ($this->cnt > 0) {
182             while ($notice->fetch()) {
183
184                 ++$cnt;
185
186                 if (!$this->max_id) {
187                     $this->max_id = $notice->id;
188                 }
189
190                 if ($cnt > $this->rpp) {
191                     break;
192                 }
193
194                 $notices[] = clone($notice);
195             }
196         }
197
198         return $notices;
199     }
200
201     /**
202      * Output search results as an Atom feed
203      *
204      * @return void
205      */
206
207     function showAtom()
208     {
209         $notices = $this->getNotices();
210
211         $this->initAtom();
212         $this->showFeed();
213
214         foreach ($notices as $n) {
215
216             $profile = $n->getProfile();
217
218             // Don't show notices from deleted users
219
220             if (!empty($profile)) {
221                 $this->showEntry($n);
222             }
223         }
224
225         $this->endAtom();
226     }
227
228     /**
229      * Show feed specific Atom elements
230      *
231      * @return void
232      */
233
234     function showFeed()
235     {
236         // TODO: A9 OpenSearch stuff like search.twitter.com?
237
238         $server   = common_config('site', 'server');
239         $sitename = common_config('site', 'name');
240
241         // XXX: Use xmlns:statusnet instead?
242
243         $this->elementStart('feed',
244             array('xmlns' => 'http://www.w3.org/2005/Atom',
245
246                              // XXX: xmlns:twitter causes Atom validation to fail
247                              // It's used for the source attr on notices
248
249                              'xmlns:twitter' => 'http://api.twitter.com/',
250                              'xml:lang' => 'en-US')); // XXX Other locales ?
251
252         $taguribase = TagURI::base();
253         $this->element('id', null, "tag:$taguribase:search/$server");
254
255         $site_uri = common_path(false);
256
257         $search_uri = $site_uri . 'api/search.atom?q=' . urlencode($this->query);
258
259         if ($this->rpp != 15) {
260             $search_uri .= '&rpp=' . $this->rpp;
261         }
262
263         // FIXME: this alternate link is not quite right because our
264         // web-based notice search doesn't support a rpp (responses per
265         // page) param yet
266
267         $this->element('link', array('type' => 'text/html',
268                                      'rel'  => 'alternate',
269                                      'href' => $site_uri . 'search/notice?q=' .
270                                         urlencode($this->query)));
271
272         // self link
273
274         $self_uri = $search_uri;
275         $self_uri .= ($this->page > 1) ? '&page=' . $this->page : '';
276
277         $this->element('link', array('type' => 'application/atom+xml',
278                                      'rel'  => 'self',
279                                      'href' => $self_uri));
280
281         $this->element('title', null, "$this->query - $sitename Search");
282         $this->element('updated', null, common_date_iso8601('now'));
283
284         // XXX: The below "rel" links are not valid Atom, but it's what
285         // Twitter does...
286
287         // refresh link
288
289         $refresh_uri = $search_uri . "&since_id=" . $this->max_id;
290
291         $this->element('link', array('type' => 'application/atom+xml',
292                                      'rel'  => 'refresh',
293                                      'href' => $refresh_uri));
294
295         // pagination links
296
297         if ($this->cnt > $this->rpp) {
298
299             $next_uri = $search_uri . "&max_id=" . $this->max_id .
300                 '&page=' . ($this->page + 1);
301
302             $this->element('link', array('type' => 'application/atom+xml',
303                                          'rel'  => 'next',
304                                          'href' => $next_uri));
305         }
306
307         if ($this->page > 1) {
308
309             $previous_uri = $search_uri . "&max_id=" . $this->max_id .
310                 '&page=' . ($this->page - 1);
311
312             $this->element('link', array('type' => 'application/atom+xml',
313                                          'rel'  => 'previous',
314                                          'href' => $previous_uri));
315         }
316
317     }
318
319     /**
320      * Build an Atom entry similar to search.twitter.com's based on
321      * a given notice
322      *
323      * @param Notice $notice the notice to use
324      *
325      * @return void
326      */
327
328     function showEntry($notice)
329     {
330         $server  = common_config('site', 'server');
331         $profile = $notice->getProfile();
332         $nurl    = common_local_url('shownotice', array('notice' => $notice->id));
333
334         $this->elementStart('entry');
335
336         $taguribase = TagURI::base();
337
338         $this->element('id', null, "tag:$taguribase:$notice->id");
339         $this->element('published', null, common_date_w3dtf($notice->created));
340         $this->element('link', array('type' => 'text/html',
341                                      'rel'  => 'alternate',
342                                      'href' => $nurl));
343         $this->element('title', null, common_xml_safe_str(trim($notice->content)));
344         $this->element('content', array('type' => 'html'), $notice->rendered);
345         $this->element('updated', null, common_date_w3dtf($notice->created));
346         $this->element('link', array('type' => 'image/png',
347                                      // XXX: Twitter uses rel="image" (not valid)
348                                      'rel' => 'related',
349                                      'href' => $profile->avatarUrl()));
350
351         // @todo: Here is where we'd put in a link to an atom feed for threads
352
353         $source = null;
354
355         $ns = $notice->getSource();
356         if ($ns) {
357             if (!empty($ns->name) && !empty($ns->url)) {
358                 $source = '<a href="'
359                    . htmlspecialchars($ns->url)
360                    . '" rel="nofollow">'
361                    . htmlspecialchars($ns->name)
362                    . '</a>';
363             } else {
364                 $source = $ns->code;
365             }
366         }
367
368         $this->element("twitter:source", null, $source);
369
370         $this->elementStart('author');
371
372         $name = $profile->nickname;
373
374         if ($profile->fullname) {
375             $name .= ' (' . $profile->fullname . ')';
376         }
377
378         $this->element('name', null, $name);
379         $this->element('uri', null, common_profile_uri($profile));
380         $this->elementEnd('author');
381
382         $this->elementEnd('entry');
383     }
384
385     /**
386      * Initialize the Atom output, send headers
387      *
388      * @return void
389      */
390
391     function initAtom()
392     {
393         header('Content-Type: application/atom+xml; charset=utf-8');
394         $this->startXml();
395     }
396
397     /**
398      * End the Atom feed
399      *
400      * @return void
401      */
402
403     function endAtom()
404     {
405         $this->elementEnd('feed');
406     }
407
408 }