summaryrefslogtreecommitdiff
path: root/vendor/oojs/oojs-ui/php/Tag.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/oojs/oojs-ui/php/Tag.php')
-rw-r--r--vendor/oojs/oojs-ui/php/Tag.php365
1 files changed, 365 insertions, 0 deletions
diff --git a/vendor/oojs/oojs-ui/php/Tag.php b/vendor/oojs/oojs-ui/php/Tag.php
new file mode 100644
index 00000000..da8c2bfa
--- /dev/null
+++ b/vendor/oojs/oojs-ui/php/Tag.php
@@ -0,0 +1,365 @@
+<?php
+
+namespace OOUI;
+
+class Tag {
+
+ /* Members */
+
+ /**
+ * Tag name for this instance.
+ *
+ * @var string HTML tag name
+ */
+ protected $tag = '';
+
+ /**
+ * Attributes.
+ *
+ * @var array HTML attributes
+ */
+ protected $attributes = array();
+
+ /**
+ * Classes.
+ *
+ * @var array CSS classes
+ */
+ protected $classes = array();
+
+ /**
+ * Content.
+ *
+ * @var array Content text and elements
+ */
+ protected $content = array();
+
+ /**
+ * Group.
+ *
+ * @var GroupElement|null Group element is in
+ */
+ protected $elementGroup = null;
+
+ /**
+ * Infusion support.
+ *
+ * @var boolean Whether to serialize tag/element/widget state for client-side use.
+ */
+ protected $infusable = false;
+
+ /* Methods */
+
+ /**
+ * Create element.
+ *
+ * @param string $tag HTML tag name
+ */
+ public function __construct( $tag = 'div' ) {
+ $this->tag = $tag;
+ }
+
+ /**
+ * Check for CSS class.
+ *
+ * @param string $name CSS class name
+ * @return boolean
+ */
+ public function hasClass( $class ) {
+ return in_array( $class, $this->classes );
+ }
+
+ /**
+ * Add CSS classes.
+ *
+ * @param array $classes List of classes to add
+ * @chainable
+ */
+ public function addClasses( array $classes ) {
+ $this->classes = array_merge( $this->classes, $classes );
+ return $this;
+ }
+
+ /**
+ * Remove CSS classes.
+ *
+ * @param array $classes List of classes to remove
+ * @chainable
+ */
+ public function removeClasses( array $classes ) {
+ $this->classes = array_diff( $this->classes, $classes );
+ return $this;
+ }
+
+ /**
+ * Toggle CSS classes.
+ *
+ * @param array $classes List of classes to add
+ * @param boolean $toggle Add classes
+ * @chainable
+ */
+ public function toggleClasses( array $classes, $toggle = null ) {
+ if ( $toggle === null ) {
+ $this->classes = array_diff(
+ array_merge( $this->classes, $classes ),
+ array_intersect( $this->classes, $classes )
+ );
+ } elseif ( $toggle ) {
+ $this->classes = array_merge( $this->classes, $classes );
+ } else {
+ $this->classes = array_diff( $this->classes, $classes );
+ }
+ return $this;
+ }
+
+ /**
+ * Get HTML attribute value.
+ *
+ * @param string $key HTML attribute name
+ * @return string|null
+ */
+ public function getAttribute( $key ) {
+ return isset( $this->attributes[$key] ) ? $this->attributes[$key] : null;
+ }
+
+ /**
+ * Add HTML attributes.
+ *
+ * @param array $attributes List of attribute key/value pairs to add
+ * @chainable
+ */
+ public function setAttributes( array $attributes ) {
+ foreach ( $attributes as $key => $value ) {
+ $this->attributes[$key] = $value;
+ }
+ return $this;
+ }
+
+ /**
+ * Set value of input element ('value' attribute for most, element content for textarea).
+ *
+ * @param string $value Value to set
+ * @chainable
+ */
+ public function setValue( $value ) {
+ if ( strtolower( $this->tag ) === 'textarea' ) {
+ $this->clearContent();
+ $this->appendContent( $value );
+ } else {
+ $this->setAttributes( array( 'value' => $value ) );
+ }
+ return $this;
+ }
+
+ /**
+ * Remove HTML attributes.
+ *
+ * @param array $keys List of attribute keys to remove
+ * @chainable
+ */
+ public function removeAttributes( array $keys ) {
+ foreach ( $keys as $key ) {
+ unset( $this->attributes[$key] );
+ }
+ return $this;
+ }
+
+ /**
+ * Add content to the end.
+ *
+ * Accepts variadic arguments (the $content argument can be repeated any number of times).
+ *
+ * @param string|Tag|HtmlSnippet $content Content to append. Strings will be HTML-escaped
+ * for output, use a HtmlSnippet instance to prevent that.
+ * @chainable
+ */
+ public function appendContent( /* $content... */ ) {
+ $contents = func_get_args();
+ $this->content = array_merge( $this->content, $contents );
+ return $this;
+ }
+
+ /**
+ * Add content to the beginning.
+ *
+ * Accepts variadic arguments (the $content argument can be repeated any number of times).
+ *
+ * @param string|Tag|HtmlSnippet $content Content to prepend. Strings will be HTML-escaped
+ * for output, use a HtmlSnippet instance to prevent that.
+ * @chainable
+ */
+ public function prependContent( /* $content... */ ) {
+ $contents = func_get_args();
+ array_splice( $this->content, 0, 0, $contents );
+ return $this;
+ }
+
+ /**
+ * Remove all content.
+ *
+ * @chainable
+ */
+ public function clearContent() {
+ $this->content = array();
+ return $this;
+ }
+
+ /**
+ * Get group element is in.
+ *
+ * @return GroupElement|null Group element, null if none
+ */
+ public function getElementGroup() {
+ return $this->elementGroup;
+ }
+
+ /**
+ * Set group element is in.
+ *
+ * @param GroupElement|null $group Group element, null if none
+ * @chainable
+ */
+ public function setElementGroup( $group ) {
+ $this->elementGroup = $group;
+ return $this;
+ }
+
+ /**
+ * Enable widget for client-side infusion.
+ *
+ * @param boolean $infusable True to allow tag/element/widget to be referenced client-side.
+ * @chainable
+ */
+ public function setInfusable( $infusable ) {
+ $this->infusable = $infusable;
+ return $this;
+ }
+
+ /**
+ * Get client-side infusability.
+ *
+ * @return boolean If this tag/element/widget can be referenced client-side.
+ */
+ public function isInfusable() {
+ return $this->infusable;
+ }
+
+ private static $id_cnt = 0;
+ /**
+ * Ensure that this given Tag is infusable and has a unique `id`
+ * attribute.
+ * @chainable
+ */
+ public function ensureInfusableId() {
+ $this->setInfusable( true );
+ if ( $this->getAttribute( 'id' ) === null ) {
+ $this->setAttributes( array( 'id' => "ooui-" . ( self::$id_cnt++ ) ) );
+ }
+ return $this;
+ }
+
+ /**
+ * Return an augmented `attributes` array, including synthetic attributes
+ * which are created from other properties (like the `classes` array)
+ * but which shouldn't be retained in the user-visible `attributes`.
+ * @return array An attributes array.
+ */
+ protected function getGeneratedAttributes() {
+ // Copy attributes, add `class` attribute from `$this->classes` array.
+ $attributesArray = $this->attributes;
+ if ( $this->classes ) {
+ $attributesArray['class'] = implode( ' ', array_unique( $this->classes ) );
+ }
+ if ( $this->infusable ) {
+ // Indicate that this is "just" a tag (not a widget)
+ $attributesArray['data-ooui'] = json_encode( array( '_' => 'Tag' ) );
+ }
+ return $attributesArray;
+ }
+
+ /**
+ * Render element into HTML.
+ *
+ * @return string HTML serialization
+ */
+ public function toString() {
+ $attributes = '';
+ foreach ( $this->getGeneratedAttributes() as $key => $value ) {
+ if ( !preg_match( '/^[0-9a-zA-Z-]+$/', $key ) ) {
+ throw new Exception( 'Attribute name must consist of only ASCII letters, numbers and dash' );
+ }
+
+ if ( $key === 'href' || $key === 'action' ) {
+ // Make it impossible to point a link or a form to a 'javascript:' URL. There's no good way
+ // to blacklist them because of very lax parsing, so instead we whitelist known-good
+ // protocols (and also accept protocol-less and protocol-relative links). There are no good
+ // reasons to ever use 'javascript:' URLs anyway.
+ $protocolWhitelist = array(
+ // Sourced from MediaWiki's $wgUrlProtocols
+ 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
+ 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
+ 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp',
+ );
+
+ // Protocol-relative URLs are handled really badly by parse_url()
+ if ( substr( $value, 0, 2 ) === '//' ) {
+ $url = "http:$value";
+ } else {
+ $url = $value;
+ }
+ // Must suppress warnings when the value is not a valid URL. parse_url() returns false then.
+ // @codingStandardsIgnoreStart
+ $scheme = @parse_url( $url, PHP_URL_SCHEME );
+ // @codingStandardsIgnoreEnd
+
+ if ( !( $scheme === null || in_array( strtolower( $scheme ), $protocolWhitelist ) ) ) {
+ throw new Exception( "Potentially unsafe '$key' attribute value. " .
+ "Scheme: '$scheme'; value: '$value'." );
+ }
+ }
+
+ // Use single-quotes around the attribute value in HTML, because
+ // some of the values might be JSON strings
+ // 1. Encode both single and double quotes (and other special chars)
+ $value = htmlspecialchars( $value, ENT_QUOTES );
+ // 2. Decode double quotes, for readability.
+ $value = str_replace( '&quot;', '"', $value );
+ // 3. Wrap attribute value in single quotes in the HTML.
+ $attributes .= ' ' . $key . "='" . $value . "'";
+ }
+
+ // Content
+ $content = '';
+ foreach ( $this->content as $part ) {
+ if ( is_string( $part ) ) {
+ $content .= htmlspecialchars( $part );
+ } elseif ( $part instanceof Tag || $part instanceof HtmlSnippet ) {
+ $content .= (string)$part;
+ }
+ }
+
+ if ( !preg_match( '/^[0-9a-zA-Z]+$/', $this->tag ) ) {
+ throw new Exception( 'Tag name must consist of only ASCII letters and numbers' );
+ }
+
+ // Tag
+ return '<' . $this->tag . $attributes . '>' . $content . '</' . $this->tag . '>';
+ }
+
+ /**
+ * Magic method implementation.
+ *
+ * PHP doesn't allow __toString to throw exceptions and will trigger a fatal error if it does.
+ * This is a wrapper around the real toString() to convert them to errors instead.
+ *
+ * @return string
+ */
+ public function __toString() {
+ try {
+ return $this->toString();
+ } catch ( Exception $ex ) {
+ trigger_error( (string)$ex, E_USER_ERROR );
+ return '';
+ }
+ }
+}