<?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');
}
|