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