Fix #1147, disable SMS-related features and remove them from the UI.
[statusnet:slux.git] / lib / action.php
1 <?php
2   /**
3    * Laconica, the distributed open-source microblogging tool
4    *
5    * Base class for all actions (~views)
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  Action
23    * @package   Laconica
24    * @author    Evan Prodromou <evan@controlyourself.ca>
25    * @author    Sarven Capadisli <csarven@controlyourself.ca>
26    * @copyright 2008 Control Yourself, Inc.
27    * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
28    * @link      http://laconi.ca/
29    */
30
31 if (!defined('LACONICA')) {
32   exit(1);
33  }
34
35 require_once INSTALLDIR.'/lib/noticeform.php';
36 require_once INSTALLDIR.'/lib/htmloutputter.php';
37
38 /**
39  * Base class for all actions
40  *
41  * This is the base class for all actions in the package. An action is
42  * more or less a "view" in an MVC framework.
43  *
44  * Actions are responsible for extracting and validating parameters; using
45  * model classes to read and write to the database; and doing ouput.
46  *
47  * @category Output
48  * @package  Laconica
49  * @author   Evan Prodromou <evan@controlyourself.ca>
50  * @author   Sarven Capadisli <csarven@controlyourself.ca>
51  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
52  * @link     http://laconi.ca/
53  *
54  * @see      HTMLOutputter
55  */
56 class Action extends HTMLOutputter // lawsuit
57 {
58   var $args;
59
60   /**
61    * Constructor
62    *
63    * Just wraps the HTMLOutputter constructor.
64    *
65    * @param string  $output URI to output to, default = stdout
66    * @param boolean $indent Whether to indent output, default true
67    *
68    * @see XMLOutputter::__construct
69    * @see HTMLOutputter::__construct
70    */
71   function __construct($output='php://output', $indent=true)
72   {
73     parent::__construct($output, $indent);
74   }
75
76   /**
77    * For initializing members of the class.
78    *
79    * @param array $argarray misc. arguments
80    *
81    * @return boolean true
82    */
83   function prepare($argarray)
84   {
85     $this->args =& common_copy_args($argarray);
86     return true;
87   }
88
89   /**
90    * Show page, a template method.
91    *
92    * @return nothing
93    */
94   function showPage()
95   {
96     $this->startHTML();
97     $this->showHead();
98     $this->showBody();
99     $this->endHTML();
100   }
101
102   /**
103    * Show head, a template method.
104    *
105    * @return nothing
106    */
107   function showHead()
108   {
109     // XXX: attributes (profile?)
110     $this->elementStart('head');
111     $this->showTitle();
112     $this->showStylesheets();
113     $this->showScripts();
114     $this->showOpenSearch();
115     $this->showFeeds();
116     $this->showDescription();
117     $this->extraHead();
118     $this->elementEnd('head');
119   }
120
121   /**
122    * Show title, a template method.
123    *
124    * @return nothing
125    */
126   function showTitle()
127   {
128     $this->element('title', null,
129                    sprintf(_("%s - %s"),
130                            $this->title(),
131                            common_config('site', 'name')));
132   }
133
134   /**
135    * Returns the page title
136    *
137    * SHOULD overload
138    *
139    * @return string page title
140    */
141
142   function title()
143   {
144     return _("Untitled page");
145   }
146
147   /**
148    * Show stylesheets
149    *
150    * @return nothing
151    */
152   function showStylesheets()
153   {
154     $this->element('link', array('rel' => 'stylesheet',
155                                  'type' => 'text/css',
156                                  'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION,
157                                  'media' => 'screen, projection, tv'));
158     $this->element('link', array('rel' => 'stylesheet',
159                                  'type' => 'text/css',
160                                  'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
161                                  'media' => 'screen, projection, tv'));
162     $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
163                    'href="'.theme_path('css/ie.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
164     foreach (array(6,7) as $ver) {
165       if (file_exists(theme_file('css/ie'.$ver.'.css', 'base'))) {
166         // Yes, IE people should be put in jail.
167         $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
168                        'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
169       }
170     }
171     $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
172                    'href="'.theme_path('css/ie.css', null).'?version='.LACONICA_VERSION.'" /><![endif]');
173   }
174
175   /**
176    * Show javascript headers
177    *
178    * @return nothing
179    */
180   function showScripts()
181   {
182     $this->element('script', array('type' => 'text/javascript',
183                                    'src' => common_path('js/jquery.min.js')),
184                    ' ');
185     $this->element('script', array('type' => 'text/javascript',
186                                    'src' => common_path('js/jquery.form.js')),
187                    ' ');
188     $this->element('script', array('type' => 'text/javascript',
189                                    'src' => common_path('js/xbImportNode.js')),
190                    ' ');
191     $this->element('script', array('type' => 'text/javascript',
192                                    'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
193                    ' ');
194   }
195
196   /**
197    * Show OpenSearch headers
198    *
199    * @return nothing
200    */
201   function showOpenSearch()
202   {
203     $this->element('link', array('rel' => 'search',
204                                  'type' => 'application/opensearchdescription+xml',
205                                  'href' =>  common_local_url('opensearch', array('type' => 'people')),
206                                  'title' => common_config('site', 'name').' People Search'));
207     $this->element('link', array('rel' => 'search', 'type' => 'application/opensearchdescription+xml',
208                                  'href' =>  common_local_url('opensearch', array('type' => 'notice')),
209                                  'title' => common_config('site', 'name').' Notice Search'));
210   }
211
212   /**
213    * Show feed headers
214    *
215    * MAY overload
216    *
217    * @return nothing
218    */
219   function showFeeds()
220   {
221     // does nothing by default
222   }
223
224   /**
225    * Show description.
226    *
227    * SHOULD overload
228    *
229    * @return nothing
230    */
231   function showDescription()
232   {
233     // does nothing by default
234   }
235
236   /**
237    * Show extra stuff in <head>.
238    *
239    * MAY overload
240    *
241    * @return nothing
242    */
243   function extraHead()
244   {
245     // does nothing by default
246   }
247
248   /**
249    * Show body.
250    *
251    * Calls template methods
252    *
253    * @return nothing
254    */
255   function showBody()
256   {
257     $this->elementStart('body', array('id' => $this->trimmed('action')));
258     $this->elementStart('div', array('id' => 'wrap'));
259     $this->showHeader();
260     $this->showCore();
261     $this->showFooter();
262     $this->elementEnd('div');
263     $this->elementEnd('body');
264   }
265
266   /**
267    * Show header of the page.
268    *
269    * Calls template methods
270    *
271    * @return nothing
272    */
273   function showHeader()
274   {
275     $this->elementStart('div', array('id' => 'header'));
276     $this->showLogo();
277     $this->showPrimaryNav();
278     $this->showSiteNotice();
279     if (common_logged_in()) {
280       $this->showNoticeForm();
281     } else {
282       $this->showAnonymousMessage();
283     }
284     $this->elementEnd('div');
285   }
286
287   /**
288    * Show configured logo.
289    *
290    * @return nothing
291    */
292   function showLogo()
293   {
294     $this->elementStart('address', array('id' => 'site_contact',
295                                          'class' => 'vcard'));
296     $this->elementStart('a', array('class' => 'url home bookmark',
297                                    'href' => common_local_url('public')));
298     if (common_config('site', 'logo') || file_exists(theme_file('logo.png'))) {
299       $this->element('img', array('class' => 'logo photo',
300                                   'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : theme_path('logo.png'),
301                                   'alt' => common_config('site', 'name')));
302     }
303     $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
304     $this->elementEnd('a');
305     $this->elementEnd('address');
306   }
307
308   /**
309    * Show primary navigation.
310    *
311    * @return nothing
312    */
313   function showPrimaryNav()
314   {
315     $this->elementStart('dl', array('id' => 'site_nav_global_primary'));
316     $this->element('dt', null, _('Primary site navigation'));
317     $this->elementStart('dd');
318     $user = common_current_user();
319     $this->elementStart('ul', array('class' => 'nav'));
320     if ($user) {
321       $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
322                       _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
323     }
324     $this->menuItem(common_local_url('peoplesearch'),
325                     _('Search'), _('Search for people or text'), false, 'nav_search');
326     if ($user) {
327       $this->menuItem(common_local_url('profilesettings'),
328                       _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
329             
330       if (common_config('xmpp', 'enabled')) {
331         $this->menuItem(common_local_url('imsettings'),
332                         _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
333       } else if (common_config('sms', 'enabled')){
334         $this->menuItem(common_local_url('smssettings'),
335                         _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
336       } else {
337         $this->menuItem(common_local_url('twittersettings'),
338                         _('Connect'), _('Connect to Twitter'), false, 'nav_connect');
339       }
340         
341             
342       $this->menuItem(common_local_url('logout'),
343                       _('Logout'), _('Logout from the site'), false, 'nav_logout');
344     } else {
345       $this->menuItem(common_local_url('login'),
346                       _('Login'), _('Login to the site'), false, 'nav_login');
347       if (!common_config('site', 'closed')) {
348         $this->menuItem(common_local_url('register'),
349                         _('Register'), _('Create an account'), false, 'nav_register');
350       }
351       $this->menuItem(common_local_url('openidlogin'),
352                       _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
353     }
354     $this->menuItem(common_local_url('doc', array('title' => 'help')),
355                     _('Help'), _('Help me!'), false, 'nav_help');
356     $this->elementEnd('ul');
357     $this->elementEnd('dd');
358     $this->elementEnd('dl');
359   }
360
361   /**
362    * Show site notice.
363    *
364    * @return nothing
365    */
366   function showSiteNotice()
367   {
368     // Revist. Should probably do an hAtom pattern here
369     $text = common_config('site', 'notice');
370     if ($text) {
371       $this->elementStart('dl', array('id' => 'site_notice',
372                                       'class' => 'system_notice'));
373       $this->element('dt', null, _('Site notice'));
374       $this->elementStart('dd', null);
375       $this->raw($text);
376       $this->elementEnd('dd');
377       $this->elementEnd('dl');
378     }
379   }
380
381   /**
382    * Show notice form.
383    *
384    * MAY overload if no notice form needed... or direct message box????
385    *
386    * @return nothing
387    */
388   function showNoticeForm()
389   {
390     $notice_form = new NoticeForm($this);
391     $notice_form->show();
392   }
393
394   /**
395    * Show anonymous message.
396    *
397    * SHOULD overload
398    *
399    * @return nothing
400    */
401   function showAnonymousMessage()
402   {
403     // needs to be defined by the class
404   }
405
406   /**
407    * Show core.
408    *
409    * Shows local navigation, content block and aside.
410    *
411    * @return nothing
412    */
413   function showCore()
414   {
415     $this->elementStart('div', array('id' => 'core'));
416     $this->showLocalNavBlock();
417     $this->showContentBlock();
418     $this->showAside();
419     $this->elementEnd('div');
420   }
421
422   /**
423    * Show local navigation block.
424    *
425    * @return nothing
426    */
427   function showLocalNavBlock()
428   {
429     $this->elementStart('dl', array('id' => 'site_nav_local_views'));
430     $this->element('dt', null, _('Local views'));
431     $this->elementStart('dd');
432     $this->showLocalNav();
433     $this->elementEnd('dd');
434     $this->elementEnd('dl');
435   }
436
437   /**
438    * Show local navigation.
439    *
440    * SHOULD overload
441    *
442    * @return nothing
443    */
444   function showLocalNav()
445   {
446     // does nothing by default
447   }
448
449   /**
450    * Show content block.
451    *
452    * @return nothing
453    */
454   function showContentBlock()
455   {
456     $this->elementStart('div', array('id' => 'content'));
457     $this->showPageTitle();
458     $this->showPageNoticeBlock();
459     $this->elementStart('div', array('id' => 'content_inner'));
460     // show the actual content (forms, lists, whatever)
461     $this->showContent();
462     $this->elementEnd('div');
463     $this->elementEnd('div');
464   }
465
466   /**
467    * Show page title.
468    *
469    * @return nothing
470    */
471   function showPageTitle()
472   {
473     $this->element('h1', null, $this->title());
474   }
475
476   /**
477    * Show page notice block.
478    *
479    * @return nothing
480    */
481   function showPageNoticeBlock()
482   {
483     $this->elementStart('dl', array('id' => 'page_notice',
484                                     'class' => 'system_notice'));
485     $this->element('dt', null, _('Page notice'));
486     $this->elementStart('dd');
487     $this->showPageNotice();
488     $this->elementEnd('dd');
489     $this->elementEnd('dl');
490   }
491
492   /**
493    * Show page notice.
494    *
495    * SHOULD overload (unless there's not a notice)
496    *
497    * @return nothing
498    */
499   function showPageNotice()
500   {
501   }
502
503   /**
504    * Show content.
505    *
506    * MUST overload (unless there's not a notice)
507    *
508    * @return nothing
509    */
510   function showContent()
511   {
512   }
513
514   /**
515    * Show Aside.
516    *
517    * @return nothing
518    */
519   function showAside()
520   {
521     $this->elementStart('div', array('id' => 'aside_primary',
522                                      'class' => 'aside'));
523     $this->showExportData();
524     $this->showSections();
525     $this->elementEnd('div');
526   }
527
528   /**
529    * Show export data feeds.
530    *
531    * MAY overload if there are feeds
532    *
533    * @return nothing
534    */
535   function showExportData()
536   {
537     // is there structure to this?
538     // list of (visible!) feed links
539     // can we reuse list of feeds from showFeeds() ?
540   }
541
542   /**
543    * Show sections.
544    *
545    * SHOULD overload
546    *
547    * @return nothing
548    */
549   function showSections()
550   {
551     // for each section, show it
552   }
553
554   /**
555    * Show footer.
556    *
557    * @return nothing
558    */
559   function showFooter()
560   {
561     $this->elementStart('div', array('id' => 'footer'));
562     $this->showSecondaryNav();
563     $this->showLicenses();
564     $this->elementEnd('div');
565   }
566
567   /**
568    * Show secondary navigation.
569    *
570    * @return nothing
571    */
572   function showSecondaryNav()
573   {
574     $this->elementStart('dl', array('id' => 'site_nav_global_secondary'));
575     $this->element('dt', null, _('Secondary site navigation'));
576     $this->elementStart('dd', null);
577     $this->elementStart('ul', array('class' => 'nav'));
578     $this->menuItem(common_local_url('doc', array('title' => 'help')),
579                     _('Help'));
580     $this->menuItem(common_local_url('doc', array('title' => 'about')),
581                     _('About'));
582     $this->menuItem(common_local_url('doc', array('title' => 'faq')),
583                     _('FAQ'));
584     $this->menuItem(common_local_url('doc', array('title' => 'privacy')),
585                     _('Privacy'));
586     $this->menuItem(common_local_url('doc', array('title' => 'source')),
587                     _('Source'));
588     $this->menuItem(common_local_url('doc', array('title' => 'contact')),
589                     _('Contact'));
590     $this->elementEnd('ul');
591     $this->elementEnd('dd');
592     $this->elementEnd('dl');
593   }
594
595   /**
596    * Show licenses.
597    *
598    * @return nothing
599    */
600   function showLicenses()
601   {
602     $this->elementStart('dl', array('id' => 'licenses'));
603     $this->showLaconicaLicense();
604     $this->showContentLicense();
605     $this->elementEnd('dl');
606   }
607
608   /**
609    * Show Laconica license.
610    *
611    * @return nothing
612    */
613   function showLaconicaLicense()
614   {
615     $this->element('dt', array('id' => 'site_laconica_license'), _('Laconica software license'));
616     $this->elementStart('dd', null);
617     if (common_config('site', 'broughtby')) {
618       $instr = _('**%%site.name%%** is a microblogging service brought to you by [%%site.broughtby%%](%%site.broughtbyurl%%). ');
619     } else {
620       $instr = _('**%%site.name%%** is a microblogging service. ');
621     }
622     $instr .= sprintf(_('It runs the [Laconica](http://laconi.ca/) microblogging software, version %s, available under the [GNU Affero General Public License](http://www.fsf.org/licensing/licenses/agpl-3.0.html).'), LACONICA_VERSION);
623     $output = common_markup_to_html($instr);
624     $this->raw($output);
625     $this->elementEnd('dd');
626     // do it
627   }
628
629   /**
630    * Show content license.
631    *
632    * @return nothing
633    */
634   function showContentLicense()
635   {
636     $this->element('dt', array('id' => 'site_content_license'), _('Laconica software license'));
637     $this->elementStart('dd', array('id' => 'site_content_license_cc'));
638     $this->elementStart('p');
639     $this->element('img', array('id' => 'license_cc',
640                                 'src' => common_config('license', 'image'),
641                                 'alt' => common_config('license', 'title')));
642     $m = _('All %s content and data are available under the' .
643            ' %%s license.');
644     $m = sprintf($m, common_config('site', 'name'));
645     $m = explode("%s", $m, 2);
646     $this->raw($m[0]);
647     $this->element('a', array('class' => 'license',
648                               'rel' => 'external license',
649                               'href' => common_config('license', 'url')),
650                    common_config('license', 'title'));
651     $this->raw($m[1]);
652     $this->elementEnd('p');
653     $this->elementEnd('dd');
654   }
655
656   /**
657    * Return last modified, if applicable.
658    *
659    * MAY override
660    *
661    * @return string last modified http header
662    */
663   function lastModified()
664   {
665     // For comparison with If-Last-Modified
666     // If not applicable, return null
667     return null;
668   }
669
670   /**
671    * Return etag, if applicable.
672    *
673    * MAY override
674    *
675    * @return string etag http header
676    */
677   function etag()
678   {
679     return null;
680   }
681
682   /**
683    * Return true if read only.
684    *
685    * MAY override
686    *
687    * @return boolean is read only action?
688    */
689   function isReadOnly()
690   {
691     return false;
692   }
693
694   /**
695    * Returns query argument or default value if not found
696    *
697    * @param string $key requested argument
698    * @param string $def default value to return if $key is not provided
699    *
700    * @return boolean is read only action?
701    */
702   function arg($key, $def=null)
703   {
704     if (array_key_exists($key, $this->args)) {
705       return $this->args[$key];
706     } else {
707       return $def;
708     }
709   }
710
711   /**
712    * Returns trimmed query argument or default value if not found
713    *
714    * @param string $key requested argument
715    * @param string $def default value to return if $key is not provided
716    *
717    * @return boolean is read only action?
718    */
719   function trimmed($key, $def=null)
720   {
721     $arg = $this->arg($key, $def);
722     return is_string($arg) ? trim($arg) : $arg;
723   }
724
725   /**
726    * Handler method
727    *
728    * @param array $argarray is ignored since it's now passed in in prepare()
729    *
730    * @return boolean is read only action?
731    */
732   function handle($argarray=null)
733   {
734     $lm   = $this->lastModified();
735     $etag = $this->etag();
736     if ($etag) {
737       header('ETag: ' . $etag);
738     }
739     if ($lm) {
740       header('Last-Modified: ' . date(DATE_RFC1123, $lm));
741       $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
742       if ($if_modified_since) {
743         $ims = strtotime($if_modified_since);
744         if ($lm <= $ims) {
745           if (!$etag ||
746               $this->_hasEtag($etag, $_SERVER['HTTP_IF_NONE_MATCH'])) {
747             header('HTTP/1.1 304 Not Modified');
748             // Better way to do this?
749             exit(0);
750           }
751         }
752       }
753     }
754   }
755
756   /**
757    * HasĀ etag? (private)
758    *
759    * @param string $etag          etag http header
760    * @param string $if_none_match ifNoneMatch http header
761    *
762    * @return boolean
763    */
764   function _hasEtag($etag, $if_none_match)
765   {
766     return ($if_none_match) && in_array($etag, explode(',', $if_none_match));
767   }
768
769   /**
770    * Boolean understands english (yes, no, true, false)
771    *
772    * @param string $key query key we're interested in
773    * @param string $def default value
774    *
775    * @return boolean interprets yes/no strings as boolean
776    */
777   function boolean($key, $def=false)
778   {
779     $arg = strtolower($this->trimmed($key));
780
781     if (is_null($arg)) {
782       return $def;
783     } else if (in_array($arg, array('true', 'yes', '1'))) {
784       return true;
785     } else if (in_array($arg, array('false', 'no', '0'))) {
786       return false;
787     } else {
788       return $def;
789     }
790   }
791
792   /**
793    * Server error
794    *
795    * @param string  $msg  error message to display
796    * @param integer $code http error code, 500 by default
797    *
798    * @return nothing
799    */
800   function serverError($msg, $code=500)
801   {
802     $action = $this->trimmed('action');
803     common_debug("Server error '$code' on '$action': $msg", __FILE__);
804     common_server_error($msg, $code);
805   }
806
807   /**
808    * Client error
809    *
810    * @param string  $msg  error message to display
811    * @param integer $code http error code, 400 by default
812    *
813    * @return nothing
814    */
815   function clientError($msg, $code=400)
816   {
817     $action = $this->trimmed('action');
818     common_debug("User error '$code' on '$action': $msg", __FILE__);
819     common_user_error($msg, $code);
820   }
821
822   /**
823    * Returns the current URL
824    *
825    * @return string current URL
826    */
827   function selfUrl()
828   {
829     $action = $this->trimmed('action');
830     $args   = $this->args;
831     unset($args['action']);
832     foreach (array_keys($_COOKIE) as $cookie) {
833       unset($args[$cookie]);
834     }
835     return common_local_url($action, $args);
836   }
837
838   /**
839    * Generate a menu item
840    *
841    * @param string  $url         menu URL
842    * @param string  $text        menu name
843    * @param string  $title       title attribute, null by default
844    * @param boolean $is_selected current menu item, false by default
845    * @param string  $id          element id, null by default
846    *
847    * @return nothing
848    */
849   function menuItem($url, $text, $title=null, $is_selected=false, $id=null)
850   {
851     // Added @id to li for some control.
852     // XXX: We might want to move this to htmloutputter.php
853     $lattrs = array();
854     if ($is_selected) {
855       $lattrs['class'] = 'current';
856     }
857
858     (is_null($id)) ? $lattrs : $lattrs['id'] = $id;
859
860     $this->elementStart('li', $lattrs);
861     $attrs['href'] = $url;
862     if ($title) {
863       $attrs['title'] = $title;
864     }
865     $this->element('a', $attrs, $text);
866     $this->elementEnd('li');
867   }
868
869   /**
870    * Generate pagination links
871    *
872    * @param boolean $have_before is there something before?
873    * @param boolean $have_after  is there something after?
874    * @param integer $page        current page
875    * @param string  $action      current action
876    * @param array   $args        rest of query arguments
877    *
878    * @return nothing
879    */
880   function pagination($have_before, $have_after, $page, $action, $args=null)
881   {
882     // Does a little before-after block for next/prev page
883     if ($have_before || $have_after) {
884       $this->elementStart('div', array('class' => 'pagination'));
885       $this->elementStart('dl', null);
886       $this->element('dt', null, _('Pagination'));
887       $this->elementStart('dd', null);
888       $this->elementStart('ul', array('class' => 'nav'));
889     }
890     if ($have_before) {
891       $pargs   = array('page' => $page-1);
892       $newargs = $args ? array_merge($args, $pargs) : $pargs;
893       $this->elementStart('li', array('class' => 'nav_prev'));
894       $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'prev'),
895                      _('After'));
896       $this->elementEnd('li');
897     }
898     if ($have_after) {
899       $pargs   = array('page' => $page+1);
900       $newargs = $args ? array_merge($args, $pargs) : $pargs;
901       $this->elementStart('li', array('class' => 'nav_next'));
902       $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'next'),
903                      _('Before'));
904       $this->elementEnd('li');
905     }
906     if ($have_before || $have_after) {
907       $this->elementEnd('ul');
908       $this->elementEnd('dd');
909       $this->elementEnd('dl');
910       $this->elementEnd('div');
911     }
912   }
913 }