Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1432208
HTMLFormFieldCloner.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
HTMLFormFieldCloner.php
View Options
<?php
namespace
MediaWiki\HTMLForm\Field
;
use
InvalidArgumentException
;
use
MediaWiki\Html\Html
;
use
MediaWiki\HTMLForm\HTMLForm
;
use
MediaWiki\HTMLForm\HTMLFormField
;
use
MediaWiki\Parser\Sanitizer
;
use
MediaWiki\Request\DerivativeRequest
;
use
MediaWiki\Xml\Xml
;
/**
* A container for HTMLFormFields that allows for multiple copies of the set of
* fields to be displayed to and entered by the user.
*
* Recognized parameters, besides the general ones, include:
* fields - HTMLFormField descriptors for the subfields this cloner manages.
* The format is just like for the HTMLForm. A field with key 'delete' is
* special: it must have type = submit and will serve to delete the group
* of fields.
* required - If specified, at least one group of fields must be submitted.
* format - HTMLForm display format to use when displaying the subfields:
* 'table', 'div', or 'raw'. This is ignored when using OOUI.
* row-legend - If non-empty, each group of subfields will be enclosed in a
* fieldset. The value is the name of a message key to use as the legend.
* create-button-message - Message to use as the text of the button to
* add an additional group of fields.
* delete-button-message - Message to use as the text of automatically-
* generated 'delete' button. Ignored if 'delete' is included in 'fields'.
*
* In the generated HTML, the subfields will be named along the lines of
* "clonerName[index][fieldname]", with ids "clonerId--index--fieldid". 'index'
* may be a number or an arbitrary string, and may likely change when the page
* is resubmitted. Cloners may be nested, resulting in field names along the
* lines of "cloner1Name[index1][cloner2Name][index2][fieldname]" and
* corresponding ids.
*
* Use of cloner may result in submissions of the page that are not submissions
* of the HTMLForm, when non-JavaScript clients use the create or remove buttons.
*
* The result is an array, with values being arrays mapping subfield names to
* their values. On non-HTMLForm-submission page loads, there may also be
* additional (string) keys present with other types of values.
*
* @since 1.23
* @stable to extend
*/
class
HTMLFormFieldCloner
extends
HTMLFormField
{
/** @var int */
private
static
$counter
=
0
;
/**
* @var string String uniquely identifying this cloner instance and
* unlikely to exist otherwise in the generated HTML, while still being
* valid as part of an HTML id.
*/
protected
$uniqueId
;
/** @var array<string, HTMLFormField[]> */
protected
$mFields
=
[];
/**
* @stable to call
* @inheritDoc
*/
public
function
__construct
(
$params
)
{
$this
->
uniqueId
=
$this
->
getClassName
()
.
++
self
::
$counter
.
'x'
;
parent
::
__construct
(
$params
);
if
(
empty
(
$this
->
mParams
[
'fields'
]
)
||
!
is_array
(
$this
->
mParams
[
'fields'
]
)
)
{
throw
new
InvalidArgumentException
(
'HTMLFormFieldCloner called without any fields'
);
}
// Make sure the delete button, if explicitly specified, is sensible
if
(
isset
(
$this
->
mParams
[
'fields'
][
'delete'
]
)
)
{
$class
=
'mw-htmlform-cloner-delete-button'
;
$info
=
$this
->
mParams
[
'fields'
][
'delete'
]
+
[
'formnovalidate'
=>
true
,
'cssclass'
=>
$class
];
unset
(
$info
[
'name'
],
$info
[
'class'
]
);
if
(
!
isset
(
$info
[
'type'
]
)
||
$info
[
'type'
]
!==
'submit'
)
{
throw
new
InvalidArgumentException
(
'HTMLFormFieldCloner delete field, if specified, must be of type "submit"'
);
}
if
(
!
in_array
(
$class
,
explode
(
' '
,
$info
[
'cssclass'
]
)
)
)
{
$info
[
'cssclass'
]
.=
" $class"
;
}
$this
->
mParams
[
'fields'
][
'delete'
]
=
$info
;
}
}
/**
* @param string $key Array key under which these fields should be named
* @return HTMLFormField[]
*/
protected
function
getFieldsForKey
(
$key
)
{
if
(
!
isset
(
$this
->
mFields
[
$key
]
)
)
{
$this
->
mFields
[
$key
]
=
$this
->
createFieldsForKey
(
$key
);
}
return
$this
->
mFields
[
$key
];
}
/**
* Create the HTMLFormFields that go inside this element, using the
* specified key.
*
* @param string $key Array key under which these fields should be named
* @return HTMLFormField[]
*/
protected
function
createFieldsForKey
(
$key
)
{
$fields
=
[];
foreach
(
$this
->
mParams
[
'fields'
]
as
$fieldname
=>
$info
)
{
$name
=
"{$this->mName}[$key][$fieldname]"
;
if
(
isset
(
$info
[
'name'
]
)
)
{
$info
[
'name'
]
=
"{$this->mName}[$key][{$info['name']}]"
;
}
else
{
$info
[
'name'
]
=
$name
;
}
if
(
isset
(
$info
[
'id'
]
)
)
{
$info
[
'id'
]
=
Sanitizer
::
escapeIdForAttribute
(
"{$this->mID}--$key--{$info['id']}"
);
}
else
{
$info
[
'id'
]
=
Sanitizer
::
escapeIdForAttribute
(
"{$this->mID}--$key--$fieldname"
);
}
// Copy the hide-if and disable-if rules to "child" fields, so that the JavaScript code handling them
// (resources/src/mediawiki.htmlform/cond-state.js) doesn't have to handle nested fields.
if
(
$this
->
mCondState
)
{
foreach
(
[
'hide'
,
'disable'
]
as
$type
)
{
if
(
!
isset
(
$this
->
mCondState
[
$type
]
)
)
{
continue
;
}
$param
=
$type
.
'-if'
;
if
(
isset
(
$info
[
$param
]
)
)
{
// Hide or disable child field if either its rules say so, or parent's rules say so.
$info
[
$param
]
=
[
'OR'
,
$info
[
$param
],
$this
->
mCondState
[
$type
]
];
}
else
{
// Hide or disable child field if parent's rules say so.
$info
[
$param
]
=
$this
->
mCondState
[
$type
];
}
}
}
$cloner
=
$this
;
$info
[
'cloner'
]
=
&
$cloner
;
$info
[
'cloner-key'
]
=
$key
;
$field
=
HTMLForm
::
loadInputFromParameters
(
$fieldname
,
$info
,
$this
->
mParent
);
$fields
[
$fieldname
]
=
$field
;
}
return
$fields
;
}
/**
* Re-key the specified values array to match the names applied by
* createFieldsForKey().
*
* @param string $key Array key under which these fields should be named
* @param array $values Values array from the request
* @return array
*/
protected
function
rekeyValuesArray
(
$key
,
$values
)
{
$data
=
[];
foreach
(
$values
as
$fieldname
=>
$value
)
{
$name
=
"{$this->mName}[$key][$fieldname]"
;
$data
[
$name
]
=
$value
;
}
return
$data
;
}
/**
* @param string $name
* @return string[]
*/
protected
function
parseFieldPath
(
$name
)
{
$fieldKeys
=
[];
while
(
preg_match
(
'/^(.+)
\[
([^
\]
]+)
\]
$/'
,
$name
,
$m
)
)
{
array_unshift
(
$fieldKeys
,
$m
[
2
]
);
$name
=
$m
[
1
];
}
array_unshift
(
$fieldKeys
,
$name
);
return
$fieldKeys
;
}
/**
* Find the nearest field to a field in this cloner matched the given name,
* walk through the chain of cloners.
*
* @param HTMLFormField $field
* @param string $find
* @return HTMLFormField|null
*/
public
function
findNearestField
(
$field
,
$find
)
{
$findPath
=
$this
->
parseFieldPath
(
$find
);
// Access to fields as child or in other group is not allowed.
// Further support for a more complicated path may conduct here.
if
(
count
(
$findPath
)
>
1
)
{
return
null
;
}
if
(
!
isset
(
$this
->
mParams
[
'fields'
][
$find
]
)
)
{
$cloner
=
$this
->
mParams
[
'cloner'
]
??
null
;
if
(
$cloner
instanceof
self
)
{
return
$cloner
->
findNearestField
(
$this
,
$find
);
}
return
null
;
}
$fields
=
$this
->
getFieldsForKey
(
$field
->
mParams
[
'cloner-key'
]
);
return
$fields
[
$find
];
}
/**
* @param HTMLFormField $field
* @return string[]
*/
protected
function
getFieldPath
(
$field
)
{
$path
=
[
$this
->
mParams
[
'fieldname'
],
$field
->
mParams
[
'cloner-key'
]
];
$cloner
=
$this
->
mParams
[
'cloner'
]
??
null
;
if
(
$cloner
instanceof
self
)
{
$path
=
array_merge
(
$cloner
->
getFieldPath
(
$this
),
$path
);
}
return
$path
;
}
/**
* Extract field data for a given field that belongs to this cloner.
*
* @param HTMLFormField $field
* @param mixed[] $alldata
* @return mixed
*/
public
function
extractFieldData
(
$field
,
$alldata
)
{
foreach
(
$this
->
getFieldPath
(
$field
)
as
$key
)
{
$alldata
=
$alldata
[
$key
];
}
return
$alldata
[
$field
->
mParams
[
'fieldname'
]];
}
protected
function
needsLabel
()
{
return
false
;
}
public
function
loadDataFromRequest
(
$request
)
{
// It's possible that this might be posted with no fields. Detect that
// by looking for an edit token.
if
(
!
$request
->
getCheck
(
'wpEditToken'
)
&&
$request
->
getArray
(
$this
->
mName
)
===
null
)
{
return
$this
->
getDefault
();
}
$values
=
$request
->
getArray
(
$this
->
mName
)
??
[];
$ret
=
[];
foreach
(
$values
as
$key
=>
$value
)
{
if
(
$key
===
'create'
||
isset
(
$value
[
'delete'
]
)
)
{
$ret
[
'nonjs'
]
=
1
;
continue
;
}
// Add back in $request->getValues() so things that look for e.g.
// wpEditToken don't fail.
$data
=
$this
->
rekeyValuesArray
(
$key
,
$value
)
+
$request
->
getValues
();
$fields
=
$this
->
getFieldsForKey
(
$key
);
$subrequest
=
new
DerivativeRequest
(
$request
,
$data
,
$request
->
wasPosted
()
);
$row
=
[];
foreach
(
$fields
as
$fieldname
=>
$field
)
{
if
(
$field
->
skipLoadData
(
$subrequest
)
)
{
continue
;
}
if
(
!
empty
(
$field
->
mParams
[
'disabled'
]
)
)
{
$row
[
$fieldname
]
=
$field
->
getDefault
();
}
else
{
$row
[
$fieldname
]
=
$field
->
loadDataFromRequest
(
$subrequest
);
}
}
$ret
[]
=
$row
;
}
if
(
isset
(
$values
[
'create'
]
)
)
{
// Non-JS client clicked the "create" button.
$fields
=
$this
->
getFieldsForKey
(
$this
->
uniqueId
);
$row
=
[];
foreach
(
$fields
as
$fieldname
=>
$field
)
{
if
(
!
empty
(
$field
->
mParams
[
'nodata'
]
)
)
{
continue
;
}
$row
[
$fieldname
]
=
$field
->
getDefault
();
}
$ret
[]
=
$row
;
}
return
$ret
;
}
public
function
getDefault
()
{
$ret
=
parent
::
getDefault
();
// The default is one entry with all subfields at their defaults.
if
(
$ret
===
null
)
{
$fields
=
$this
->
getFieldsForKey
(
$this
->
uniqueId
);
$row
=
[];
foreach
(
$fields
as
$fieldname
=>
$field
)
{
if
(
!
empty
(
$field
->
mParams
[
'nodata'
]
)
)
{
continue
;
}
$row
[
$fieldname
]
=
$field
->
getDefault
();
}
$ret
=
[
$row
];
}
return
$ret
;
}
/**
* @inheritDoc
* @phan-param array[] $values
*/
public
function
cancelSubmit
(
$values
,
$alldata
)
{
if
(
isset
(
$values
[
'nonjs'
]
)
)
{
return
true
;
}
foreach
(
$values
as
$key
=>
$value
)
{
$fields
=
$this
->
getFieldsForKey
(
$key
);
foreach
(
$fields
as
$fieldname
=>
$field
)
{
if
(
!
array_key_exists
(
$fieldname
,
$value
)
)
{
continue
;
}
if
(
$field
->
cancelSubmit
(
$value
[
$fieldname
],
$alldata
)
)
{
return
true
;
}
}
}
return
parent
::
cancelSubmit
(
$values
,
$alldata
);
}
/**
* @inheritDoc
* @phan-param array[] $values
*/
public
function
validate
(
$values
,
$alldata
)
{
if
(
isset
(
$this
->
mParams
[
'required'
]
)
&&
$this
->
mParams
[
'required'
]
!==
false
&&
!
$values
)
{
return
$this
->
msg
(
'htmlform-cloner-required'
);
}
if
(
isset
(
$values
[
'nonjs'
]
)
)
{
// The submission was a non-JS create/delete click, so fail
// validation in case cancelSubmit() somehow didn't already handle
// it.
return
false
;
}
foreach
(
$values
as
$key
=>
$value
)
{
$fields
=
$this
->
getFieldsForKey
(
$key
);
foreach
(
$fields
as
$fieldname
=>
$field
)
{
if
(
!
array_key_exists
(
$fieldname
,
$value
)
||
$field
->
isHidden
(
$alldata
)
)
{
continue
;
}
$ok
=
$field
->
validate
(
$value
[
$fieldname
],
$alldata
);
if
(
$ok
!==
true
)
{
return
false
;
}
}
}
return
parent
::
validate
(
$values
,
$alldata
);
}
/**
* Get the input HTML for the specified key.
*
* @param string $key Array key under which the fields should be named
* @param array $values
* @return string
*/
protected
function
getInputHTMLForKey
(
$key
,
array
$values
)
{
$displayFormat
=
$this
->
mParams
[
'format'
]
??
$this
->
mParent
->
getDisplayFormat
();
// Conveniently, PHP method names are case-insensitive.
$getFieldHtmlMethod
=
$displayFormat
==
'table'
?
'getTableRow'
:
(
'get'
.
$displayFormat
);
$html
=
''
;
$hidden
=
''
;
$hasLabel
=
false
;
$fields
=
$this
->
getFieldsForKey
(
$key
);
foreach
(
$fields
as
$fieldname
=>
$field
)
{
$v
=
array_key_exists
(
$fieldname
,
$values
)
?
$values
[
$fieldname
]
:
$field
->
getDefault
();
if
(
$field
instanceof
HTMLHiddenField
)
{
// HTMLHiddenField doesn't generate its own HTML
[
$name
,
$value
,
$params
]
=
$field
->
getHiddenFieldData
(
$v
);
$hidden
.=
Html
::
hidden
(
$name
,
$value
,
$params
)
.
"
\n
"
;
}
else
{
$html
.=
$field
->
$getFieldHtmlMethod
(
$v
);
$labelValue
=
trim
(
$field
->
getLabel
()
);
if
(
$labelValue
!==
"
\u
{00A0}"
&&
$labelValue
!==
' '
&&
$labelValue
!==
''
)
{
$hasLabel
=
true
;
}
}
}
if
(
!
isset
(
$fields
[
'delete'
]
)
)
{
$field
=
$this
->
getDeleteButtonHtml
(
$key
);
if
(
$displayFormat
===
'table'
)
{
$html
.=
$field
->
$getFieldHtmlMethod
(
$field
->
getDefault
()
);
}
else
{
$html
.=
$field
->
getInputHTML
(
$field
->
getDefault
()
);
}
}
if
(
$displayFormat
!==
'raw'
)
{
$classes
=
[
'mw-htmlform-cloner-row'
];
if
(
!
$hasLabel
)
{
// Avoid strange spacing when no labels exist
$classes
[]
=
'mw-htmlform-nolabel'
;
}
$attribs
=
[
'class'
=>
$classes
];
if
(
$displayFormat
===
'table'
)
{
$html
=
Html
::
rawElement
(
'table'
,
$attribs
,
Html
::
rawElement
(
'tbody'
,
[],
"
\n
$html
\n
"
)
)
.
"
\n
"
;
}
else
{
$html
=
Html
::
rawElement
(
'div'
,
$attribs
,
"
\n
$html
\n
"
);
}
}
$html
.=
$hidden
;
if
(
!
empty
(
$this
->
mParams
[
'row-legend'
]
)
)
{
$legend
=
$this
->
msg
(
$this
->
mParams
[
'row-legend'
]
)->
text
();
$html
=
Xml
::
fieldset
(
$legend
,
$html
);
}
return
$html
;
}
/**
* @param string $key Array key indicating to which field the delete button belongs
* @return HTMLFormField
*/
protected
function
getDeleteButtonHtml
(
$key
):
HTMLFormField
{
$name
=
"{$this->mName}[$key][delete]"
;
$label
=
$this
->
mParams
[
'delete-button-message'
]
??
'htmlform-cloner-delete'
;
$field
=
HTMLForm
::
loadInputFromParameters
(
$name
,
[
'type'
=>
'submit'
,
'formnovalidate'
=>
true
,
'name'
=>
$name
,
'id'
=>
Sanitizer
::
escapeIdForAttribute
(
"{$this->mID}--$key--delete"
),
'cssclass'
=>
'mw-htmlform-cloner-delete-button'
,
'default'
=>
$this
->
getMessage
(
$label
)->
text
(),
'disabled'
=>
$this
->
mParams
[
'disabled'
]
??
false
,
],
$this
->
mParent
);
return
$field
;
}
protected
function
getCreateButtonHtml
():
HTMLFormField
{
$name
=
"{$this->mName}[create]"
;
$label
=
$this
->
mParams
[
'create-button-message'
]
??
'htmlform-cloner-create'
;
return
HTMLForm
::
loadInputFromParameters
(
$name
,
[
'type'
=>
'submit'
,
'formnovalidate'
=>
true
,
'name'
=>
$name
,
'id'
=>
Sanitizer
::
escapeIdForAttribute
(
"{$this->mID}--create"
),
'cssclass'
=>
'mw-htmlform-cloner-create-button'
,
'default'
=>
$this
->
getMessage
(
$label
)->
text
(),
'disabled'
=>
$this
->
mParams
[
'disabled'
]
??
false
,
],
$this
->
mParent
);
}
public
function
getInputHTML
(
$values
)
{
$html
=
''
;
foreach
(
(
array
)
$values
as
$key
=>
$value
)
{
if
(
$key
===
'nonjs'
)
{
continue
;
}
$html
.=
Html
::
rawElement
(
'li'
,
[
'class'
=>
'mw-htmlform-cloner-li'
],
$this
->
getInputHTMLForKey
(
$key
,
$value
)
);
}
$template
=
$this
->
getInputHTMLForKey
(
$this
->
uniqueId
,
[]
);
$html
=
Html
::
rawElement
(
'ul'
,
[
'id'
=>
"mw-htmlform-cloner-list-{$this->mID}"
,
'class'
=>
'mw-htmlform-cloner-ul'
,
'data-template'
=>
$template
,
'data-unique-id'
=>
$this
->
uniqueId
,
],
$html
);
$field
=
$this
->
getCreateButtonHtml
();
$html
.=
$field
->
getInputHTML
(
$field
->
getDefault
()
);
return
$html
;
}
/**
* Get the input OOUI HTML for the specified key.
*
* @param string $key Array key under which the fields should be named
* @param array $values
* @return string
*/
protected
function
getInputOOUIForKey
(
$key
,
array
$values
)
{
$html
=
''
;
$hidden
=
''
;
$fields
=
$this
->
getFieldsForKey
(
$key
);
foreach
(
$fields
as
$fieldname
=>
$field
)
{
$v
=
array_key_exists
(
$fieldname
,
$values
)
?
$values
[
$fieldname
]
:
$field
->
getDefault
();
if
(
$field
instanceof
HTMLHiddenField
)
{
// HTMLHiddenField doesn't generate its own HTML
[
$name
,
$value
,
$params
]
=
$field
->
getHiddenFieldData
(
$v
);
$hidden
.=
Html
::
hidden
(
$name
,
$value
,
$params
)
.
"
\n
"
;
}
else
{
$html
.=
$field
->
getOOUI
(
$v
);
}
}
if
(
!
isset
(
$fields
[
'delete'
]
)
)
{
$field
=
$this
->
getDeleteButtonHtml
(
$key
);
$fieldHtml
=
$field
->
getInputOOUI
(
$field
->
getDefault
()
);
$fieldHtml
->
setInfusable
(
true
);
$html
.=
$fieldHtml
;
}
$html
=
Html
::
rawElement
(
'div'
,
[
'class'
=>
'mw-htmlform-cloner-row'
],
"
\n
$html
\n
"
);
$html
.=
$hidden
;
if
(
!
empty
(
$this
->
mParams
[
'row-legend'
]
)
)
{
$legend
=
$this
->
msg
(
$this
->
mParams
[
'row-legend'
]
)->
text
();
$html
=
Xml
::
fieldset
(
$legend
,
$html
);
}
return
$html
;
}
public
function
getInputOOUI
(
$values
)
{
$html
=
''
;
foreach
(
(
array
)
$values
as
$key
=>
$value
)
{
if
(
$key
===
'nonjs'
)
{
continue
;
}
$html
.=
Html
::
rawElement
(
'li'
,
[
'class'
=>
'mw-htmlform-cloner-li'
],
$this
->
getInputOOUIForKey
(
$key
,
$value
)
);
}
$template
=
$this
->
getInputOOUIForKey
(
$this
->
uniqueId
,
[]
);
$html
=
Html
::
rawElement
(
'ul'
,
[
'id'
=>
"mw-htmlform-cloner-list-{$this->mID}"
,
'class'
=>
'mw-htmlform-cloner-ul'
,
'data-template'
=>
$template
,
'data-unique-id'
=>
$this
->
uniqueId
,
],
$html
);
$field
=
$this
->
getCreateButtonHtml
();
$fieldHtml
=
$field
->
getInputOOUI
(
$field
->
getDefault
()
);
$fieldHtml
->
setInfusable
(
true
);
$html
.=
$fieldHtml
;
return
$html
;
}
}
/** @deprecated class alias since 1.42 */
class_alias
(
HTMLFormFieldCloner
::
class
,
'HTMLFormFieldCloner'
);
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 21:31 (1 d, 6 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
8d/06/fab67b9fc20f55431b441c3d246f
Default Alt Text
HTMLFormFieldCloner.php (17 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment