529 lines
16 KiB
PHP
529 lines
16 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @defgroup form Form
|
|
* Implements a toolkit for the server-side implementation of forms, including
|
|
* initializing forms with presets, reading submitted content, validating
|
|
* content, and saving the results.
|
|
*/
|
|
|
|
/**
|
|
* @file classes/form/Form.php
|
|
*
|
|
* Copyright (c) 2014-2021 Simon Fraser University
|
|
* Copyright (c) 2000-2021 John Willinsky
|
|
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
|
*
|
|
* @class Form
|
|
*
|
|
* @ingroup core
|
|
*
|
|
* @brief Class defining basic operations for handling HTML forms.
|
|
*/
|
|
|
|
namespace PKP\form;
|
|
|
|
use APP\core\Application;
|
|
use APP\notification\NotificationManager;
|
|
use APP\template\TemplateManager;
|
|
use PKP\core\PKPRequest;
|
|
use PKP\facades\Locale;
|
|
use PKP\form\validation\FormValidator;
|
|
use PKP\notification\PKPNotification;
|
|
use PKP\plugins\Hook;
|
|
use PKP\session\SessionManager;
|
|
|
|
class Form
|
|
{
|
|
/** @var string The template file containing the HTML form */
|
|
public $_template;
|
|
|
|
/** @var array Associative array containing form data */
|
|
public $_data;
|
|
|
|
/** @var array Validation checks for this form */
|
|
public $_checks;
|
|
|
|
/** @var array Errors occurring in form validation */
|
|
public $_errors;
|
|
|
|
/** @var array Array of field names where an error occurred and the associated error message */
|
|
public $errorsArray;
|
|
|
|
/** @var array Array of field names where an error occurred */
|
|
public $errorFields;
|
|
|
|
/** @var array Array of errors for the form section currently being processed */
|
|
public $formSectionErrors;
|
|
|
|
/** @var array Client-side validation rules */
|
|
public $cssValidation;
|
|
|
|
/** @var string Symbolic name of required locale */
|
|
public $requiredLocale;
|
|
|
|
/** @var array Set of supported locales */
|
|
public $supportedLocales;
|
|
|
|
/** @var string Default form locale */
|
|
public $defaultLocale;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param string $template the path to the form template file
|
|
* @param null|mixed $requiredLocale
|
|
* @param null|mixed $supportedLocales
|
|
*/
|
|
public function __construct($template = null, $callHooks = true, $requiredLocale = null, $supportedLocales = null)
|
|
{
|
|
if ($requiredLocale === null) {
|
|
$requiredLocale = Locale::getPrimaryLocale();
|
|
}
|
|
$this->requiredLocale = $requiredLocale;
|
|
if ($supportedLocales === null) {
|
|
$supportedLocales = Locale::getSupportedFormLocales();
|
|
}
|
|
$this->supportedLocales = $supportedLocales;
|
|
|
|
$this->defaultLocale = Locale::getLocale();
|
|
|
|
$this->_template = $template;
|
|
$this->_data = [];
|
|
$this->_checks = [];
|
|
$this->_errors = [];
|
|
$this->errorsArray = [];
|
|
$this->errorFields = [];
|
|
$this->formSectionErrors = [];
|
|
|
|
if ($callHooks === true) {
|
|
// Call hooks based on the calling entity, assuming
|
|
// this method is only called by a subclass. Results
|
|
// in hook calls named e.g. "papergalleyform::Constructor"
|
|
// Note that class names are always lower case.
|
|
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
|
Hook::call(strtolower_codesafe(end($classNameParts)) . '::Constructor', [$this, &$template]);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// Setters and Getters
|
|
//
|
|
/**
|
|
* Set the template
|
|
*
|
|
* @param string $template
|
|
*/
|
|
public function setTemplate($template)
|
|
{
|
|
$this->_template = $template;
|
|
}
|
|
|
|
/**
|
|
* Get the template
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getTemplate()
|
|
{
|
|
return $this->_template;
|
|
}
|
|
|
|
/**
|
|
* Get the required locale for this form (i.e. the locale for which
|
|
* required fields must be set, all others being optional)
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRequiredLocale()
|
|
{
|
|
return $this->requiredLocale;
|
|
}
|
|
|
|
//
|
|
// Public Methods
|
|
//
|
|
/**
|
|
* Display the form.
|
|
*
|
|
* @param PKPRequest $request
|
|
* @param string $template the template to be rendered, mandatory
|
|
* if no template has been specified on class instantiation.
|
|
*/
|
|
public function display($request = null, $template = null)
|
|
{
|
|
$this->fetch($request, $template, true);
|
|
}
|
|
|
|
/**
|
|
* Returns a string of the rendered form
|
|
*
|
|
* @param PKPRequest $request
|
|
* @param string $template the template to be rendered, mandatory
|
|
* if no template has been specified on class instantiation.
|
|
* @param bool $display
|
|
*
|
|
* @return string the rendered form
|
|
*/
|
|
public function fetch($request, $template = null, $display = false)
|
|
{
|
|
// Set custom template.
|
|
if (!is_null($template)) {
|
|
$this->_template = $template;
|
|
}
|
|
|
|
// Call hooks based on the calling entity, assuming
|
|
// this method is only called by a subclass. Results
|
|
// in hook calls named e.g. "papergalleyform::display"
|
|
// Note that class names are always lower case.
|
|
$returner = null;
|
|
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
|
if (Hook::call(strtolower_codesafe(end($classNameParts)) . '::display', [$this, &$returner])) {
|
|
return $returner;
|
|
}
|
|
|
|
$templateMgr = TemplateManager::getManager($request);
|
|
$templateMgr->setCacheability(TemplateManager::CACHEABILITY_NO_STORE);
|
|
|
|
$context = $request->getContext();
|
|
|
|
// Attach this form object to the Form Builder Vocabulary for validation to work
|
|
$fbv = $templateMgr->getFBV();
|
|
$fbv->setForm($this);
|
|
|
|
$templateMgr->assign(array_merge(
|
|
$this->_data,
|
|
[
|
|
'isError' => !$this->isValid(),
|
|
'errors' => $this->getErrorsArray(),
|
|
'formLocales' => $this->supportedLocales,
|
|
'formLocale' => $this->getDefaultFormLocale(),
|
|
]
|
|
));
|
|
|
|
if (!$templateMgr->getTemplateVars('primaryLocale')) {
|
|
$templateMgr->assign([
|
|
'primaryLocale' => $context
|
|
? $context->getPrimaryLocale()
|
|
: (Application::isInstalled() ? $request->getSite()->getPrimaryLocale() : null),
|
|
]);
|
|
}
|
|
|
|
if ($display) {
|
|
$templateMgr->display($this->_template);
|
|
$returner = null;
|
|
} else {
|
|
$returner = $templateMgr->fetch($this->_template);
|
|
}
|
|
|
|
// Reset the FBV's form in case template manager fetches another template not within a form.
|
|
$fbv->setForm(null);
|
|
|
|
return $returner;
|
|
}
|
|
|
|
/**
|
|
* Get the value of a form field.
|
|
*
|
|
* @param string $key
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getData($key)
|
|
{
|
|
return $this->_data[$key] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Set the value of one or several form fields.
|
|
*
|
|
* @param string|array $key If a string, then set a single field. If an associative array, then set many.
|
|
* @param null|mixed $value
|
|
*/
|
|
public function setData($key, $value = null)
|
|
{
|
|
if (is_array($key)) {
|
|
foreach ($key as $aKey => $aValue) {
|
|
$this->setData($aKey, $aValue);
|
|
}
|
|
} else {
|
|
$this->_data[$key] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize form data for a new form.
|
|
*/
|
|
public function initData()
|
|
{
|
|
// Call hooks based on the calling entity, assuming
|
|
// this method is only called by a subclass. Results
|
|
// in hook calls named e.g. "papergalleyform::initData"
|
|
// Note that class and function names are always lower
|
|
// case.
|
|
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
|
Hook::call(strtolower_codesafe(end($classNameParts) . '::initData'), [$this]);
|
|
}
|
|
|
|
/**
|
|
* Assign form data to user-submitted data.
|
|
* Can be overridden from subclasses.
|
|
*/
|
|
public function readInputData()
|
|
{
|
|
// Default implementation does nothing.
|
|
}
|
|
|
|
/**
|
|
* Validate form data.
|
|
*
|
|
* @param bool $callHooks True (default) iff hooks are to be called.
|
|
*/
|
|
public function validate($callHooks = true)
|
|
{
|
|
if (!isset($this->errorsArray)) {
|
|
$this->getErrorsArray();
|
|
}
|
|
|
|
foreach ($this->_checks as $check) {
|
|
if (!isset($this->errorsArray[$check->getField()]) && !$check->isValid()) {
|
|
if (method_exists($check, 'getErrorFields') && method_exists($check, 'isArray') && call_user_func([&$check, 'isArray'])) {
|
|
$errorFields = call_user_func([&$check, 'getErrorFields']);
|
|
for ($i = 0, $count = count($errorFields); $i < $count; $i++) {
|
|
$this->addError($errorFields[$i], $check->getMessage());
|
|
$this->errorFields[$errorFields[$i]] = 1;
|
|
}
|
|
} else {
|
|
$this->addError($check->getField(), $check->getMessage());
|
|
$this->errorFields[$check->getField()] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($callHooks === true) {
|
|
// Call hooks based on the calling entity, assuming
|
|
// this method is only called by a subclass. Results
|
|
// in hook calls named e.g. "papergalleyform::validate"
|
|
// Note that class and function names are always lower
|
|
// case.
|
|
$value = null;
|
|
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
|
if (Hook::call(strtolower_codesafe(end($classNameParts) . '::validate'), [$this, &$value])) {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
if (!SessionManager::isDisabled()) {
|
|
$request = Application::get()->getRequest();
|
|
$user = $request->getUser();
|
|
|
|
if (!$this->isValid() && $user) {
|
|
// Create a form error notification.
|
|
$notificationManager = new NotificationManager();
|
|
$notificationManager->createTrivialNotification(
|
|
$user->getId(),
|
|
PKPNotification::NOTIFICATION_TYPE_FORM_ERROR,
|
|
['contents' => $this->getErrorsArray()]
|
|
);
|
|
}
|
|
}
|
|
|
|
return $this->isValid();
|
|
}
|
|
|
|
/**
|
|
* Execute the form's action.
|
|
* (Note that it is assumed that the form has already been validated.)
|
|
*
|
|
* @return mixed Result from the consumer to be passed to the caller. Send a true-ish result if you want the caller to do something with the return value.
|
|
*/
|
|
public function execute(...$functionArgs)
|
|
{
|
|
// Call hooks based on the calling entity, assuming
|
|
// this method is only called by a subclass. Results
|
|
// in hook calls named e.g. "papergalleyform::execute"
|
|
// Note that class and function names are always lower
|
|
// case.
|
|
$returner = null;
|
|
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
|
Hook::call(strtolower_codesafe(end($classNameParts) . '::execute'), array_merge([$this], $functionArgs, [&$returner]));
|
|
return $returner;
|
|
}
|
|
|
|
/**
|
|
* Get the list of field names that need to support multiple locales
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getLocaleFieldNames()
|
|
{
|
|
// Call hooks based on the calling entity, assuming
|
|
// this method is only called by a subclass. Results
|
|
// in hook calls named e.g. "papergalleyform::getLocaleFieldNames"
|
|
// Note that class and function names are always lower
|
|
// case.
|
|
$returner = [];
|
|
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
|
Hook::call(strtolower_codesafe(end($classNameParts) . '::getLocaleFieldNames'), [$this, &$returner]);
|
|
return $returner;
|
|
}
|
|
|
|
/**
|
|
* Get the default form locale.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getDefaultFormLocale()
|
|
{
|
|
$formLocale = $this->defaultLocale;
|
|
if (!isset($this->supportedLocales[$formLocale])) {
|
|
$formLocale = $this->requiredLocale;
|
|
}
|
|
return $formLocale;
|
|
}
|
|
|
|
/**
|
|
* Set the default form locale.
|
|
*
|
|
* @param string $defaultLocale
|
|
*/
|
|
public function setDefaultFormLocale($defaultLocale)
|
|
{
|
|
$this->defaultLocale = $defaultLocale;
|
|
}
|
|
|
|
/**
|
|
* Add a supported locale.
|
|
*
|
|
* @param string $supportedLocale
|
|
*/
|
|
public function addSupportedFormLocale($supportedLocale)
|
|
{
|
|
if (!in_array($supportedLocale, $this->supportedLocales)) {
|
|
$site = Application::get()->getRequest()->getSite();
|
|
$siteSupportedLocales = $site->getSupportedLocaleNames();
|
|
if (array_key_exists($supportedLocale, $siteSupportedLocales)) {
|
|
$this->supportedLocales[$supportedLocale] = $siteSupportedLocales[$supportedLocale];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds specified user variables to input data.
|
|
*
|
|
* @param array $vars the names of the variables to read
|
|
*/
|
|
public function readUserVars($vars)
|
|
{
|
|
// Call hooks based on the calling entity, assuming
|
|
// this method is only called by a subclass. Results
|
|
// in hook calls named e.g. "papergalleyform::readUserVars"
|
|
// Note that class and function names are always lower
|
|
// case.
|
|
$classNameParts = explode('\\', get_class($this)); // Separate namespace info from class name
|
|
Hook::call(strtolower_codesafe(end($classNameParts) . '::readUserVars'), [$this, &$vars]);
|
|
$request = Application::get()->getRequest();
|
|
foreach ($vars as $k) {
|
|
$this->setData($k, $request->getUserVar($k));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a validation check to the form.
|
|
*
|
|
* @param FormValidator $formValidator
|
|
*/
|
|
public function addCheck($formValidator)
|
|
{
|
|
$this->_checks[] = & $formValidator;
|
|
}
|
|
|
|
/**
|
|
* Add an error to the form.
|
|
* Errors are typically assigned as the form is validated.
|
|
*
|
|
* @param string $field the name of the field where the error occurred
|
|
*/
|
|
public function addError($field, $message)
|
|
{
|
|
$this->_errors[] = new FormError($field, $message);
|
|
}
|
|
|
|
/**
|
|
* Add an error field for highlighting on form
|
|
*
|
|
* @param string $field the name of the field where the error occurred
|
|
*/
|
|
public function addErrorField($field)
|
|
{
|
|
$this->errorFields[$field] = 1;
|
|
}
|
|
|
|
/**
|
|
* Check if form passes all validation checks.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isValid()
|
|
{
|
|
return empty($this->_errors);
|
|
}
|
|
|
|
/**
|
|
* Return set of errors that occurred in form validation.
|
|
* If multiple errors occurred processing a single field, only the first error is included.
|
|
*
|
|
* @return array erroneous fields and associated error messages
|
|
*/
|
|
public function getErrorsArray()
|
|
{
|
|
$this->errorsArray = [];
|
|
foreach ($this->_errors as $error) {
|
|
if (!isset($this->errorsArray[$error->getField()])) {
|
|
$this->errorsArray[$error->getField()] = $error->getMessage();
|
|
}
|
|
}
|
|
return $this->errorsArray;
|
|
}
|
|
|
|
//
|
|
// Private helper methods
|
|
//
|
|
/**
|
|
* Convert PHP variable (literals or arrays) into HTML containing
|
|
* hidden input fields.
|
|
*
|
|
* @param string $name Name of variable
|
|
* @param mixed $value Value of variable
|
|
* @param array $stack Names of array keys (for recursive calling)
|
|
*
|
|
* @return string HTML hidden form elements describing the parameters.
|
|
*/
|
|
public function _decomposeArray($name, $value, $stack)
|
|
{
|
|
$returner = '';
|
|
if (is_array($value)) {
|
|
foreach ($value as $key => $subValue) {
|
|
$newStack = $stack;
|
|
$newStack[] = $key;
|
|
$returner .= $this->_decomposeArray($name, $subValue, $newStack);
|
|
}
|
|
} else {
|
|
$name = htmlentities($name, ENT_COMPAT);
|
|
$value = htmlentities($value, ENT_COMPAT);
|
|
$returner .= '<input type="hidden" name="' . $name;
|
|
while (($item = array_shift($stack)) !== null) {
|
|
$item = htmlentities($item, ENT_COMPAT);
|
|
$returner .= '[' . $item . ']';
|
|
}
|
|
$returner .= '" value="' . $value . "\" />\n";
|
|
}
|
|
return $returner;
|
|
}
|
|
}
|
|
|
|
if (!PKP_STRICT_MODE) {
|
|
class_alias('\PKP\form\Form', '\Form');
|
|
}
|