Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4139458
Tag.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
Tag.php
View Options
<?php
namespace
OOUI
;
class
Tag
{
/* Properties */
/**
* Tag name for this instance.
*
* @var string HTML tag name
*/
protected
$tag
=
''
;
/**
* Attributes.
*
* @var array HTML attributes
*/
protected
$attributes
=
[];
/**
* Classes.
*
* @var array CSS classes
*/
protected
$classes
=
[];
/**
* Content.
*
* @var array Content text and elements
*/
protected
$content
=
[];
/**
* Group.
*
* @var GroupElement|null Group element is in
*/
protected
$elementGroup
=
null
;
/**
* Infusion support.
*
* @var bool 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 $class CSS class name
* @return bool
*/
public
function
hasClass
(
$class
)
{
return
in_array
(
$class
,
$this
->
classes
);
}
/**
* Add CSS classes.
*
* @param array $classes List of classes to add
* @return $this
*/
public
function
addClasses
(
array
$classes
)
{
$this
->
classes
=
array_unique
(
array_merge
(
$this
->
classes
,
$classes
)
);
return
$this
;
}
/**
* Remove CSS classes.
*
* @param array $classes List of classes to remove
* @return $this
*/
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 bool|null $toggle Add classes
* @return $this
*/
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
;
}
/**
* @return string
*/
public
function
getTag
()
{
return
$this
->
tag
;
}
/**
* Get HTML attribute value.
*
* @param string $key HTML attribute name
* @return string|null
*/
public
function
getAttribute
(
$key
)
{
return
$this
->
attributes
[
$key
]
??
null
;
}
/**
* Add HTML attributes.
*
* @param array $attributes List of attribute key/value pairs to add
* @return $this
*/
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 mixed $value New value should usually be a string
* @return $this
*/
public
function
setValue
(
$value
)
{
if
(
strtolower
(
$this
->
tag
)
===
'textarea'
)
{
$this
->
clearContent
();
$this
->
appendContent
(
$value
);
}
else
{
$this
->
setAttributes
(
[
'value'
=>
$value
]
);
}
return
$this
;
}
/**
* Remove HTML attributes.
*
* @param array $keys List of attribute keys to remove
* @return $this
*/
public
function
removeAttributes
(
array
$keys
)
{
foreach
(
$keys
as
$key
)
{
unset
(
$this
->
attributes
[
$key
]
);
}
return
$this
;
}
/**
* Remove any items that match by reference
*
* String items should never match by reference
* so will not be removed.
*
* @param string|Tag|HtmlSnippet|array ...$content Content to remove.
* @return $this
*/
public
function
removeContent
(
...
$content
)
{
if
(
isset
(
$content
[
0
]
)
&&
is_array
(
$content
[
0
]
)
)
{
return
$this
->
removeContent
(
...
$content
[
0
]
);
}
foreach
(
$content
as
$i
=>
$item
)
{
if
(
!
is_int
(
$i
)
)
{
throw
new
\AssertionError
(
'$content cannot have string keys'
);
}
if
(
!
is_string
(
$item
)
)
{
// Use strict type comparions so we don't
// compare objects with existing strings
$index
=
array_search
(
$item
,
$this
->
content
,
true
);
if
(
$index
!==
false
)
{
array_splice
(
$this
->
content
,
$index
,
1
);
}
}
}
return
$this
;
}
/**
* Add content to the end. Strings will be HTML-escaped, use HtmlSnippet to prevent that.
*
* Accepts either variadic arguments (the $content argument can be repeated any number of times)
* or an array of arguments.
*
* For example, these uses are valid:
* * $tag->appendContent( [ $element1, $element2 ] );
* * $tag->appendContent( $element1, $element2 );
* This, however, is not acceptable
* * $tag->appendContent( [ $element1, $element2 ], $element3 );
*
* Objects that are already in $this->content will be moved
* to the end of the list, not duplicated.
*
* @param string|Tag|HtmlSnippet|array ...$content Content to append
* @return $this
*/
public
function
appendContent
(
...
$content
)
{
if
(
isset
(
$content
[
0
]
)
&&
is_array
(
$content
[
0
]
)
)
{
return
$this
->
appendContent
(
...
$content
[
0
]
);
}
$this
->
removeContent
(
...
$content
);
$this
->
content
=
array_merge
(
$this
->
content
,
$content
);
return
$this
;
}
/**
* Add content to the beginning.
*
* Accepts either variadic arguments (the $content argument can be repeated any number of times)
* or an array of arguments.
*
* Objects that are already in $this->content will be moved
* to the end of the list, not duplicated.
*
* For example, these uses are valid:
* * $tag->prependContent( [ $element1, $element2 ] );
* * $tag->prependContent( $element1, $element2 );
* This, however, is not acceptable
* * $tag->prependContent( [ $element1, $element2 ], $element3 );
*
* @param string|Tag|HtmlSnippet|array ...$content Content to prepend. Strings will be HTML-escaped
* for output, use a HtmlSnippet instance to prevent that.
* @return $this
*/
public
function
prependContent
(
...
$content
)
{
if
(
isset
(
$content
[
0
]
)
&&
is_array
(
$content
[
0
]
)
)
{
return
$this
->
prependContent
(
...
$content
[
0
]
);
}
$this
->
removeContent
(
...
$content
);
array_splice
(
$this
->
content
,
0
,
0
,
$content
);
return
$this
;
}
/**
* Remove all content.
*
* @return $this
*/
public
function
clearContent
()
{
$this
->
content
=
[];
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
* @return $this
*/
public
function
setElementGroup
(
$group
)
{
$this
->
elementGroup
=
$group
;
return
$this
;
}
/**
* Enable widget for client-side infusion.
*
* @param bool $infusable True to allow tag/element/widget to be referenced client-side.
* @return $this
*/
public
function
setInfusable
(
$infusable
)
{
$this
->
infusable
=
$infusable
;
return
$this
;
}
/**
* Get client-side infusability.
*
* @return bool If this tag/element/widget can be referenced client-side.
*/
public
function
isInfusable
()
{
return
$this
->
infusable
;
}
/** @var int */
private
static
$elementId
=
0
;
/**
* Generate a unique ID for element
*
* @return string ID
*/
public
static
function
generateElementId
()
{
self
::
$elementId
++;
return
'ooui-php-'
.
self
::
$elementId
;
}
/**
* Reset the unique ID, for consistent test output.
* Warning: Do not use this function outside of tests!
*
* @see https://phabricator.wikimedia.org/T345515
*/
public
static
function
resetElementId
()
{
self
::
$elementId
=
0
;
}
/**
* Ensure that this given Tag is infusable and has a unique `id`
* attribute.
* @return $this
*/
public
function
ensureInfusableId
()
{
$this
->
setInfusable
(
true
);
if
(
$this
->
getAttribute
(
'id'
)
===
null
)
{
$this
->
setAttributes
(
[
'id'
=>
self
::
generateElementId
()
]
);
}
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 string[]
*/
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
(
[
'_'
=>
'Tag'
]
);
}
return
$attributesArray
;
}
/**
* Check whether the string $haystack begins with the string $needle.
*
* @param string $haystack
* @param string $needle
* @return bool True if $haystack begins with $needle, false otherwise.
*/
private
static
function
stringStartsWith
(
$haystack
,
$needle
)
{
return
strncmp
(
$haystack
,
$needle
,
strlen
(
$needle
)
)
===
0
;
}
/**
* Check whether user-supplied URL is safe, that is, whether outputting it will not result in XSS
* vulnerability. (Note that URLs must be HTML-escaped regardless of this check.)
*
* The point is to disallow 'javascript:' URLs (there are no good reasons to ever use them
* anyway), but there's no good way to prohibit them because of very lax parsing in browsers.
*
* An URL is safe if:
*
* - it is empty, or
* - it starts with an allowed protocol, followed by a colon (absolute URL), or
* - it starts with two slashes `//` (protocol-relative URL), or
* - it starts with a single slash `/`, or dot and slash `./` (relative URL), or
* - it starts with a question mark `?` (replace query part in current URL), or
* - it starts with a pound sign `#` (replace fragment part in current URL)
*
* Plain relative URLs (like `index.php`) are not allowed, since it's pretty much impossible to
* distinguish them from malformed absolute ones (again, very lax rules for parsing protocols).
*
* @param string $url URL
* @return bool [description]
*/
public
static
function
isSafeUrl
(
$url
)
{
// Keep this function in sync with OO.ui.isSafeUrl
$protocolAllowList
=
[
// 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'
,
];
if
(
$url
===
''
)
{
return
true
;
}
foreach
(
$protocolAllowList
as
$protocol
)
{
if
(
self
::
stringStartsWith
(
$url
,
$protocol
.
':'
)
)
{
return
true
;
}
}
// This matches '//' too
if
(
self
::
stringStartsWith
(
$url
,
'/'
)
||
self
::
stringStartsWith
(
$url
,
'./'
)
)
{
return
true
;
}
if
(
self
::
stringStartsWith
(
$url
,
'?'
)
||
self
::
stringStartsWith
(
$url
,
'#'
)
)
{
return
true
;
}
return
false
;
}
/**
* Render element into HTML.
* @return string HTML serialization
*/
public
function
toString
()
{
// List of void elements from HTML5, section 8.1.2 as of 2016-09-19
static
$voidElements
=
[
'area'
,
'base'
,
'br'
,
'col'
,
'embed'
,
'hr'
,
'img'
,
'input'
,
'keygen'
,
'link'
,
'meta'
,
'param'
,
'source'
,
'track'
,
'wbr'
,
];
$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'
);
}
// Note that this is not a complete list of HTML attributes that need this validation.
// We only check for the ones that are generated by built-in OOUI PHP elements.
if
(
$key
===
'href'
||
$key
===
'action'
)
{
if
(
!
self
::
isSafeUrl
(
$value
)
)
{
// We can't tell for sure whether this URL is safe (although it might be). The only safe
// URLs that we can't check for is relative ones, so just prefix with './'. This should
// change nothing for relative URLs, and it will neutralize sneaky 'javascript:' URLs.
$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
(
'"'
,
'"'
,
$value
);
// 3. Wrap attribute value in single quotes in the HTML.
$attributes
.=
' '
.
$key
.
"='"
.
$value
.
"'"
;
}
// Content
$content
=
''
;
foreach
(
$this
->
content
as
$part
)
{
if
(
$part
instanceof
Tag
||
$part
instanceof
HtmlSnippet
)
{
$content
.=
(
string
)
$part
;
}
else
{
$content
.=
htmlspecialchars
(
(
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
if
(
!
$content
&&
in_array
(
$this
->
tag
,
$voidElements
)
)
{
return
'<'
.
$this
->
tag
.
$attributes
.
' />'
;
}
else
{
return
'<'
.
$this
->
tag
.
$attributes
.
'>'
.
$content
.
'</'
.
$this
->
tag
.
'>'
;
}
}
/**
* Magic method implementation.
*
* It was not possible to throw an exception from within a __toString() method prior to PHP 7.4.0.
* Doing so will result in a fatal error.
* 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
);
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 15:05 (3 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
db/27/255959a56dbaf83ad5fa5611132d
Default Alt Text
Tag.php (13 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment