PageRenderTime 60ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/action.php

https://gitlab.com/windigo-gs/windigos-gnu-social
PHP | 1656 lines | 928 code | 167 blank | 561 comment | 152 complexity | 9902ce9538be9e80a3db0eda2fcddd78 MD5 | raw file
Possible License(s): AGPL-3.0, BSD-3-Clause, GPL-2.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * StatusNet, 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 StatusNet
  24. * @author Evan Prodromou <evan@status.net>
  25. * @author Sarven Capadisli <csarven@status.net>
  26. * @copyright 2008 StatusNet, Inc.
  27. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  28. * @link http://status.net/
  29. */
  30. if (!defined('GNUSOCIAL')) { exit(1); }
  31. /**
  32. * Base class for all actions
  33. *
  34. * This is the base class for all actions in the package. An action is
  35. * more or less a "view" in an MVC framework.
  36. *
  37. * Actions are responsible for extracting and validating parameters; using
  38. * model classes to read and write to the database; and doing ouput.
  39. *
  40. * @category Output
  41. * @package StatusNet
  42. * @author Evan Prodromou <evan@status.net>
  43. * @author Sarven Capadisli <csarven@status.net>
  44. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  45. * @link http://status.net/
  46. *
  47. * @see HTMLOutputter
  48. */
  49. class Action extends HTMLOutputter // lawsuit
  50. {
  51. // This should be protected/private in the future
  52. public $args = array();
  53. // Action properties, set per-class
  54. protected $action = false;
  55. protected $ajax = false;
  56. protected $menus = true;
  57. protected $needLogin = false;
  58. protected $needPost = false; // implies canPost if true
  59. protected $canPost = false; // can this action handle POST method?
  60. // The currently scoped profile (normally Profile::current; from $this->auth_user for API)
  61. protected $scoped = null;
  62. // Related to front-end user representation
  63. protected $format = null;
  64. protected $error = null;
  65. protected $msg = null;
  66. /**
  67. * Constructor
  68. *
  69. * Just wraps the HTMLOutputter constructor.
  70. *
  71. * @param string $output URI to output to, default = stdout
  72. * @param boolean $indent Whether to indent output, default true
  73. *
  74. * @see XMLOutputter::__construct
  75. * @see HTMLOutputter::__construct
  76. */
  77. function __construct($output='php://output', $indent=null)
  78. {
  79. parent::__construct($output, $indent);
  80. }
  81. function getError()
  82. {
  83. return $this->error;
  84. }
  85. function getInfo()
  86. {
  87. return $this->msg;
  88. }
  89. static public function run(array $args=array(), $output='php://output', $indent=null) {
  90. $class = get_called_class();
  91. $action = new $class($output, $indent);
  92. $action->execute($args);
  93. return $action;
  94. }
  95. public function execute(array $args=array()) {
  96. // checkMirror stuff
  97. if (common_config('db', 'mirror') && $this->isReadOnly($args)) {
  98. if (is_array(common_config('db', 'mirror'))) {
  99. // "load balancing", ha ha
  100. $arr = common_config('db', 'mirror');
  101. $k = array_rand($arr);
  102. $mirror = $arr[$k];
  103. } else {
  104. $mirror = common_config('db', 'mirror');
  105. }
  106. // everyone else uses the mirror
  107. common_config_set('db', 'database', $mirror);
  108. }
  109. $status = $this->prepare($args);
  110. if ($status) {
  111. $this->handle($args);
  112. } else {
  113. common_debug('Prepare failed for Action.');
  114. }
  115. $this->flush();
  116. Event::handle('EndActionExecute', array($status, $this));
  117. }
  118. /**
  119. * For initializing members of the class.
  120. *
  121. * @param array $argarray misc. arguments
  122. *
  123. * @return boolean true
  124. */
  125. protected function prepare(array $args=array())
  126. {
  127. if ($this->needPost && !$this->isPost()) {
  128. // TRANS: Client error. POST is a HTTP command. It should not be translated.
  129. $this->clientError(_('This method requires a POST.'), 405);
  130. }
  131. // needPost, of course, overrides canPost if true
  132. if (!$this->canPost) {
  133. $this->canPost = $this->needPost;
  134. }
  135. $this->args = common_copy_args($args);
  136. // This could be set with get_called_action and then
  137. // chop off 'Action' from the class name. In lower case.
  138. $this->action = strtolower($this->trimmed('action'));
  139. if ($this->ajax || $this->boolean('ajax')) {
  140. // check with StatusNet::isAjax()
  141. StatusNet::setAjax(true);
  142. }
  143. if ($this->needLogin) {
  144. $this->checkLogin(); // if not logged in, this redirs/excepts
  145. }
  146. $this->updateScopedProfile();
  147. return true;
  148. }
  149. function updateScopedProfile() {
  150. $this->scoped = Profile::current();
  151. return $this->scoped;
  152. }
  153. // Must be run _after_ prepare
  154. public function getActionName()
  155. {
  156. return $this->action;
  157. }
  158. /**
  159. * Show page, a template method.
  160. *
  161. * @return nothing
  162. */
  163. function showPage()
  164. {
  165. if (Event::handle('StartShowHTML', array($this))) {
  166. $this->startHTML();
  167. $this->flush();
  168. Event::handle('EndShowHTML', array($this));
  169. }
  170. if (Event::handle('StartShowHead', array($this))) {
  171. $this->showHead();
  172. $this->flush();
  173. Event::handle('EndShowHead', array($this));
  174. }
  175. if (Event::handle('StartShowBody', array($this))) {
  176. $this->showBody();
  177. Event::handle('EndShowBody', array($this));
  178. }
  179. if (Event::handle('StartEndHTML', array($this))) {
  180. $this->endHTML();
  181. Event::handle('EndEndHTML', array($this));
  182. }
  183. }
  184. function endHTML()
  185. {
  186. global $_startTime;
  187. if (isset($_startTime)) {
  188. $endTime = microtime(true);
  189. $diff = round(($endTime - $_startTime) * 1000);
  190. $this->raw("<!-- ${diff}ms -->");
  191. }
  192. return parent::endHTML();
  193. }
  194. /**
  195. * Show head, a template method.
  196. *
  197. * @return nothing
  198. */
  199. function showHead()
  200. {
  201. // XXX: attributes (profile?)
  202. $this->elementStart('head');
  203. if (Event::handle('StartShowHeadElements', array($this))) {
  204. if (Event::handle('StartShowHeadTitle', array($this))) {
  205. $this->showTitle();
  206. Event::handle('EndShowHeadTitle', array($this));
  207. }
  208. $this->showShortcutIcon();
  209. $this->showStylesheets();
  210. $this->showOpenSearch();
  211. $this->showFeeds();
  212. $this->showDescription();
  213. $this->extraHead();
  214. Event::handle('EndShowHeadElements', array($this));
  215. }
  216. $this->elementEnd('head');
  217. }
  218. /**
  219. * Show title, a template method.
  220. *
  221. * @return nothing
  222. */
  223. function showTitle()
  224. {
  225. $this->element('title', null,
  226. // TRANS: Page title. %1$s is the title, %2$s is the site name.
  227. sprintf(_('%1$s - %2$s'),
  228. $this->title(),
  229. common_config('site', 'name')));
  230. }
  231. /**
  232. * Returns the page title
  233. *
  234. * SHOULD overload
  235. *
  236. * @return string page title
  237. */
  238. function title()
  239. {
  240. // TRANS: Page title for a page without a title set.
  241. return _('Untitled page');
  242. }
  243. /**
  244. * Show themed shortcut icon
  245. *
  246. * @return nothing
  247. */
  248. function showShortcutIcon()
  249. {
  250. if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/favicon.ico')) {
  251. $this->element('link', array('rel' => 'shortcut icon',
  252. 'href' => Theme::path('favicon.ico')));
  253. } else {
  254. // favicon.ico should be HTTPS if the rest of the page is
  255. $this->element('link', array('rel' => 'shortcut icon',
  256. 'href' => common_path('favicon.ico', StatusNet::isHTTPS())));
  257. }
  258. if (common_config('site', 'mobile')) {
  259. if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/apple-touch-icon.png')) {
  260. $this->element('link', array('rel' => 'apple-touch-icon',
  261. 'href' => Theme::path('apple-touch-icon.png')));
  262. } else {
  263. $this->element('link', array('rel' => 'apple-touch-icon',
  264. 'href' => common_path('apple-touch-icon.png')));
  265. }
  266. }
  267. }
  268. /**
  269. * Show stylesheets
  270. *
  271. * @return nothing
  272. */
  273. function showStylesheets()
  274. {
  275. if (Event::handle('StartShowStyles', array($this))) {
  276. // Use old name for StatusNet for compatibility on events
  277. if (Event::handle('StartShowStylesheets', array($this))) {
  278. $this->primaryCssLink(null, 'screen, projection, tv, print');
  279. Event::handle('EndShowStylesheets', array($this));
  280. }
  281. $this->cssLink('js/extlib/jquery-ui/css/smoothness/jquery-ui.css');
  282. if (Event::handle('StartShowUAStyles', array($this))) {
  283. $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
  284. 'href="'.Theme::path('css/ie.css', 'base').'?version='.GNUSOCIAL_VERSION.'" /><![endif]');
  285. foreach (array(6,7) as $ver) {
  286. if (file_exists(Theme::file('css/ie'.$ver.'.css', 'base'))) {
  287. // Yes, IE people should be put in jail.
  288. $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
  289. 'href="'.Theme::path('css/ie'.$ver.'.css', 'base').'?version='.GNUSOCIAL_VERSION.'" /><![endif]');
  290. }
  291. }
  292. if (file_exists(Theme::file('css/ie.css'))) {
  293. $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
  294. 'href="'.Theme::path('css/ie.css', null).'?version='.GNUSOCIAL_VERSION.'" /><![endif]');
  295. }
  296. Event::handle('EndShowUAStyles', array($this));
  297. }
  298. Event::handle('EndShowStyles', array($this));
  299. if (common_config('custom_css', 'enabled')) {
  300. $css = common_config('custom_css', 'css');
  301. if (Event::handle('StartShowCustomCss', array($this, &$css))) {
  302. if (trim($css) != '') {
  303. $this->style($css);
  304. }
  305. Event::handle('EndShowCustomCss', array($this));
  306. }
  307. }
  308. }
  309. }
  310. function primaryCssLink($mainTheme=null, $media=null)
  311. {
  312. $theme = new Theme($mainTheme);
  313. // Some themes may have external stylesheets, such as using the
  314. // Google Font APIs to load webfonts.
  315. foreach ($theme->getExternals() as $url) {
  316. $this->cssLink($url, $mainTheme, $media);
  317. }
  318. // If the currently-selected theme has dependencies on other themes,
  319. // we'll need to load their display.css files as well in order.
  320. $baseThemes = $theme->getDeps();
  321. foreach ($baseThemes as $baseTheme) {
  322. $this->cssLink('css/display.css', $baseTheme, $media);
  323. }
  324. $this->cssLink('css/display.css', $mainTheme, $media);
  325. // Additional styles for RTL languages
  326. if (is_rtl(common_language())) {
  327. if (file_exists(Theme::file('css/rtl.css'))) {
  328. $this->cssLink('css/rtl.css', $mainTheme, $media);
  329. }
  330. }
  331. }
  332. /**
  333. * Show javascript headers
  334. *
  335. * @return nothing
  336. */
  337. function showScripts()
  338. {
  339. if (Event::handle('StartShowScripts', array($this))) {
  340. if (Event::handle('StartShowJQueryScripts', array($this))) {
  341. $this->script('extlib/jquery.js');
  342. $this->script('extlib/jquery.form.js');
  343. $this->script('extlib/jquery-ui/jquery-ui.js');
  344. $this->script('extlib/jquery.cookie.js');
  345. $this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/extlib/json2.js', StatusNet::isHTTPS()).'"); }');
  346. $this->script('extlib/jquery.infieldlabel.js');
  347. Event::handle('EndShowJQueryScripts', array($this));
  348. }
  349. if (Event::handle('StartShowStatusNetScripts', array($this))) {
  350. $this->script('util.js');
  351. $this->script('xbImportNode.js');
  352. $this->script('geometa.js');
  353. // This route isn't available in single-user mode.
  354. // Not sure why, but it causes errors here.
  355. $this->inlineScript('var _peopletagAC = "' .
  356. common_local_url('peopletagautocomplete') . '";');
  357. $this->showScriptMessages();
  358. // Anti-framing code to avoid clickjacking attacks in older browsers.
  359. // This will show a blank page if the page is being framed, which is
  360. // consistent with the behavior of the 'X-Frame-Options: SAMEORIGIN'
  361. // header, which prevents framing in newer browser.
  362. if (common_config('javascript', 'bustframes')) {
  363. $this->inlineScript('if (window.top !== window.self) { document.write = ""; window.top.location = window.self.location; setTimeout(function () { document.body.innerHTML = ""; }, 1); window.self.onload = function () { document.body.innerHTML = ""; }; }');
  364. }
  365. Event::handle('EndShowStatusNetScripts', array($this));
  366. }
  367. Event::handle('EndShowScripts', array($this));
  368. }
  369. }
  370. /**
  371. * Exports a map of localized text strings to JavaScript code.
  372. *
  373. * Plugins can add to what's exported by hooking the StartScriptMessages or EndScriptMessages
  374. * events and appending to the array. Try to avoid adding strings that won't be used, as
  375. * they'll be added to HTML output.
  376. */
  377. function showScriptMessages()
  378. {
  379. $messages = array();
  380. if (Event::handle('StartScriptMessages', array($this, &$messages))) {
  381. // Common messages needed for timeline views etc...
  382. // TRANS: Localized tooltip for '...' expansion button on overlong remote messages.
  383. $messages['showmore_tooltip'] = _m('TOOLTIP', 'Show more');
  384. // TRANS: Inline reply form submit button: submits a reply comment.
  385. $messages['reply_submit'] = _m('BUTTON', 'Reply');
  386. // TRANS: Placeholder text for inline reply form. Clicking in this box will turn it into a mini notice form.
  387. $messages['reply_placeholder'] = _m('Write a reply...');
  388. $messages = array_merge($messages, $this->getScriptMessages());
  389. Event::handle('EndScriptMessages', array($this, &$messages));
  390. }
  391. if (!empty($messages)) {
  392. $this->inlineScript('SN.messages=' . json_encode($messages));
  393. }
  394. return $messages;
  395. }
  396. /**
  397. * If the action will need localizable text strings, export them here like so:
  398. *
  399. * return array('pool_deepend' => _('Deep end'),
  400. * 'pool_shallow' => _('Shallow end'));
  401. *
  402. * The exported map will be available via SN.msg() to JS code:
  403. *
  404. * $('#pool').html('<div class="deepend"></div><div class="shallow"></div>');
  405. * $('#pool .deepend').text(SN.msg('pool_deepend'));
  406. * $('#pool .shallow').text(SN.msg('pool_shallow'));
  407. *
  408. * Exports a map of localized text strings to JavaScript code.
  409. *
  410. * Plugins can add to what's exported on any action by hooking the StartScriptMessages or
  411. * EndScriptMessages events and appending to the array. Try to avoid adding strings that won't
  412. * be used, as they'll be added to HTML output.
  413. */
  414. function getScriptMessages()
  415. {
  416. return array();
  417. }
  418. /**
  419. * Show OpenSearch headers
  420. *
  421. * @return nothing
  422. */
  423. function showOpenSearch()
  424. {
  425. $this->element('link', array('rel' => 'search',
  426. 'type' => 'application/opensearchdescription+xml',
  427. 'href' => common_local_url('opensearch', array('type' => 'people')),
  428. 'title' => common_config('site', 'name').' People Search'));
  429. $this->element('link', array('rel' => 'search', 'type' => 'application/opensearchdescription+xml',
  430. 'href' => common_local_url('opensearch', array('type' => 'notice')),
  431. 'title' => common_config('site', 'name').' Notice Search'));
  432. }
  433. /**
  434. * Show feed headers
  435. *
  436. * MAY overload
  437. *
  438. * @return nothing
  439. */
  440. function showFeeds()
  441. {
  442. $feeds = $this->getFeeds();
  443. if ($feeds) {
  444. foreach ($feeds as $feed) {
  445. $this->element('link', array('rel' => $feed->rel(),
  446. 'href' => $feed->url,
  447. 'type' => $feed->mimeType(),
  448. 'title' => $feed->title));
  449. }
  450. }
  451. }
  452. /**
  453. * Show description.
  454. *
  455. * SHOULD overload
  456. *
  457. * @return nothing
  458. */
  459. function showDescription()
  460. {
  461. // does nothing by default
  462. }
  463. /**
  464. * Show extra stuff in <head>.
  465. *
  466. * MAY overload
  467. *
  468. * @return nothing
  469. */
  470. function extraHead()
  471. {
  472. // does nothing by default
  473. }
  474. /**
  475. * Show body.
  476. *
  477. * Calls template methods
  478. *
  479. * @return nothing
  480. */
  481. function showBody()
  482. {
  483. $params = array('id' => $this->getActionName());
  484. if ($this->scoped instanceof Profile) {
  485. $params['class'] = 'user_in';
  486. }
  487. $this->elementStart('body', $params);
  488. $this->elementStart('div', array('id' => 'wrap'));
  489. if (Event::handle('StartShowHeader', array($this))) {
  490. $this->showHeader();
  491. $this->flush();
  492. Event::handle('EndShowHeader', array($this));
  493. }
  494. $this->showCore();
  495. $this->flush();
  496. if (Event::handle('StartShowFooter', array($this))) {
  497. $this->showFooter();
  498. $this->flush();
  499. Event::handle('EndShowFooter', array($this));
  500. }
  501. $this->elementEnd('div');
  502. $this->showScripts();
  503. $this->elementEnd('body');
  504. }
  505. /**
  506. * Show header of the page.
  507. *
  508. * Calls template methods
  509. *
  510. * @return nothing
  511. */
  512. function showHeader()
  513. {
  514. $this->elementStart('div', array('id' => 'header'));
  515. $this->showLogo();
  516. $this->showPrimaryNav();
  517. if (Event::handle('StartShowSiteNotice', array($this))) {
  518. $this->showSiteNotice();
  519. Event::handle('EndShowSiteNotice', array($this));
  520. }
  521. $this->elementEnd('div');
  522. }
  523. /**
  524. * Show configured logo.
  525. *
  526. * @return nothing
  527. */
  528. function showLogo()
  529. {
  530. $this->elementStart('address', array('id' => 'site_contact',
  531. 'class' => 'vcard'));
  532. if (Event::handle('StartAddressData', array($this))) {
  533. if (common_config('singleuser', 'enabled')) {
  534. $user = User::singleUser();
  535. $url = common_local_url('showstream',
  536. array('nickname' => $user->nickname));
  537. } else if (common_logged_in()) {
  538. $cur = common_current_user();
  539. $url = common_local_url('all', array('nickname' => $cur->nickname));
  540. } else {
  541. $url = common_local_url('public');
  542. }
  543. $this->elementStart('a', array('class' => 'url home bookmark',
  544. 'href' => $url));
  545. if (StatusNet::isHTTPS()) {
  546. $logoUrl = common_config('site', 'ssllogo');
  547. if (empty($logoUrl)) {
  548. // if logo is an uploaded file, try to fall back to HTTPS file URL
  549. $httpUrl = common_config('site', 'logo');
  550. if (!empty($httpUrl)) {
  551. $f = File::getKV('url', $httpUrl);
  552. if (!empty($f) && !empty($f->filename)) {
  553. // this will handle the HTTPS case
  554. $logoUrl = File::url($f->filename);
  555. }
  556. }
  557. }
  558. } else {
  559. $logoUrl = common_config('site', 'logo');
  560. }
  561. if (empty($logoUrl) && file_exists(Theme::file('logo.png'))) {
  562. // This should handle the HTTPS case internally
  563. $logoUrl = Theme::path('logo.png');
  564. }
  565. if (!empty($logoUrl)) {
  566. $this->element('img', array('class' => 'logo photo',
  567. 'src' => $logoUrl,
  568. 'alt' => common_config('site', 'name')));
  569. }
  570. $this->text(' ');
  571. $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
  572. $this->elementEnd('a');
  573. Event::handle('EndAddressData', array($this));
  574. }
  575. $this->elementEnd('address');
  576. }
  577. /**
  578. * Show primary navigation.
  579. *
  580. * @return nothing
  581. */
  582. function showPrimaryNav()
  583. {
  584. $this->elementStart('div', array('id' => 'site_nav_global_primary'));
  585. $user = common_current_user();
  586. if (!empty($user) || !common_config('site', 'private')) {
  587. $form = new SearchForm($this);
  588. $form->show();
  589. }
  590. $pn = new PrimaryNav($this);
  591. $pn->show();
  592. $this->elementEnd('div');
  593. }
  594. /**
  595. * Show site notice.
  596. *
  597. * @return nothing
  598. */
  599. function showSiteNotice()
  600. {
  601. // Revist. Should probably do an hAtom pattern here
  602. $text = common_config('site', 'notice');
  603. if ($text) {
  604. $this->elementStart('div', array('id' => 'site_notice',
  605. 'class' => 'system_notice'));
  606. $this->raw($text);
  607. $this->elementEnd('div');
  608. }
  609. }
  610. /**
  611. * Show notice form.
  612. *
  613. * MAY overload if no notice form needed... or direct message box????
  614. *
  615. * @return nothing
  616. */
  617. function showNoticeForm()
  618. {
  619. // TRANS: Tab on the notice form.
  620. $tabs = array('status' => array('title' => _m('TAB','Status'),
  621. 'href' => common_local_url('newnotice')));
  622. $this->elementStart('div', 'input_forms');
  623. if (Event::handle('StartShowEntryForms', array(&$tabs))) {
  624. $this->elementStart('ul', array('class' => 'nav',
  625. 'id' => 'input_form_nav'));
  626. foreach ($tabs as $tag => $data) {
  627. $tag = htmlspecialchars($tag);
  628. $attrs = array('id' => 'input_form_nav_'.$tag,
  629. 'class' => 'input_form_nav_tab');
  630. if ($tag == 'status') {
  631. // We're actually showing the placeholder form,
  632. // but we special-case the 'Status' tab as if
  633. // it were a small version of it.
  634. $attrs['class'] .= ' current';
  635. }
  636. $this->elementStart('li', $attrs);
  637. $this->element('a',
  638. array('onclick' => 'return SN.U.switchInputFormTab("'.$tag.'");',
  639. 'href' => $data['href']),
  640. $data['title']);
  641. $this->elementEnd('li');
  642. }
  643. $this->elementEnd('ul');
  644. $attrs = array('class' => 'input_form current',
  645. 'id' => 'input_form_placeholder');
  646. $this->elementStart('div', $attrs);
  647. $form = new NoticePlaceholderForm($this);
  648. $form->show();
  649. $this->elementEnd('div');
  650. foreach ($tabs as $tag => $data) {
  651. $attrs = array('class' => 'input_form',
  652. 'id' => 'input_form_'.$tag);
  653. $this->elementStart('div', $attrs);
  654. $form = null;
  655. if (Event::handle('StartMakeEntryForm', array($tag, $this, &$form))) {
  656. if ($tag == 'status') {
  657. $options = $this->noticeFormOptions();
  658. $form = new NoticeForm($this, $options);
  659. }
  660. Event::handle('EndMakeEntryForm', array($tag, $this, $form));
  661. }
  662. if (!empty($form)) {
  663. $form->show();
  664. }
  665. $this->elementEnd('div');
  666. }
  667. }
  668. $this->elementEnd('div');
  669. }
  670. function noticeFormOptions()
  671. {
  672. return array();
  673. }
  674. /**
  675. * Show anonymous message.
  676. *
  677. * SHOULD overload
  678. *
  679. * @return nothing
  680. */
  681. function showAnonymousMessage()
  682. {
  683. // needs to be defined by the class
  684. }
  685. /**
  686. * Show core.
  687. *
  688. * Shows local navigation, content block and aside.
  689. *
  690. * @return nothing
  691. */
  692. function showCore()
  693. {
  694. $this->elementStart('div', array('id' => 'core'));
  695. $this->elementStart('div', array('id' => 'aside_primary_wrapper'));
  696. $this->elementStart('div', array('id' => 'content_wrapper'));
  697. $this->elementStart('div', array('id' => 'site_nav_local_views_wrapper'));
  698. if (Event::handle('StartShowLocalNavBlock', array($this))) {
  699. $this->showLocalNavBlock();
  700. $this->flush();
  701. Event::handle('EndShowLocalNavBlock', array($this));
  702. }
  703. if (Event::handle('StartShowContentBlock', array($this))) {
  704. $this->showContentBlock();
  705. $this->flush();
  706. Event::handle('EndShowContentBlock', array($this));
  707. }
  708. if (Event::handle('StartShowAside', array($this))) {
  709. $this->showAside();
  710. $this->flush();
  711. Event::handle('EndShowAside', array($this));
  712. }
  713. $this->elementEnd('div');
  714. $this->elementEnd('div');
  715. $this->elementEnd('div');
  716. $this->elementEnd('div');
  717. }
  718. /**
  719. * Show local navigation block.
  720. *
  721. * @return nothing
  722. */
  723. function showLocalNavBlock()
  724. {
  725. // Need to have this ID for CSS; I'm too lazy to add it to
  726. // all menus
  727. $this->elementStart('div', array('id' => 'site_nav_local_views'));
  728. // Cheat cheat cheat!
  729. $this->showLocalNav();
  730. $this->elementEnd('div');
  731. }
  732. /**
  733. * If there's a logged-in user, show a bit of login context
  734. *
  735. * @return nothing
  736. */
  737. function showProfileBlock()
  738. {
  739. if (common_logged_in()) {
  740. $block = new DefaultProfileBlock($this);
  741. $block->show();
  742. }
  743. }
  744. /**
  745. * Show local navigation.
  746. *
  747. * SHOULD overload
  748. *
  749. * @return nothing
  750. */
  751. function showLocalNav()
  752. {
  753. $nav = new DefaultLocalNav($this);
  754. $nav->show();
  755. }
  756. /**
  757. * Show menu for an object (group, profile)
  758. *
  759. * This block will only show if a subclass has overridden
  760. * the showObjectNav() method.
  761. *
  762. * @return nothing
  763. */
  764. function showObjectNavBlock()
  765. {
  766. $rmethod = new ReflectionMethod($this, 'showObjectNav');
  767. $dclass = $rmethod->getDeclaringClass()->getName();
  768. if ($dclass != 'Action') {
  769. // Need to have this ID for CSS; I'm too lazy to add it to
  770. // all menus
  771. $this->elementStart('div', array('id' => 'site_nav_object',
  772. 'class' => 'section'));
  773. $this->showObjectNav();
  774. $this->elementEnd('div');
  775. }
  776. }
  777. /**
  778. * Show object navigation.
  779. *
  780. * If there are things to do with this object, show it here.
  781. *
  782. * @return nothing
  783. */
  784. function showObjectNav()
  785. {
  786. /* Nothing here. */
  787. }
  788. /**
  789. * Show content block.
  790. *
  791. * @return nothing
  792. */
  793. function showContentBlock()
  794. {
  795. $this->elementStart('div', array('id' => 'content'));
  796. if (common_logged_in()) {
  797. if (Event::handle('StartShowNoticeForm', array($this))) {
  798. $this->showNoticeForm();
  799. Event::handle('EndShowNoticeForm', array($this));
  800. }
  801. }
  802. if (Event::handle('StartShowPageTitle', array($this))) {
  803. $this->showPageTitle();
  804. Event::handle('EndShowPageTitle', array($this));
  805. }
  806. $this->showPageNoticeBlock();
  807. $this->elementStart('div', array('id' => 'content_inner'));
  808. // show the actual content (forms, lists, whatever)
  809. $this->showContent();
  810. $this->elementEnd('div');
  811. $this->elementEnd('div');
  812. }
  813. /**
  814. * Show page title.
  815. *
  816. * @return nothing
  817. */
  818. function showPageTitle()
  819. {
  820. $this->element('h1', null, $this->title());
  821. }
  822. /**
  823. * Show page notice block.
  824. *
  825. * Only show the block if a subclassed action has overrided
  826. * Action::showPageNotice(), or an event handler is registered for
  827. * the StartShowPageNotice event, in which case we assume the
  828. * 'page_notice' definition list is desired. This is to prevent
  829. * empty 'page_notice' definition lists from being output everywhere.
  830. *
  831. * @return nothing
  832. */
  833. function showPageNoticeBlock()
  834. {
  835. $rmethod = new ReflectionMethod($this, 'showPageNotice');
  836. $dclass = $rmethod->getDeclaringClass()->getName();
  837. if ($dclass != 'Action' || Event::hasHandler('StartShowPageNotice')) {
  838. $this->elementStart('div', array('id' => 'page_notice',
  839. 'class' => 'system_notice'));
  840. if (Event::handle('StartShowPageNotice', array($this))) {
  841. $this->showPageNotice();
  842. Event::handle('EndShowPageNotice', array($this));
  843. }
  844. $this->elementEnd('div');
  845. }
  846. }
  847. /**
  848. * Show page notice.
  849. *
  850. * SHOULD overload (unless there's not a notice)
  851. *
  852. * @return nothing
  853. */
  854. function showPageNotice()
  855. {
  856. }
  857. /**
  858. * Show content.
  859. *
  860. * MUST overload (unless there's not a notice)
  861. *
  862. * @return nothing
  863. */
  864. protected function showContent()
  865. {
  866. }
  867. /**
  868. * Show Aside.
  869. *
  870. * @return nothing
  871. */
  872. function showAside()
  873. {
  874. $this->elementStart('div', array('id' => 'aside_primary',
  875. 'class' => 'aside'));
  876. $this->showProfileBlock();
  877. if (Event::handle('StartShowObjectNavBlock', array($this))) {
  878. $this->showObjectNavBlock();
  879. Event::handle('EndShowObjectNavBlock', array($this));
  880. }
  881. if (Event::handle('StartShowSections', array($this))) {
  882. $this->showSections();
  883. Event::handle('EndShowSections', array($this));
  884. }
  885. if (Event::handle('StartShowExportData', array($this))) {
  886. $this->showExportData();
  887. Event::handle('EndShowExportData', array($this));
  888. }
  889. $this->elementEnd('div');
  890. }
  891. /**
  892. * Show export data feeds.
  893. *
  894. * @return void
  895. */
  896. function showExportData()
  897. {
  898. $feeds = $this->getFeeds();
  899. if ($feeds) {
  900. $fl = new FeedList($this);
  901. $fl->show($feeds);
  902. }
  903. }
  904. /**
  905. * Show sections.
  906. *
  907. * SHOULD overload
  908. *
  909. * @return nothing
  910. */
  911. function showSections()
  912. {
  913. // for each section, show it
  914. }
  915. /**
  916. * Show footer.
  917. *
  918. * @return nothing
  919. */
  920. function showFooter()
  921. {
  922. $this->elementStart('div', array('id' => 'footer'));
  923. if (Event::handle('StartShowInsideFooter', array($this))) {
  924. $this->showSecondaryNav();
  925. $this->showLicenses();
  926. Event::handle('EndShowInsideFooter', array($this));
  927. }
  928. $this->elementEnd('div');
  929. }
  930. /**
  931. * Show secondary navigation.
  932. *
  933. * @return nothing
  934. */
  935. function showSecondaryNav()
  936. {
  937. $sn = new SecondaryNav($this);
  938. $sn->show();
  939. }
  940. /**
  941. * Show licenses.
  942. *
  943. * @return nothing
  944. */
  945. function showLicenses()
  946. {
  947. $this->showGNUsocialLicense();
  948. $this->showContentLicense();
  949. }
  950. /**
  951. * Show GNU social license.
  952. *
  953. * @return nothing
  954. */
  955. function showGNUsocialLicense()
  956. {
  957. if (common_config('site', 'broughtby')) {
  958. // TRANS: First sentence of the GNU social site license. Used if 'broughtby' is set.
  959. // TRANS: Text between [] is a link description, text between () is the link itself.
  960. // TRANS: Make sure there is no whitespace between "]" and "(".
  961. // TRANS: "%%site.broughtby%%" is the value of the variable site.broughtby
  962. $instr = _('**%%site.name%%** is a social network, courtesy of [%%site.broughtby%%](%%site.broughtbyurl%%).');
  963. } else {
  964. // TRANS: First sentence of the GNU social site license. Used if 'broughtby' is not set.
  965. $instr = _('**%%site.name%%** is a social network.');
  966. }
  967. $instr .= ' ';
  968. // TRANS: Second sentence of the GNU social site license. Mentions the GNU social source code license.
  969. // TRANS: Make sure there is no whitespace between "]" and "(".
  970. // TRANS: [%1$s](%2$s) is a link description followed by the link itself
  971. // TRANS: %3$s is the version of GNU social that is being used.
  972. $instr .= sprintf(_('It runs on [%1$s](%2$s), version %3$s, available under the [GNU Affero General Public License](http://www.fsf.org/licensing/licenses/agpl-3.0.html).'), GNUSOCIAL_ENGINE, GNUSOCIAL_ENGINE_URL, GNUSOCIAL_VERSION);
  973. $output = common_markup_to_html($instr);
  974. $this->raw($output);
  975. // do it
  976. }
  977. /**
  978. * Show content license.
  979. *
  980. * @return nothing
  981. */
  982. function showContentLicense()
  983. {
  984. if (Event::handle('StartShowContentLicense', array($this))) {
  985. switch (common_config('license', 'type')) {
  986. case 'private':
  987. // TRANS: Content license displayed when license is set to 'private'.
  988. // TRANS: %1$s is the site name.
  989. $this->element('p', null, sprintf(_('Content and data of %1$s are private and confidential.'),
  990. common_config('site', 'name')));
  991. // fall through
  992. case 'allrightsreserved':
  993. if (common_config('license', 'owner')) {
  994. // TRANS: Content license displayed when license is set to 'allrightsreserved'.
  995. // TRANS: %1$s is the copyright owner.
  996. $this->element('p', null, sprintf(_('Content and data copyright by %1$s. All rights reserved.'),
  997. common_config('license', 'owner')));
  998. } else {
  999. // TRANS: Content license displayed when license is set to 'allrightsreserved' and no owner is set.
  1000. $this->element('p', null, _('Content and data copyright by contributors. All rights reserved.'));
  1001. }
  1002. break;
  1003. case 'cc': // fall through
  1004. default:
  1005. $this->elementStart('p');
  1006. $image = common_config('license', 'image');
  1007. $sslimage = common_config('license', 'sslimage');
  1008. if (StatusNet::isHTTPS()) {
  1009. if (!empty($sslimage)) {
  1010. $url = $sslimage;
  1011. } else if (preg_match('#^http://i.creativecommons.org/#', $image)) {
  1012. // CC support HTTPS on their images
  1013. $url = preg_replace('/^http/', 'https', $image);
  1014. } else {
  1015. // Better to show mixed content than no content
  1016. $url = $image;
  1017. }
  1018. } else {
  1019. $url = $image;
  1020. }
  1021. $this->element('img', array('id' => 'license_cc',
  1022. 'src' => $url,
  1023. 'alt' => common_config('license', 'title'),
  1024. 'width' => '80',
  1025. 'height' => '15'));
  1026. $this->text(' ');
  1027. // TRANS: license message in footer.
  1028. // TRANS: %1$s is the site name, %2$s is a link to the license URL, with a licence name set in configuration.
  1029. $notice = _('All %1$s content and data are available under the %2$s license.');
  1030. $link = "<a class=\"license\" rel=\"external license\" href=\"" .
  1031. htmlspecialchars(common_config('license', 'url')) .
  1032. "\">" .
  1033. htmlspecialchars(common_config('license', 'title')) .
  1034. "</a>";
  1035. $this->raw(sprintf(htmlspecialchars($notice),
  1036. htmlspecialchars(common_config('site', 'name')),
  1037. $link));
  1038. $this->elementEnd('p');
  1039. break;
  1040. }
  1041. Event::handle('EndShowContentLicense', array($this));
  1042. }
  1043. }
  1044. /**
  1045. * Return last modified, if applicable.
  1046. *
  1047. * MAY override
  1048. *
  1049. * @return string last modified http header
  1050. */
  1051. function lastModified()
  1052. {
  1053. // For comparison with If-Last-Modified
  1054. // If not applicable, return null
  1055. return null;
  1056. }
  1057. /**
  1058. * Return etag, if applicable.
  1059. *
  1060. * MAY override
  1061. *
  1062. * @return string etag http header
  1063. */
  1064. function etag()
  1065. {
  1066. return null;
  1067. }
  1068. /**
  1069. * Return true if read only.
  1070. *
  1071. * MAY override
  1072. *
  1073. * @param array $args other arguments
  1074. *
  1075. * @return boolean is read only action?
  1076. */
  1077. function isReadOnly($args)
  1078. {
  1079. return false;
  1080. }
  1081. /**
  1082. * Returns query argument or default value if not found
  1083. *
  1084. * @param string $key requested argument
  1085. * @param string $def default value to return if $key is not provided
  1086. *
  1087. * @return boolean is read only action?
  1088. */
  1089. function arg($key, $def=null)
  1090. {
  1091. if (array_key_exists($key, $this->args)) {
  1092. return $this->args[$key];
  1093. } else {
  1094. return $def;
  1095. }
  1096. }
  1097. /**
  1098. * Returns trimmed query argument or default value if not found
  1099. *
  1100. * @param string $key requested argument
  1101. * @param string $def default value to return if $key is not provided
  1102. *
  1103. * @return boolean is read only action?
  1104. */
  1105. function trimmed($key, $def=null)
  1106. {
  1107. $arg = $this->arg($key, $def);
  1108. return is_string($arg) ? trim($arg) : $arg;
  1109. }
  1110. /**
  1111. * Handler method
  1112. *
  1113. * @return boolean is read only action?
  1114. */
  1115. protected function handle()
  1116. {
  1117. header('Vary: Accept-Encoding,Cookie');
  1118. $lm = $this->lastModified();
  1119. $etag = $this->etag();
  1120. if ($etag) {
  1121. header('ETag: ' . $etag);
  1122. }
  1123. if ($lm) {
  1124. header('Last-Modified: ' . date(DATE_RFC1123, $lm));
  1125. if ($this->isCacheable()) {
  1126. header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
  1127. header( "Cache-Control: private, must-revalidate, max-age=0" );
  1128. header( "Pragma:");
  1129. }
  1130. }
  1131. $checked = false;
  1132. if ($etag) {
  1133. $if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ?
  1134. $_SERVER['HTTP_IF_NONE_MATCH'] : null;
  1135. if ($if_none_match) {
  1136. // If this check fails, ignore the if-modified-since below.
  1137. $checked = true;
  1138. if ($this->_hasEtag($etag, $if_none_match)) {
  1139. header('HTTP/1.1 304 Not Modified');
  1140. // Better way to do this?
  1141. exit(0);
  1142. }
  1143. }
  1144. }
  1145. if (!$checked && $lm && array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
  1146. $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
  1147. $ims = strtotime($if_modified_since);
  1148. if ($lm <= $ims) {
  1149. header('HTTP/1.1 304 Not Modified');
  1150. // Better way to do this?
  1151. exit(0);
  1152. }
  1153. }
  1154. }
  1155. /**
  1156. * Is this action cacheable?
  1157. *
  1158. * If the action returns a last-modified
  1159. *
  1160. * @param array $argarray is ignored since it's now passed in in prepare()
  1161. *
  1162. * @return boolean is read only action?
  1163. */
  1164. function isCacheable()
  1165. {
  1166. return true;
  1167. }
  1168. /**
  1169. * Has etag? (private)
  1170. *
  1171. * @param string $etag etag http header
  1172. * @param string $if_none_match ifNoneMatch http header
  1173. *
  1174. * @return boolean
  1175. */
  1176. function _hasEtag($etag, $if_none_match)
  1177. {
  1178. $etags = explode(',', $if_none_match);
  1179. return in_array($etag, $etags) || in_array('*', $etags);
  1180. }
  1181. /**
  1182. * Boolean understands english (yes, no, true, false)
  1183. *
  1184. * @param string $key query key we're interested in
  1185. * @param string $def default value
  1186. *
  1187. * @return boolean interprets yes/no strings as boolean
  1188. */
  1189. function boolean($key, $def=false)
  1190. {
  1191. $arg = strtolower($this->trimmed($key));
  1192. if (is_null($arg)) {
  1193. return $def;
  1194. } else if (in_array($arg, array('true', 'yes', '1', 'on'))) {
  1195. return true;
  1196. } else if (in_array($arg, array('false', 'no', '0'))) {
  1197. return false;
  1198. } else {
  1199. return $def;
  1200. }
  1201. }
  1202. /**
  1203. * Integer value of an argument
  1204. *
  1205. * @param string $key query key we're interested in
  1206. * @param string $defValue optional default value (default null)
  1207. * @param string $maxValue optional max value (default null)
  1208. * @param string $minValue optional min value (default null)
  1209. *
  1210. * @return integer integer value
  1211. */
  1212. function int($key, $defValue=null, $maxValue=null, $minValue=null)
  1213. {
  1214. $arg = intval($this->arg($key));
  1215. if (!is_numeric($this->arg($key)) || $arg != $this->arg($key)) {
  1216. return $defValue;
  1217. }
  1218. if (!is_null($maxValue)) {
  1219. $arg = min($arg, $maxValue);
  1220. }
  1221. if (!is_null($minValue)) {
  1222. $arg = max($arg, $minValue);
  1223. }
  1224. return $arg;
  1225. }
  1226. /**
  1227. * Server error
  1228. *
  1229. * @param string $msg error message to display
  1230. * @param integer $code http error code, 500 by default
  1231. *
  1232. * @return nothing
  1233. */
  1234. function serverError($msg, $code=500, $format=null)
  1235. {
  1236. if ($format === null) {
  1237. $format = $this->format;
  1238. }
  1239. common_debug("Server error '{$code}' on '{$this->action}': {$msg}", __FILE__);
  1240. if (!array_key_exists($code, ServerErrorAction::$status)) {
  1241. $code = 500;
  1242. }
  1243. $status_string = ServerErrorAction::$status[$code];
  1244. switch ($format) {
  1245. case 'xml':
  1246. header("HTTP/1.1 {$code} {$status_string}");
  1247. $this->initDocument('xml');
  1248. $this->elementStart('hash');
  1249. $this->element('error', null, $msg);
  1250. $this->element('request', null, $_SERVER['REQUEST_URI']);
  1251. $this->elementEnd('hash');
  1252. $this->endDocument('xml');
  1253. break;
  1254. case 'json':
  1255. if (!isset($this->callback)) {
  1256. header("HTTP/1.1 {$code} {$status_string}");
  1257. }
  1258. $this->initDocument('json');
  1259. $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
  1260. print(json_encode($error_array));
  1261. $this->endDocument('json');
  1262. break;
  1263. default:
  1264. throw new ServerException($msg, $code);
  1265. }
  1266. exit((int)$code);
  1267. }
  1268. /**
  1269. * Client error
  1270. *
  1271. * @param string $msg error message to display
  1272. * @param integer $code http error code, 400 by default
  1273. * @param string $format error format (json, xml, text) for ApiAction
  1274. *
  1275. * @return nothing
  1276. * @throws ClientException always
  1277. */
  1278. function clientError($msg, $code=400, $format=null)
  1279. {
  1280. // $format is currently only relevant for an ApiAction anyway
  1281. if ($format === null) {
  1282. $format = $this->format;
  1283. }
  1284. common_debug("User error '{$code}' on '{$this->action}': {$msg}", __FILE__);
  1285. if (!array_key_exists($code, ClientErrorAction::$status)) {
  1286. $code = 400;
  1287. }
  1288. $status_string = ClientErrorAction::$status[$code];
  1289. switch ($format) {
  1290. case 'xml':
  1291. header("HTTP/1.1 {$code} {$status_string}");
  1292. $this->initDocument('xml');
  1293. $this->elementStart('hash');
  1294. $this->element('error', null, $msg);
  1295. $this->element('request', null, $_SERVER['REQUEST_URI']);
  1296. $this->elementEnd('hash');
  1297. $this->endDocument('xml');
  1298. break;
  1299. case 'json':
  1300. if (!isset($this->callback)) {
  1301. header("HTTP/1.1 {$code} {$status_string}");
  1302. }
  1303. $this->initDocument('json');
  1304. $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
  1305. $this->text(json_encode($error_array));
  1306. $this->endDocument('json');
  1307. break;
  1308. case 'text':
  1309. header("HTTP/1.1 {$code} {$status_string}");
  1310. header('Content-Type: text/plain; charset=utf-8');
  1311. echo $msg;
  1312. break;
  1313. default:
  1314. throw new ClientException($msg, $code);
  1315. }
  1316. exit((int)$code);
  1317. }
  1318. /**
  1319. * If not logged in, take appropriate action (redir or exception)
  1320. *
  1321. * @param boolean $redir Redirect to login if not logged in
  1322. *
  1323. * @return boolean true if logged in (never returns if not)
  1324. */
  1325. public function checkLogin($redir=true)
  1326. {
  1327. if (common_logged_in()) {
  1328. return true;
  1329. }
  1330. if ($redir==true) {
  1331. common_set_returnto($_SERVER['REQUEST_URI']);
  1332. common_redirect(common_local_url('login'));
  1333. }
  1334. // TRANS: Error message displayed when trying to perform an action that requires a logged in user.
  1335. $this->clientError(_('Not logged in.'), 403);
  1336. }
  1337. /**
  1338. * Returns the current URL
  1339. *
  1340. * @return string current URL
  1341. */
  1342. function selfUrl()
  1343. {
  1344. list($action, $args) = $this->returnToArgs();
  1345. return common_local_url($action, $args);
  1346. }
  1347. /**
  1348. * Returns arguments sufficient for re-constructing URL
  1349. *
  1350. * @return array two elements: action, other args
  1351. */
  1352. function returnToArgs()
  1353. {
  1354. $action = $this->getActionName();
  1355. $args = $this->args;
  1356. unset($args['action']);
  1357. if (common_config('site', 'fancy')) {
  1358. unset($args['p']);
  1359. }
  1360. if (array_key_exists('submit', $args)) {
  1361. unset($args['submit']);
  1362. }
  1363. foreach (array_keys($_COOKIE) as $cookie) {
  1364. unset($args[$cookie]);
  1365. }
  1366. return array($action, $args);
  1367. }
  1368. /**
  1369. * Generate a menu item
  1370. *
  1371. * @param string $url menu URL
  1372. * @param string $text menu name
  1373. * @param string $title title attribute, null by default
  1374. * @param boolean $is_selected current menu item, false by default
  1375. * @param string $id element id, null by default
  1376. *
  1377. * @return nothing
  1378. */
  1379. function menuItem($url, $text, $title=null, $is_selected=false, $id=null, $class=null)
  1380. {
  1381. // Added @id to li for some control.
  1382. // XXX: We might want to move this to htmloutputter.php
  1383. $lattrs = array();
  1384. $classes = array();
  1385. if ($class !== null) {
  1386. $classes[] = trim($class);
  1387. }
  1388. if ($is_selected) {
  1389. $classes[] = 'current';
  1390. }
  1391. if (!empty($classes)) {
  1392. $lattrs['class'] = implode(' ', $classes);
  1393. }
  1394. if (!is_null($id)) {
  1395. $lattrs['id'] = $id;
  1396. }
  1397. $this->elementStart('li', $lattrs);
  1398. $attrs['href'] = $url;
  1399. if ($title) {
  1400. $attrs['title'] = $title;
  1401. }
  1402. $this->element('a', $attrs, $text);

Large files files are truncated, but you can click here to view the full file