htmloutputter.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * Low-level generator for HTML
  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 Output
  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')) {
  31. exit(1);
  32. }
  33. // Can include XHTML options but these are too fragile in practice.
  34. define('PAGE_TYPE_PREFS', 'text/html');
  35. /**
  36. * Low-level generator for HTML
  37. *
  38. * Abstracts some of the code necessary for HTML generation. Especially
  39. * has methods for generating HTML form elements. Note that these have
  40. * been created kind of haphazardly, not with an eye to making a general
  41. * HTML-creation class.
  42. *
  43. * @category Output
  44. * @package StatusNet
  45. * @author Evan Prodromou <evan@status.net>
  46. * @author Sarven Capadisli <csarven@status.net>
  47. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  48. * @link http://status.net/
  49. *
  50. * @see Action
  51. * @see XMLOutputter
  52. */
  53. class HTMLOutputter extends XMLOutputter
  54. {
  55. protected $DTD = ['doctype' => 'html',
  56. 'spec' => '-//W3C//DTD XHTML 1.0 Strict//EN',
  57. 'uri' => 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'];
  58. /**
  59. * Constructor
  60. *
  61. * Just wraps the XMLOutputter constructor.
  62. *
  63. * @param string $output URI to output to, default = stdout
  64. * @param boolean $indent Whether to indent output, default true
  65. */
  66. public function __construct($output = 'php://output', $indent = null)
  67. {
  68. parent::__construct($output, $indent);
  69. }
  70. /**
  71. * Start an HTML document
  72. *
  73. * If $type isn't specified, will attempt to do content negotiation.
  74. *
  75. * Attempts to do content negotiation for language, also.
  76. *
  77. * @param string $type MIME type to use; default is to do negotation.
  78. *
  79. * @return void
  80. * @throws ClientException
  81. * @todo extract content negotiation code to an HTTP module or class.
  82. *
  83. */
  84. public function startHTML($type = null)
  85. {
  86. if (!$type) {
  87. $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ?
  88. $_SERVER['HTTP_ACCEPT'] : null;
  89. // XXX: allow content negotiation for RDF, RSS, or XRDS
  90. $cp = common_accept_to_prefs($httpaccept);
  91. $sp = common_accept_to_prefs(PAGE_TYPE_PREFS);
  92. $type = common_negotiate_type($cp, $sp);
  93. if (!$type) {
  94. // TRANS: Client exception 406
  95. throw new ClientException(_('This page is not available in a ' .
  96. 'media type you accept'), 406);
  97. }
  98. }
  99. header('Content-Type: ' . $type);
  100. // Output anti-framing headers to prevent clickjacking (respected by newer
  101. // browsers).
  102. if (common_config('javascript', 'bustframes')) {
  103. header('X-XSS-Protection: 1; mode=block'); // detect XSS Reflection attacks
  104. header('X-Frame-Options: SAMEORIGIN'); // no rendering if origin mismatch
  105. }
  106. $this->extraHeaders();
  107. if (preg_match("/.*\/.*xml/", $type)) {
  108. // Required for XML documents
  109. $this->startXML();
  110. }
  111. $this->writeDTD();
  112. $language = $this->getLanguage();
  113. $attrs = [
  114. 'xmlns' => 'http://www.w3.org/1999/xhtml',
  115. 'xml:lang' => $language,
  116. 'lang' => $language
  117. ];
  118. if (Event::handle('StartHtmlElement', [$this, &$attrs])) {
  119. $this->elementStart('html', $attrs);
  120. Event::handle('EndHtmlElement', [$this, &$attrs]);
  121. }
  122. }
  123. /**
  124. * To specify additional HTTP headers for the action
  125. *
  126. * @return void
  127. */
  128. public function extraHeaders()
  129. {
  130. // Needs to be overloaded
  131. }
  132. protected function writeDTD()
  133. {
  134. $this->xw->writeDTD(
  135. $this->DTD['doctype'],
  136. $this->DTD['spec'],
  137. $this->DTD['uri']
  138. );
  139. }
  140. public function getLanguage()
  141. {
  142. // FIXME: correct language for interface
  143. return common_language();
  144. }
  145. public function setDTD($doctype, $spec, $uri)
  146. {
  147. $this->DTD = ['doctype' => $doctype, 'spec' => $spec, 'uri' => $uri];
  148. }
  149. /**
  150. * Ends an HTML document
  151. *
  152. * @return void
  153. */
  154. public function endHTML()
  155. {
  156. $this->elementEnd('html');
  157. $this->endXML();
  158. }
  159. /**
  160. * Output an HTML text input element
  161. *
  162. * Despite the name, it is specifically for outputting a
  163. * text input element, not other <input> elements. It outputs
  164. * a cluster of elements, including a <label> and an associated
  165. * instructions span.
  166. *
  167. * If $attrs['type'] does not exist it will be set to 'text'.
  168. *
  169. * @param string $id element ID, must be unique on page
  170. * @param string $label text of label for the element
  171. * @param string $value value of the element, default null
  172. * @param string $instructions instructions for valid input
  173. * @param string $name name of the element; if null, the id will be used
  174. * @param bool $required HTML5 required attribute (exclude when false)
  175. * @param array $attrs Initial attributes manually set in an array (overwritten by previous options)
  176. *
  177. * @return void
  178. * @todo add a $maxLength parameter
  179. * @todo add a $size parameter
  180. *
  181. */
  182. public function input($id, $label, $value = null, $instructions = null, $name = null, $required = false, array $attrs = [])
  183. {
  184. $this->element('label', ['for' => $id], $label);
  185. if (!array_key_exists('type', $attrs)) {
  186. $attrs['type'] = 'text';
  187. }
  188. $attrs['id'] = $id;
  189. $attrs['name'] = is_null($name) ? $id : $name;
  190. if (array_key_exists('placeholder', $attrs) && (is_null($attrs['placeholder']) || $attrs['placeholder'] === '')) {
  191. // If placeholder is type-aware equal to '' or null, unset it as we apparently don't want a placeholder value
  192. unset($attrs['placeholder']);
  193. } else {
  194. // If the placeholder is set use it, or use the label as fallback.
  195. $attrs['placeholder'] = isset($attrs['placeholder']) ? $attrs['placeholder'] : $label;
  196. }
  197. if (!is_null($value)) { // value can be 0 or ''
  198. $attrs['value'] = $value;
  199. }
  200. if (!empty($required)) {
  201. $attrs['required'] = 'required';
  202. }
  203. $this->element('input', $attrs);
  204. if ($instructions) {
  205. $this->element('p', 'form_guide', $instructions);
  206. }
  207. }
  208. /**
  209. * output an HTML checkbox and associated elements
  210. *
  211. * Note that the value is default 'true' (the string), which can
  212. * be used by Action::boolean()
  213. *
  214. * @param string $id element ID, must be unique on page
  215. * @param string $label text of label for the element
  216. * @param bool $checked if the box is checked, default false
  217. * @param string $instructions instructions for valid input
  218. * @param string $value value of the checkbox, default 'true'
  219. * @param bool $disabled show the checkbox disabled, default false
  220. *
  221. * @return void
  222. *
  223. * @todo add a $name parameter
  224. */
  225. public function checkbox(
  226. $id,
  227. $label,
  228. $checked = false,
  229. $instructions = null,
  230. $value = 'true',
  231. $disabled = false
  232. )
  233. {
  234. $attrs = ['name' => $id,
  235. 'type' => 'checkbox',
  236. 'class' => 'checkbox',
  237. 'id' => $id];
  238. if ($value) {
  239. $attrs['value'] = $value;
  240. }
  241. if ($checked) {
  242. $attrs['checked'] = 'checked';
  243. }
  244. if ($disabled) {
  245. $attrs['disabled'] = 'true';
  246. }
  247. $this->element('input', $attrs);
  248. $this->text(' ');
  249. $this->element(
  250. 'label',
  251. ['class' => 'checkbox',
  252. 'for' => $id],
  253. $label
  254. );
  255. $this->text(' ');
  256. if ($instructions) {
  257. $this->element('p', 'form_guide', $instructions);
  258. }
  259. }
  260. /**
  261. * output an HTML combobox/select and associated elements
  262. *
  263. * $content is an array of key-value pairs for the dropdown, where
  264. * the key is the option value attribute and the value is the option
  265. * text. (Careful on the overuse of 'value' here.)
  266. *
  267. * @param string $id element ID, must be unique on page
  268. * @param string $label text of label for the element
  269. * @param array $content options array, value => text
  270. * @param string $instructions instructions for valid input
  271. * @param bool $blank_select whether to have a blank entry, default false
  272. * @param string $selected selected value, default null
  273. *
  274. * @return void
  275. *
  276. * @todo add a $name parameter
  277. */
  278. public function dropdown(
  279. $id,
  280. $label,
  281. $content,
  282. $instructions = null,
  283. $blank_select = false,
  284. $selected = null
  285. )
  286. {
  287. $this->element('label', ['for' => $id], $label);
  288. $this->elementStart('select', ['id' => $id, 'name' => $id]);
  289. if ($blank_select) {
  290. $this->element('option', ['value' => '']);
  291. }
  292. foreach ($content as $value => $option) {
  293. if ($value == $selected) {
  294. $this->element(
  295. 'option',
  296. ['value' => $value,
  297. 'selected' => 'selected'],
  298. $option
  299. );
  300. } else {
  301. $this->element('option', ['value' => $value], $option);
  302. }
  303. }
  304. $this->elementEnd('select');
  305. if ($instructions) {
  306. $this->element('p', 'form_guide', $instructions);
  307. }
  308. }
  309. /**
  310. * output an HTML hidden element
  311. *
  312. * $id is re-used as name
  313. *
  314. * @param string $id element ID, must be unique on page
  315. * @param string $value hidden element value, default null
  316. * @param string $name name, if different than ID
  317. *
  318. * @return void
  319. */
  320. public function hidden($id, $value, $name = null)
  321. {
  322. $this->element('input', ['name' => $name ?: $id,
  323. 'type' => 'hidden',
  324. 'id' => $id,
  325. 'value' => $value]);
  326. }
  327. /**
  328. * output an HTML password input and associated elements
  329. *
  330. * @param string $id element ID, must be unique on page
  331. * @param string $label text of label for the element
  332. * @param string $instructions instructions for valid input
  333. *
  334. * @return void
  335. *
  336. * @todo add a $name parameter
  337. */
  338. public function password($id, $label, $instructions = null)
  339. {
  340. $this->element('label', ['for' => $id], $label);
  341. $attrs = ['name' => $id,
  342. 'type' => 'password',
  343. 'class' => 'password',
  344. 'id' => $id];
  345. $this->element('input', $attrs);
  346. if ($instructions) {
  347. $this->element('p', 'form_guide', $instructions);
  348. }
  349. }
  350. /**
  351. * output an HTML submit input and associated elements
  352. *
  353. * @param string $id element ID, must be unique on page
  354. * @param string $label text of the button
  355. * @param string $cls class of the button, default 'submit'
  356. * @param string $name name, if different than ID
  357. * @param string $title title text for the submit button
  358. *
  359. * @return void
  360. *
  361. * @todo add a $name parameter
  362. */
  363. public function submit($id, $label, $cls = 'submit', $name = null, $title = null)
  364. {
  365. $this->element('input', ['type' => 'submit',
  366. 'id' => $id,
  367. 'name' => $name ?: $id,
  368. 'class' => $cls,
  369. 'value' => $label,
  370. 'title' => $title]);
  371. }
  372. /**
  373. * output a script (almost always javascript) tag
  374. *
  375. * @param string $src relative or absolute script path
  376. * @param string $type 'type' attribute value of the tag
  377. *
  378. * @return void
  379. */
  380. public function script($src, $type = 'text/javascript')
  381. {
  382. if (Event::handle('StartScriptElement', [$this, &$src, &$type])) {
  383. $url = parse_url($src);
  384. if (empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) {
  385. // XXX: this seems like a big assumption
  386. if (strpos($src, 'plugins/') === 0 || strpos($src, 'local/') === 0) {
  387. $src = common_path($src, GNUsocial::isHTTPS()) . '?version=' . GNUSOCIAL_VERSION;
  388. } else {
  389. if (GNUsocial::isHTTPS()) {
  390. $server = common_config('javascript', 'sslserver');
  391. if (empty($server)) {
  392. if (is_string(common_config('site', 'sslserver')) &&
  393. mb_strlen(common_config('site', 'sslserver')) > 0) {
  394. $server = common_config('site', 'sslserver');
  395. } elseif (common_config('site', 'server')) {
  396. $server = common_config('site', 'server');
  397. }
  398. $path = common_config('site', 'path') . '/js/';
  399. } else {
  400. $path = common_config('javascript', 'sslpath');
  401. if (empty($path)) {
  402. $path = common_config('javascript', 'path');
  403. }
  404. }
  405. $protocol = 'https';
  406. } else {
  407. $path = common_config('javascript', 'path');
  408. if (empty($path)) {
  409. $path = common_config('site', 'path') . '/js/';
  410. }
  411. $server = common_config('javascript', 'server');
  412. if (empty($server)) {
  413. $server = common_config('site', 'server');
  414. }
  415. $protocol = 'http';
  416. }
  417. if ($path[strlen($path) - 1] != '/') {
  418. $path .= '/';
  419. }
  420. if ($path[0] != '/') {
  421. $path = '/' . $path;
  422. }
  423. $src = $protocol . '://' . $server . $path . $src . '?version=' . GNUSOCIAL_VERSION;
  424. }
  425. }
  426. $this->element(
  427. 'script',
  428. ['type' => $type,
  429. 'src' => $src],
  430. ' '
  431. );
  432. Event::handle('EndScriptElement', [$this, $src, $type]);
  433. }
  434. }
  435. /**
  436. * output a css link
  437. *
  438. * @param string $src relative path within the theme directory, or an absolute path
  439. * @param string $theme 'theme' that contains the stylesheet
  440. * @param string media 'media' attribute of the tag
  441. *
  442. * @return void
  443. */
  444. public function cssLink($src, $theme = null, $media = null)
  445. {
  446. if (Event::handle('StartCssLinkElement', [$this, &$src, &$theme, &$media])) {
  447. $url = parse_url($src);
  448. if (empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) {
  449. if (file_exists(Theme::file($src, $theme))) {
  450. $src = Theme::path($src, $theme);
  451. } else {
  452. $src = common_path($src, GNUsocial::isHTTPS());
  453. }
  454. $src .= '?version=' . GNUSOCIAL_VERSION;
  455. }
  456. $this->element('link', ['rel' => 'stylesheet',
  457. 'type' => 'text/css',
  458. 'href' => $src,
  459. 'media' => $media]);
  460. Event::handle('EndCssLinkElement', [$this, $src, $theme, $media]);
  461. }
  462. }
  463. /**
  464. * output a style (almost always css) tag with inline
  465. * code.
  466. *
  467. * @param string $code code to put in the style tag
  468. * @param string $type 'type' attribute value of the tag
  469. * @param string $media 'media' attribute value of the tag
  470. *
  471. * @return void
  472. */
  473. public function style($code, $type = 'text/css', $media = null)
  474. {
  475. if (Event::handle('StartStyleElement', [$this, &$code, &$type, &$media])) {
  476. $this->elementStart('style', ['type' => $type, 'media' => $media]);
  477. $this->raw($code);
  478. $this->elementEnd('style');
  479. Event::handle('EndStyleElement', [$this, $code, $type, $media]);
  480. }
  481. }
  482. /**
  483. * output an HTML textarea and associated elements
  484. *
  485. * @param string $id element ID, must be unique on page
  486. * @param string $label text of label for the element
  487. * @param string $content content of the textarea, default none
  488. * @param string $instructions instructions for valid input
  489. * @param string $name name of textarea; if null, $id will be used
  490. * @param int $cols number of columns
  491. * @param int $rows number of rows
  492. * @param bool $required HTML5 required attribute (exclude when false)
  493. *
  494. * @return void
  495. */
  496. public function textarea(
  497. $id,
  498. $label,
  499. $content = null,
  500. $instructions = null,
  501. $name = null,
  502. $cols = null,
  503. $rows = null,
  504. $required = false
  505. )
  506. {
  507. $this->element('label', ['for' => $id], $label);
  508. $attrs = [
  509. 'rows' => 3,
  510. 'cols' => 40,
  511. 'id' => $id
  512. ];
  513. $attrs['name'] = is_null($name) ? $id : $name;
  514. if ($cols != null) {
  515. $attrs['cols'] = $cols;
  516. }
  517. if ($rows != null) {
  518. $attrs['rows'] = $rows;
  519. }
  520. if (!empty($required)) {
  521. $attrs['required'] = 'required';
  522. }
  523. $this->element(
  524. 'textarea',
  525. $attrs,
  526. $content
  527. );
  528. if ($instructions) {
  529. $this->element('p', 'form_guide', $instructions);
  530. }
  531. }
  532. /**
  533. * Internal script to autofocus the given element on page onload.
  534. *
  535. * @param string $id element ID, must refer to an existing element
  536. *
  537. * @return void
  538. *
  539. */
  540. public function autofocus($id)
  541. {
  542. $this->inlineScript(
  543. ' $(document).ready(function() {' .
  544. ' var el = $("#' . $id . '");' .
  545. ' if (el.length) { el.focus(); }' .
  546. ' });'
  547. );
  548. }
  549. /**
  550. * output a script (almost always javascript) tag with inline
  551. * code.
  552. *
  553. * @param string $code code to put in the script tag
  554. * @param string $type 'type' attribute value of the tag
  555. *
  556. * @return void
  557. */
  558. public function inlineScript($code, $type = 'text/javascript')
  559. {
  560. if (Event::handle('StartInlineScriptElement', [$this, &$code, &$type])) {
  561. $this->elementStart('script', ['type' => $type]);
  562. if ($type == 'text/javascript') {
  563. $this->raw('/*<![CDATA[*/ '); // XHTML compat
  564. }
  565. $this->raw($code);
  566. if ($type == 'text/javascript') {
  567. $this->raw(' /*]]>*/'); // XHTML compat
  568. }
  569. $this->elementEnd('script');
  570. Event::handle('EndInlineScriptElement', [$this, $code, $type]);
  571. }
  572. }
  573. }