htmloutputter.php 20.4 KB
Newer Older
1 2
<?php
/**
3
 * StatusNet, the distributed open-source microblogging tool
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *
 * Low-level generator for HTML
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Output
23
 * @package   StatusNet
24 25
 * @author    Evan Prodromou <evan@status.net>
 * @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')) { exit(1); }
32

33 34
// Can include XHTML options but these are too fragile in practice.
define('PAGE_TYPE_PREFS', 'text/html');
35 36 37 38 39 40 41 42 43 44

/**
 * Low-level generator for HTML
 *
 * Abstracts some of the code necessary for HTML generation. Especially
 * has methods for generating HTML form elements. Note that these have
 * been created kind of haphazardly, not with an eye to making a general
 * HTML-creation class.
 *
 * @category Output
45
 * @package  StatusNet
46 47
 * @author   Evan Prodromou <evan@status.net>
 * @author   Sarven Capadisli <csarven@status.net>
48
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
49
 * @link     http://status.net/
50
 *
51
 * @see      Action
52
 * @see      XMLOutputter
53 54 55 56
 */

class HTMLOutputter extends XMLOutputter
{
57 58 59
    protected $DTD = array('doctype' => 'html',
                           'spec'    => '-//W3C//DTD XHTML 1.0 Strict//EN',
                           'uri'     => 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
60 61 62 63 64 65 66 67 68
    /**
     * Constructor
     *
     * Just wraps the XMLOutputter constructor.
     *
     * @param string  $output URI to output to, default = stdout
     * @param boolean $indent Whether to indent output, default true
     */

69
    function __construct($output='php://output', $indent=null)
70 71 72 73 74 75 76
    {
        parent::__construct($output, $indent);
    }

    /**
     * Start an HTML document
     *
77
     * If $type isn't specified, will attempt to do content negotiation.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
     *
     * Attempts to do content negotiation for language, also.
     *
     * @param string $type MIME type to use; default is to do negotation.
     *
     * @todo extract content negotiation code to an HTTP module or class.
     *
     * @return void
     */

    function startHTML($type=null)
    {
        if (!$type) {
            $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ?
              $_SERVER['HTTP_ACCEPT'] : null;

            // XXX: allow content negotiation for RDF, RSS, or XRDS

            $cp = common_accept_to_prefs($httpaccept);
            $sp = common_accept_to_prefs(PAGE_TYPE_PREFS);

            $type = common_negotiate_type($cp, $sp);

            if (!$type) {
102
                // TRANS: Client exception 406
103 104
                throw new ClientException(_('This page is not available in a '.
                                            'media type you accept'), 406);
105 106 107
            }
        }

108
        header('Content-Type: '.$type);
109

110 111 112
	// Output anti-framing headers to prevent clickjacking (respected by newer
        // browsers).
	if (common_config('javascript', 'bustframes')) {
113
            header('X-XSS-Protection: 1; mode=block'); // detect XSS Reflection attacks
114 115 116
            header('X-Frame-Options: SAMEORIGIN'); // no rendering if origin mismatch
        }

117
        $this->extraHeaders();
118 119
        if (preg_match("/.*\/.*xml/", $type)) {
            // Required for XML documents
120
            $this->startXML();
121
        }
122 123

        $this->writeDTD();
124

125
        $language = $this->getLanguage();
126

127 128 129 130 131 132 133 134 135 136
        $attrs = array(
            'xmlns' => 'http://www.w3.org/1999/xhtml',
            'xml:lang' => $language,
            'lang' => $language
        );

        if (Event::handle('StartHtmlElement', array($this, &$attrs))) {
            $this->elementStart('html', $attrs);
            Event::handle('EndHtmlElement', array($this, &$attrs));
        }
137 138
    }

139 140 141 142 143 144 145 146 147 148 149 150
    public function setDTD($doctype, $spec, $uri)
    {
        $this->DTD = array('doctype' => $doctype, 'spec' => $spec, 'uri' => $uri);
    }

    protected function writeDTD()
    {
        $this->xw->writeDTD($this->DTD['doctype'],
                            $this->DTD['spec'],
                            $this->DTD['uri']);
    }

151 152 153 154 155 156
    function getLanguage()
    {
        // FIXME: correct language for interface
        return common_language();
    }

157 158 159 160 161 162 163 164 165 166
    /**
    *  Ends an HTML document
    *
    *  @return void
    */
    function endHTML()
    {
        $this->elementEnd('html');
        $this->endXML();
    }
167

168 169 170 171 172 173 174 175 176
    /**
    *  To specify additional HTTP headers for the action
    *
    *  @return void
    */
    function extraHeaders()
    {
        // Needs to be overloaded
    }
177

178 179 180 181 182 183 184 185
    /**
     * Output an HTML text input element
     *
     * Despite the name, it is specifically for outputting a
     * text input element, not other <input> elements. It outputs
     * a cluster of elements, including a <label> and an associated
     * instructions span.
     *
186 187
     * If $attrs['type'] does not exist it will be set to 'text'.
     *
188 189 190 191
     * @param string $id           element ID, must be unique on page
     * @param string $label        text of label for the element
     * @param string $value        value of the element, default null
     * @param string $instructions instructions for valid input
192 193
     * @param string $name         name of the element; if null, the id will
     *                             be used
194
     * @param bool   $required     HTML5 required attribute (exclude when false)
195
     * @param array  $attrs        Initial attributes manually set in an array (overwritten by previous options)
196 197 198 199 200 201 202
     *
     * @todo add a $maxLength parameter
     * @todo add a $size parameter
     *
     * @return void
     */

203
    function input($id, $label, $value=null, $instructions=null, $name=null, $required=false, array $attrs=array())
204 205
    {
        $this->element('label', array('for' => $id), $label);
206 207 208 209
        if (!array_key_exists('type', $attrs)) {
            $attrs['type'] = 'text';
        }
        $attrs['id'] = $id;
210
        $attrs['name'] = is_null($name) ? $id : $name;
211 212 213 214 215 216 217 218
        if (array_key_exists('placeholder', $attrs) && (is_null($attrs['placeholder']) || $attrs['placeholder'] === '')) {
            // If placeholder is type-aware equal to '' or null, unset it as we apparently don't want a placeholder value
            unset($attrs['placeholder']);
        } else {
            // If the placeholder is set use it, or use the label as fallback.
            $attrs['placeholder'] = isset($attrs['placeholder']) ? $attrs['placeholder'] : $label;
        }

219
        if (!is_null($value)) { // value can be 0 or ''
220
            $attrs['value'] = $value;
221
        }
222 223 224
        if (!empty($required)) {
            $attrs['required'] = 'required';
        }
225 226
        $this->element('input', $attrs);
        if ($instructions) {
227
            $this->element('p', 'form_guide', $instructions);
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
        }
    }

    /**
     * output an HTML checkbox and associated elements
     *
     * Note that the value is default 'true' (the string), which can
     * be used by Action::boolean()
     *
     * @param string $id           element ID, must be unique on page
     * @param string $label        text of label for the element
     * @param string $checked      if the box is checked, default false
     * @param string $instructions instructions for valid input
     * @param string $value        value of the checkbox, default 'true'
     * @param string $disabled     show the checkbox disabled, default false
     *
     * @return void
     *
     * @todo add a $name parameter
     */

    function checkbox($id, $label, $checked=false, $instructions=null,
                      $value='true', $disabled=false)
    {
        $attrs = array('name' => $id,
                       'type' => 'checkbox',
                       'class' => 'checkbox',
                       'id' => $id);
        if ($value) {
257
            $attrs['value'] = $value;
258 259 260 261 262 263 264 265 266
        }
        if ($checked) {
            $attrs['checked'] = 'checked';
        }
        if ($disabled) {
            $attrs['disabled'] = 'true';
        }
        $this->element('input', $attrs);
        $this->text(' ');
sarven's avatar
sarven committed
267
        $this->element('label', array('class' => 'checkbox',
268 269 270 271
                                      'for' => $id),
                       $label);
        $this->text(' ');
        if ($instructions) {
272
            $this->element('p', 'form_guide', $instructions);
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
        }
    }

    /**
     * output an HTML combobox/select and associated elements
     *
     * $content is an array of key-value pairs for the dropdown, where
     * the key is the option value attribute and the value is the option
     * text. (Careful on the overuse of 'value' here.)
     *
     * @param string $id           element ID, must be unique on page
     * @param string $label        text of label for the element
     * @param array  $content      options array, value => text
     * @param string $instructions instructions for valid input
     * @param string $blank_select whether to have a blank entry, default false
     * @param string $selected     selected value, default null
     *
     * @return void
     *
     * @todo add a $name parameter
     */

    function dropdown($id, $label, $content, $instructions=null,
                      $blank_select=false, $selected=null)
    {
        $this->element('label', array('for' => $id), $label);
        $this->elementStart('select', array('id' => $id, 'name' => $id));
        if ($blank_select) {
            $this->element('option', array('value' => ''));
        }
        foreach ($content as $value => $option) {
            if ($value == $selected) {
                $this->element('option', array('value' => $value,
306
                                               'selected' => 'selected'),
307 308 309 310 311 312 313
                               $option);
            } else {
                $this->element('option', array('value' => $value), $option);
            }
        }
        $this->elementEnd('select');
        if ($instructions) {
314
            $this->element('p', 'form_guide', $instructions);
315 316 317 318 319 320 321 322 323 324
        }
    }

    /**
     * output an HTML hidden element
     *
     * $id is re-used as name
     *
     * @param string $id    element ID, must be unique on page
     * @param string $value hidden element value, default null
325
     * @param string $name  name, if different than ID
326 327 328 329
     *
     * @return void
     */

330
    function hidden($id, $value, $name=null)
331
    {
332
        $this->element('input', array('name' => $name ?: $id,
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
                                      'type' => 'hidden',
                                      'id' => $id,
                                      'value' => $value));
    }

    /**
     * output an HTML password input and associated elements
     *
     * @param string $id           element ID, must be unique on page
     * @param string $label        text of label for the element
     * @param string $instructions instructions for valid input
     *
     * @return void
     *
     * @todo add a $name parameter
     */

    function password($id, $label, $instructions=null)
    {
        $this->element('label', array('for' => $id), $label);
        $attrs = array('name' => $id,
                       'type' => 'password',
                       'class' => 'password',
                       'id' => $id);
        $this->element('input', $attrs);
        if ($instructions) {
359
            $this->element('p', 'form_guide', $instructions);
360 361 362 363 364 365 366 367 368
        }
    }

    /**
     * output an HTML submit input and associated elements
     *
     * @param string $id    element ID, must be unique on page
     * @param string $label text of the button
     * @param string $cls   class of the button, default 'submit'
369
     * @param string $name  name, if different than ID
370
     * @param string $title  title text for the submit button
371 372 373 374 375 376
     *
     * @return void
     *
     * @todo add a $name parameter
     */

377
    function submit($id, $label, $cls='submit', $name=null, $title=null)
378 379 380
    {
        $this->element('input', array('type' => 'submit',
                                      'id' => $id,
381
                                      'name'  => $name ?: $id,
382
                                      'class' => $cls,
383
                                      'value' => $label,
384
                                      'title' => $title));
385 386
    }

387 388 389 390 391 392 393 394 395 396
    /**
     * output a script (almost always javascript) tag
     *
     * @param string $src          relative or absolute script path
     * @param string $type         'type' attribute value of the tag
     *
     * @return void
     */
    function script($src, $type='text/javascript')
    {
397
        if (Event::handle('StartScriptElement', array($this,&$src,&$type))) {
398

399
            $url = parse_url($src);
400

401 402 403 404
            if (empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) {

                // XXX: this seems like a big assumption

405
                if (strpos($src, 'plugins/') === 0 || strpos($src, 'local/') === 0) {
406

mmn's avatar
mmn committed
407
                    $src = common_path($src, GNUsocial::isHTTPS()) . '?version=' . GNUSOCIAL_VERSION;
408

409
                } else {
410

mmn's avatar
mmn committed
411
                    if (GNUsocial::isHTTPS()) {
412

413
                        $sslserver = common_config('javascript', 'sslserver');
414

415
                        if (empty($sslserver)) {
416 417 418 419 420 421
                            if (is_string(common_config('site', 'sslserver')) &&
                                mb_strlen(common_config('site', 'sslserver')) > 0) {
                                $server = common_config('site', 'sslserver');
                            } else if (common_config('site', 'server')) {
                                $server = common_config('site', 'server');
                            }
422 423 424 425 426 427 428 429
                            $path   = common_config('site', 'path') . '/js/';
                        } else {
                            $server = $sslserver;
                            $path   = common_config('javascript', 'sslpath');
                            if (empty($path)) {
                                $path = common_config('javascript', 'path');
                            }
                        }
430

431
                        $protocol = 'https';
432

433
                    } else {
434

435
                        $path = common_config('javascript', 'path');
436

437
                        if (empty($path)) {
438
                            $path = common_config('site', 'path') . '/js/';
439
                        }
440

441 442 443 444
                        $server = common_config('javascript', 'server');

                        if (empty($server)) {
                            $server = common_config('site', 'server');
445
                        }
446 447

                        $protocol = 'http';
448 449
                    }

450 451 452 453 454 455 456
                    if ($path[strlen($path)-1] != '/') {
                        $path .= '/';
                    }

                    if ($path[0] != '/') {
                        $path = '/'.$path;
                    }
457

458
                    $src = $protocol.'://'.$server.$path.$src . '?version=' . GNUSOCIAL_VERSION;
459
                }
460
            }
461

462
            $this->element('script', array('type' => $type,
463 464
                                           'src' => $src),
                           ' ');
465

466
            Event::handle('EndScriptElement', array($this,$src,$type));
467
        }
468 469
    }

470 471 472 473
    /**
     * output a script (almost always javascript) tag with inline
     * code.
     *
474
     * @param string $code         code to put in the script tag
475 476 477 478 479 480 481
     * @param string $type         'type' attribute value of the tag
     *
     * @return void
     */

    function inlineScript($code, $type='text/javascript')
    {
482 483 484 485 486 487 488 489 490 491 492
        if(Event::handle('StartInlineScriptElement', array($this,&$code,&$type))) {
            $this->elementStart('script', array('type' => $type));
            if($type == 'text/javascript') {
                $this->raw('/*<![CDATA[*/ '); // XHTML compat
            }
            $this->raw($code);
            if($type == 'text/javascript') {
                $this->raw(' /*]]>*/'); // XHTML compat
            }
            $this->elementEnd('script');
            Event::handle('EndInlineScriptElement', array($this,$code,$type));
493
        }
494 495
    }

496 497 498
    /**
     * output a css link
     *
499
     * @param string $src     relative path within the theme directory, or an absolute path
500 501 502 503 504
     * @param string $theme        'theme' that contains the stylesheet
     * @param string media         'media' attribute of the tag
     *
     * @return void
     */
505
    function cssLink($src,$theme=null,$media=null)
506
    {
507 508
        if(Event::handle('StartCssLinkElement', array($this,&$src,&$theme,&$media))) {
            $url = parse_url($src);
509
            if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment']))
510 511
            {
                if(file_exists(Theme::file($src,$theme))){
512
                   $src = Theme::path($src, $theme);
513
                }else{
mmn's avatar
mmn committed
514
                    $src = common_path($src, GNUsocial::isHTTPS());
515
                }
516
                $src.= '?version=' . GNUSOCIAL_VERSION;
517
            }
518 519 520 521 522
            $this->element('link', array('rel' => 'stylesheet',
                                    'type' => 'text/css',
                                    'href' => $src,
                                    'media' => $media));
            Event::handle('EndCssLinkElement', array($this,$src,$theme,$media));
523
        }
524 525
    }

526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
    /**
     * output a style (almost always css) tag with inline
     * code.
     *
     * @param string $code         code to put in the style tag
     * @param string $type         'type' attribute value of the tag
     * @param string $media        'media' attribute value of the tag
     *
     * @return void
     */

    function style($code, $type = 'text/css', $media = null)
    {
        if(Event::handle('StartStyleElement', array($this,&$code,&$type,&$media))) {
            $this->elementStart('style', array('type' => $type, 'media' => $media));
            $this->raw($code);
            $this->elementEnd('style');
            Event::handle('EndStyleElement', array($this,$code,$type,$media));
        }
    }

547 548 549 550 551 552 553
    /**
     * output an HTML textarea and associated elements
     *
     * @param string $id           element ID, must be unique on page
     * @param string $label        text of label for the element
     * @param string $content      content of the textarea, default none
     * @param string $instructions instructions for valid input
554 555 556
     * @param string $name         name of textarea; if null, $id will be used
     * @param int    $cols         number of columns
     * @param int    $rows         number of rows
557
     * @param bool   $required     HTML5 required attribute (exclude when false)
558 559 560 561
     *
     * @return void
     */

562 563 564 565 566 567 568
    function textarea(
        $id,
        $label,
        $content      = null,
        $instructions = null,
        $name         = null,
        $cols         = null,
569 570
        $rows         = null,
        $required     = false
571
    ) {
572
        $this->element('label', array('for' => $id), $label);
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
        $attrs = array(
            'rows' => 3,
            'cols' => 40,
            'id' => $id
        );
        $attrs['name'] = is_null($name) ? $id : $name;

        if ($cols != null) {
            $attrs['cols'] = $cols;

        }
        if ($rows != null) {
            $attrs['rows'] = $rows;
        }
        $this->element(
            'textarea',
            $attrs,
            is_null($content) ? '' : $content
        );
592
        if ($instructions) {
593
            $this->element('p', 'form_guide', $instructions);
594 595
        }
    }
596

597
   /**
598 599 600 601 602 603 604 605 606
    * Internal script to autofocus the given element on page onload.
    *
    * @param string $id element ID, must refer to an existing element
    *
    * @return void
    *
    */
    function autofocus($id)
    {
607
        $this->inlineScript(
608 609 610
                   ' $(document).ready(function() {'.
                   ' var el = $("#' . $id . '");'.
                   ' if (el.length) { el.focus(); }'.
611
                   ' });');
612
    }
613
}