diff --git a/core/includes/form.inc b/core/includes/form.inc index 7498ea5..47c14ab 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -159,6 +159,8 @@ function drupal_get_form($form_id) { * - build_info: Internal. An associative array of information stored by Form * API that is necessary to build and rebuild the form from cache when the * original context may no longer be available: + * - callback: The actual callback to be used to retrieve the form array. If + * none is provided $form_id is used instead. Can be any callable type. * - args: A list of arguments to pass to the form constructor. * - files: An optional array defining include files that need to be loaded * for building the form. Each array entry may be the path to a file or @@ -266,11 +268,11 @@ function drupal_get_form($form_id) { * ones used by Form API internals) for this kind of storage. The * recommended way to ensure that the chosen key doesn't conflict with ones * used by the Form API or other modules is to use the module name as the - * key name or a prefix for the key name. For example, the Node module uses - * $form_state['node'] in node editing forms to store information about the - * node being edited, and this information stays available across successive - * clicks of the "Preview" button as well as when the "Save" button is - * finally clicked. + * key name or a prefix for the key name. For example, the entity form + * controller classes use $form_state['entity'] in entity forms to store + * information about the entity being edited, and this information stays + * available across successive clicks of the "Preview" button (if available) + * as well as when the "Save" button is finally clicked. * - buttons: A list containing copies of all submit and button elements in * the form. * - complete_form: A reference to the $form variable containing the complete @@ -747,9 +749,13 @@ function drupal_retrieve_form($form_id, &$form_state) { // the constructor function itself. $args = $form_state['build_info']['args']; - // We first check to see if there's a function named after the $form_id. + // If an explicit form builder callback is defined we just use it, otherwise + // we look for a function named after the $form_id. + $callback = !empty($form_state['build_info']['callback']) ? $form_state['build_info']['callback'] : $form_id; + + // We first check to see if there is a valid form builder callback defined. // If there is, we simply pass the arguments on to it to get the form. - if (!function_exists($form_id)) { + if (!is_callable($callback)) { // In cases where many form_ids need to share a central constructor function, // such as the node editing form, modules can implement hook_forms(). It // maps one or more form_ids to the correct constructor functions. @@ -808,7 +814,7 @@ function drupal_retrieve_form($form_id, &$form_state) { // If $callback was returned by a hook_forms() implementation, call it. // Otherwise, call the function named after the form id. - $form = call_user_func_array(isset($callback) ? $callback : $form_id, $args); + $form = call_user_func_array($callback, $args); $form['#form_id'] = $form_id; return $form; @@ -1469,7 +1475,7 @@ function form_execute_handlers($type, &$form, &$form_state) { $batch['has_form_submits'] = TRUE; } else { - $function($form, $form_state); + call_user_func_array($function, array(&$form, &$form_state)); } $return = TRUE; } @@ -1812,7 +1818,7 @@ function form_builder($form_id, &$element, &$form_state) { // checkboxes and files. if (isset($element['#process']) && !$element['#processed']) { foreach ($element['#process'] as $process) { - $element = $process($element, $form_state, $form_state['complete_form']); + $element = call_user_func_array($process, array(&$element, &$form_state, &$form_state['complete_form'])); } $element['#processed'] = TRUE; } diff --git a/core/modules/block/block.module b/core/modules/block/block.module index c07b73d..fcc76f7 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -5,6 +5,8 @@ * Controls the visual building blocks a page is constructed with. */ +use Drupal\entity\EntityFormController; + /** * Denotes that a block is not enabled in any region and should not be shown. */ @@ -596,7 +598,7 @@ function block_custom_block_save($edit, $delta) { * Implements hook_form_FORM_ID_alter() for user_profile_form(). */ function block_form_user_profile_form_alter(&$form, &$form_state) { - $account = $form['#user']; + $account = $form_state['controller']->getEntity($form_state); $rids = array_keys($account->roles); $result = db_query("SELECT DISTINCT b.* FROM {block} b LEFT JOIN {block_role} r ON b.module = r.module AND b.delta = r.delta WHERE b.status = 1 AND b.custom <> 0 AND (r.rid IN (:rids) OR r.rid IS NULL) ORDER BY b.weight, b.module", array(':rids' => $rids)); diff --git a/core/modules/book/book.module b/core/modules/book/book.module index 86a5d10..c169cac 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -5,6 +5,8 @@ * Allows users to create and organize related content in an outline. */ +use Drupal\entity\EntityFormController; + use Drupal\node\Node; /** @@ -423,7 +425,7 @@ function book_get_books() { * @see book_pick_book_nojs_submit() */ function book_form_node_form_alter(&$form, &$form_state, $form_id) { - $node = $form['#node']; + $node = $form_state['controller']->getEntity($form_state); $access = user_access('administer book outlines'); if (!$access) { if (user_access('add content to books') && ((!empty($node->book['mlid']) && !empty($node->nid)) || book_type_is_allowed($node->type))) { @@ -462,7 +464,8 @@ function book_form_node_form_alter(&$form, &$form_state, $form_id) { * @see book_form_node_form_alter() */ function book_pick_book_nojs_submit($form, &$form_state) { - $form_state['node']->book = $form_state['values']['book']; + $node = $form_state['controller']->getEntity($form_state); + $node->book = $form_state['values']['book']; $form_state['rebuild'] = TRUE; } diff --git a/core/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc index 0a59897..6210cc5 100644 --- a/core/modules/comment/comment.admin.inc +++ b/core/modules/comment/comment.admin.inc @@ -278,7 +278,7 @@ function comment_confirm_delete_page($cid) { * @see confirm_form() */ function comment_confirm_delete($form, &$form_state, Comment $comment) { - $form['#comment'] = $comment; + $form_state['comment'] = $comment; // Always provide entity id in the same form key as in the entity edit form. $form['cid'] = array('#type' => 'value', '#value' => $comment->cid); return confirm_form( @@ -295,7 +295,7 @@ function comment_confirm_delete($form, &$form_state, Comment $comment) { * Form submission handler for comment_confirm_delete(). */ function comment_confirm_delete_submit($form, &$form_state) { - $comment = $form['#comment']; + $comment = $form_state['comment']; // Delete the comment and its replies. comment_delete($comment->cid); drupal_set_message(t('The comment and all its replies have been deleted.')); diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 39faf90..212d471 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -9,7 +9,10 @@ * book page, etc. */ +use Drupal\entity\EntityFormController; + use Drupal\node\Node; + use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -106,6 +109,9 @@ function comment_entity_info() { 'uri callback' => 'comment_uri', 'fieldable' => TRUE, 'controller class' => 'Drupal\comment\CommentStorageController', + 'form controller class' => array( + 'default' => 'Drupal\comment\CommentFormController', + ), 'entity class' => 'Drupal\comment\Comment', 'entity keys' => array( 'id' => 'cid', @@ -771,8 +777,7 @@ function comment_node_page_additions(Node $node) { // Append comment form if needed. if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) { - $comment = entity_create('comment', array('nid' => $node->nid)); - $additions['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment); + $additions['comment_form'] = comment_add($node); } if ($additions) { @@ -788,6 +793,15 @@ function comment_node_page_additions(Node $node) { } /** + * Returns a rendered form to comment the given node. + */ +function comment_add($node, $pid = NULL) { + $values = array('nid' => $node->nid, 'pid' => $pid, 'node_type' => 'comment_node_' . $node->type); + $comment = entity_create('comment', $values); + return entity_get_form($comment); +} + +/** * Retrieves comments for a thread. * * @param Drupal\node\Node $node @@ -1231,7 +1245,7 @@ function comment_form_node_type_form_alter(&$form, $form_state) { * Implements hook_form_BASE_FORM_ID_alter(). */ function comment_form_node_form_alter(&$form, $form_state) { - $node = $form['#node']; + $node = $form_state['controller']->getEntity($form_state); $form['comment_settings'] = array( '#type' => 'fieldset', '#access' => user_access('administer comments'), @@ -1658,247 +1672,17 @@ function comment_get_display_page($cid, $node_type) { */ function comment_edit_page(Comment $comment) { drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH); - $node = node_load($comment->nid); - return drupal_get_form("comment_node_{$node->type}_form", $comment); -} - -/** - * Implements hook_forms(). - */ -function comment_forms() { - $forms = array(); - foreach (node_type_get_types() as $type) { - $forms["comment_node_{$type->type}_form"]['callback'] = 'comment_form'; - } - return $forms; -} - -/** - * Form constructor for the basic commenting form. - * - * @see comment_form_validate() - * @see comment_form_submit() - * @see comment_form_build_preview() - * @ingroup forms - */ -function comment_form($form, &$form_state, Comment $comment) { - global $user; - $language_content = language(LANGUAGE_TYPE_CONTENT); - - // During initial form build, add the comment entity to the form state for - // use during form building and processing. During a rebuild, use what is in - // the form state. - if (!isset($form_state['comment'])) { - $form_state['comment'] = $comment; - } - else { - $comment = $form_state['comment']; - } - - $node = node_load($comment->nid); - $form['#node'] = $node; - - // Use #comment-form as unique jump target, regardless of node type. - $form['#id'] = drupal_html_id('comment_form'); - $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form'); - - $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT); - $is_admin = (!empty($comment->cid) && user_access('administer comments')); - - if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) { - $form['#attached']['library'][] = array('system', 'jquery.cookie'); - $form['#attributes']['class'][] = 'user-info-from-cookie'; - } - - // If not replying to a comment, use our dedicated page callback for new - // comments on nodes. - if (empty($comment->cid) && empty($comment->pid)) { - $form['#action'] = url('comment/reply/' . $comment->nid); - } - - if (isset($form_state['comment_preview'])) { - $form += $form_state['comment_preview']; - } - - $form['author'] = array( - '#weight' => 10, - ); - // Display author information in a fieldset for comment moderators. - if ($is_admin) { - $form['author'] += array( - '#type' => 'fieldset', - '#title' => t('Administration'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - } - - // Prepare default values for form elements. - if ($is_admin) { - $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name); - $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED); - $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O')); - } - else { - if ($user->uid) { - $author = $user->name; - } - else { - $author = ($comment->name ? $comment->name : ''); - } - $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED); - $date = ''; - } - - // Add the author name field depending on the current user. - if ($is_admin) { - $form['author']['name'] = array( - '#type' => 'textfield', - '#title' => t('Authored by'), - '#default_value' => $author, - '#maxlength' => 60, - '#size' => 30, - '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))), - '#autocomplete_path' => 'user/autocomplete', - ); - } - elseif ($user->uid) { - $form['author']['_author'] = array( - '#type' => 'item', - '#title' => t('Your name'), - '#markup' => theme('username', array('account' => $user)), - ); - $form['author']['name'] = array( - '#type' => 'value', - '#value' => $author, - ); - } - else { - $form['author']['name'] = array( - '#type' => 'textfield', - '#title' => t('Your name'), - '#default_value' => $author, - '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT), - '#maxlength' => 60, - '#size' => 30, - ); - } - - // Add author e-mail and homepage fields depending on the current user. - $form['author']['mail'] = array( - '#type' => 'email', - '#title' => t('E-mail'), - '#default_value' => $comment->mail, - '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT), - '#maxlength' => 64, - '#size' => 30, - '#description' => t('The content of this field is kept private and will not be shown publicly.'), - '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), - ); - $form['author']['homepage'] = array( - '#type' => 'url', - '#title' => t('Homepage'), - '#default_value' => $comment->homepage, - '#maxlength' => 255, - '#size' => 30, - '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), - ); - - // Add administrative comment publishing options. - $form['author']['date'] = array( - '#type' => 'textfield', - '#title' => t('Authored on'), - '#default_value' => $date, - '#maxlength' => 25, - '#size' => 20, - '#access' => $is_admin, - ); - $form['author']['status'] = array( - '#type' => 'radios', - '#title' => t('Status'), - '#default_value' => $status, - '#options' => array( - COMMENT_PUBLISHED => t('Published'), - COMMENT_NOT_PUBLISHED => t('Not published'), - ), - '#access' => $is_admin, - ); - - $form['subject'] = array( - '#type' => 'textfield', - '#title' => t('Subject'), - '#maxlength' => 64, - '#default_value' => $comment->subject, - '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1, - ); - - // Used for conditional validation of author fields. - $form['is_anonymous'] = array( - '#type' => 'value', - '#value' => ($comment->cid ? !$comment->uid : !$user->uid), - ); - - // Add internal comment properties. - foreach (array('cid', 'pid', 'nid', 'uid') as $key) { - $form[$key] = array('#type' => 'value', '#value' => $comment->$key); - } - $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type); - - // Make the comment inherit the node language unless specifically set. - $comment_langcode = $comment->langcode; - if ($comment_langcode == LANGUAGE_NOT_SPECIFIED) { - $comment_langcode = $language_content->langcode; - } - - // Uses the language of the content as comment language. - $form['langcode'] = array( - '#type' => 'value', - '#value' => $comment_langcode, - ); - - // Only show the save button if comment previews are optional or if we are - // already previewing the submission. - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Save'), - '#access' => ($comment->cid && user_access('administer comments')) || variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || isset($form_state['comment_preview']), - ); - $form['actions']['preview'] = array( - '#type' => 'submit', - '#value' => t('Preview'), - '#access' => (variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED), - '#submit' => array('comment_form_build_preview'), - ); - - // Attach fields. - $comment->node_type = 'comment_node_' . $node->type; - field_attach_form('comment', $comment, $form, $form_state); - - return $form; -} - -/** - * Form submission handler for the 'preview' button in comment_form(). - */ -function comment_form_build_preview($form, &$form_state) { - $comment = comment_form_submit_build_comment($form, $form_state); - $form_state['comment_preview'] = comment_preview($comment); - $form_state['rebuild'] = TRUE; + return entity_get_form($comment); } /** * Generates a comment preview. * * @param Drupal\comment\Comment $comment - * - * @see comment_form_build_preview() */ function comment_preview(Comment $comment) { global $user; - - drupal_set_title(t('Preview comment'), PASS_THROUGH); - + $preview_build = array(); $node = node_load($comment->nid); if (!form_get_errors()) { @@ -1928,7 +1712,7 @@ function comment_preview(Comment $comment) { $comment_build = comment_view($comment, $node); $comment_build['#weight'] = -100; - $form['comment_preview'] = $comment_build; + $preview_build['comment_preview'] = $comment_build; } if ($comment->pid) { @@ -1942,169 +1726,10 @@ function comment_preview(Comment $comment) { $build = node_view($node); } - $form['comment_output_below'] = $build; - $form['comment_output_below']['#weight'] = 100; + $preview_build['comment_output_below'] = $build; + $preview_build['comment_output_below']['#weight'] = 100; - return $form; -} - -/** - * Form validation handler for comment_form(). - * - * @see comment_form_submit() - */ -function comment_form_validate($form, &$form_state) { - global $user; - - entity_form_field_validate('comment', $form, $form_state); - - if (!empty($form_state['values']['cid'])) { - // Verify the name in case it is being changed from being anonymous. - $account = user_load_by_name($form_state['values']['name']); - $form_state['values']['uid'] = $account ? $account->uid : 0; - - if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) { - form_set_error('date', t('You have to specify a valid date.')); - } - if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) { - form_set_error('name', t('You have to specify a valid author.')); - } - } - elseif ($form_state['values']['is_anonymous']) { - // Validate anonymous comment author fields (if given). If the (original) - // author of this comment was an anonymous user, verify that no registered - // user with this name exists. - if ($form_state['values']['name']) { - $query = db_select('users', 'u'); - $query->addField('u', 'uid', 'uid'); - $taken = $query - ->condition('name', db_like($form_state['values']['name']), 'LIKE') - ->countQuery() - ->execute() - ->fetchField(); - if ($taken) { - form_set_error('name', t('The name you used belongs to a registered user.')); - } - } - } -} - -/** - * Prepare a comment for submission. - * - * @param Drupal\comment\Comment $comment - * - */ -function comment_submit(Comment $comment) { - if (empty($comment->date)) { - $comment->date = 'now'; - } - $comment->created = strtotime($comment->date); - $comment->changed = REQUEST_TIME; - - // If the comment was posted by a registered user, assign the author's ID. - // @todo Too fragile. Should be prepared and stored in comment_form() already. - if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) { - $comment->uid = $account->uid; - } - // If the comment was posted by an anonymous user and no author name was - // required, use "Anonymous" by default. - if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) { - $comment->name = variable_get('anonymous', t('Anonymous')); - } - - // Validate the comment's subject. If not specified, extract from comment body. - if (trim($comment->subject) == '') { - // The body may be in any format, so: - // 1) Filter it into HTML - // 2) Strip out all HTML tags - // 3) Convert entities back to plain-text. - $comment_body = $comment->comment_body[LANGUAGE_NOT_SPECIFIED][0]; - if (isset($comment_body['format'])) { - $comment_text = check_markup($comment_body['value'], $comment_body['format']); - } - else { - $comment_text = check_plain($comment_body['value']); - } - $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE); - // Edge cases where the comment body is populated only by HTML tags will - // require a default subject. - if ($comment->subject == '') { - $comment->subject = t('(No subject)'); - } - } - return $comment; -} - -/** - * Updates the comment entity by processing the submission's values. - * - * This is the default builder function for the comment form. It is called - * during the "Save" and "Preview" submit handlers to retrieve the entity to - * save or preview. This function can also be called by a "Next" button of a - * wizard to update the form state's entity with the current step's values - * before proceeding to the next step. - * - * @see comment_form() - * @see comment_form_preview() - * @see comment_form_submit() - */ -function comment_form_submit_build_comment($form, &$form_state) { - $comment = $form_state['comment']; - entity_form_submit_build_entity('comment', $comment, $form, $form_state); - comment_submit($comment); - return $comment; -} - -/** - * Form submission handler for comment_form(). - * - * @see comment_form_validate() - * @see comment_form_submit_build_comment() - */ -function comment_form_submit($form, &$form_state) { - $node = node_load($form_state['values']['nid']); - $comment = comment_form_submit_build_comment($form, $form_state); - if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) { - // Save the anonymous user information to a cookie for reuse. - if (user_is_anonymous()) { - user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage')))); - } - - comment_save($comment); - $form_state['values']['cid'] = $comment->cid; - - // Add an entry to the watchdog log. - watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid))); - - // Explain the approval queue if necessary. - if ($comment->status == COMMENT_NOT_PUBLISHED) { - if (!user_access('administer comments')) { - drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.')); - } - } - else { - drupal_set_message(t('Your comment has been posted.')); - } - $query = array(); - // Find the current display page for this comment. - $page = comment_get_display_page($comment->cid, $node->type); - if ($page > 0) { - $query['page'] = $page; - } - // Redirect to the newly posted comment. - $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid)); - } - else { - watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING); - drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error'); - // Redirect the user to the node they are commenting on. - $redirect = 'node/' . $node->nid; - } - $form_state['redirect'] = $redirect; - // Clear the block and page caches so that anonymous users see the comment - // they have posted. - cache_invalidate(array('content' => TRUE)); + return $preview_build; } /** diff --git a/core/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc index ae50379..ed91d23 100644 --- a/core/modules/comment/comment.pages.inc +++ b/core/modules/comment/comment.pages.inc @@ -43,8 +43,7 @@ function comment_reply(Node $node, $pid = NULL) { // The user is previewing a comment prior to submitting it. if ($op == t('Preview')) { if (user_access('post comments')) { - $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid)); - $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment); + $build['comment_form'] = comment_add($node, $pid); } else { drupal_set_message(t('You are not authorized to post comments.'), 'error'); @@ -92,8 +91,7 @@ function comment_reply(Node $node, $pid = NULL) { drupal_goto("node/$node->nid"); } elseif (user_access('post comments')) { - $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid)); - $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment); + $build['comment_form'] = comment_add($node, $pid); } else { drupal_set_message(t('You are not authorized to post comments.'), 'error'); diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php new file mode 100644 index 0000000..c216707 --- /dev/null +++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php @@ -0,0 +1,378 @@ +nid); + $form_state['comment']['node'] = $node; + + // Use #comment-form as unique jump target, regardless of node type. + $form['#id'] = drupal_html_id('comment_form'); + $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form'); + + $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT); + $is_admin = (!empty($comment->cid) && user_access('administer comments')); + + if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) { + $form['#attached']['library'][] = array('system', 'jquery.cookie'); + $form['#attributes']['class'][] = 'user-info-from-cookie'; + } + + // If not replying to a comment, use our dedicated page callback for new + // comments on nodes. + if (empty($comment->cid) && empty($comment->pid)) { + $form['#action'] = url('comment/reply/' . $comment->nid); + } + + if (isset($form_state['comment_preview'])) { + $form += $form_state['comment_preview']; + } + + $form['author'] = array( + '#weight' => 10, + ); + // Display author information in a fieldset for comment moderators. + if ($is_admin) { + $form['author'] += array( + '#type' => 'fieldset', + '#title' => t('Administration'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + } + + // Prepare default values for form elements. + if ($is_admin) { + $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name); + $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED); + $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O')); + } + else { + if ($user->uid) { + $author = $user->name; + } + else { + $author = ($comment->name ? $comment->name : ''); + } + $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED); + $date = ''; + } + + // Add the author name field depending on the current user. + if ($is_admin) { + $form['author']['name'] = array( + '#type' => 'textfield', + '#title' => t('Authored by'), + '#default_value' => $author, + '#maxlength' => 60, + '#size' => 30, + '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))), + '#autocomplete_path' => 'user/autocomplete', + ); + } + elseif ($user->uid) { + $form['author']['_author'] = array( + '#type' => 'item', + '#title' => t('Your name'), + '#markup' => theme('username', array('account' => $user)), + ); + + $form['author']['name'] = array( + '#type' => 'value', + '#value' => $author, + ); + } + else { + $form['author']['name'] = array( + '#type' => 'textfield', + '#title' => t('Your name'), + '#default_value' => $author, + '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT), + '#maxlength' => 60, + '#size' => 30, + ); + } + + // Add author e-mail and homepage fields depending on the current user. + $form['author']['mail'] = array( + '#type' => 'email', + '#title' => t('E-mail'), + '#default_value' => $comment->mail, + '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT), + '#maxlength' => 64, + '#size' => 30, + '#description' => t('The content of this field is kept private and will not be shown publicly.'), + '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), + ); + + $form['author']['homepage'] = array( + '#type' => 'url', + '#title' => t('Homepage'), + '#default_value' => $comment->homepage, + '#maxlength' => 255, + '#size' => 30, + '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT), + ); + + // Add administrative comment publishing options. + $form['author']['date'] = array( + '#type' => 'textfield', + '#title' => t('Authored on'), + '#default_value' => $date, + '#maxlength' => 25, + '#size' => 20, + '#access' => $is_admin, + ); + + $form['author']['status'] = array( + '#type' => 'radios', + '#title' => t('Status'), + '#default_value' => $status, + '#options' => array( + COMMENT_PUBLISHED => t('Published'), + COMMENT_NOT_PUBLISHED => t('Not published'), + ), + '#access' => $is_admin, + ); + + $form['subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#maxlength' => 64, + '#default_value' => $comment->subject, + '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1, + ); + + // Used for conditional validation of author fields. + $form['is_anonymous'] = array( + '#type' => 'value', + '#value' => ($comment->cid ? !$comment->uid : !$user->uid), + ); + + // Add internal comment properties. + foreach (array('cid', 'pid', 'nid', 'uid') as $key) { + $form[$key] = array('#type' => 'value', '#value' => $comment->$key); + } + $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type); + + // Make the comment inherit the node language unless specifically set. + $comment_langcode = $comment->langcode; + if ($comment_langcode == LANGUAGE_NOT_SPECIFIED) { + $comment_langcode = $language_content->langcode; + } + + // Uses the language of the content as comment language. + $form['langcode'] = array( + '#type' => 'value', + '#value' => $comment_langcode, + ); + + // Attach fields. + $comment->node_type = 'comment_node_' . $node->type; + + return parent::form($form, $form_state, $comment); + } + + /** + * Overrides Drupal\entity\EntityFormController::actions(). + */ + protected function actions(array $form, array &$form_state) { + $element = parent::actions($form, $form_state); + $comment = $this->getEntity($form_state); + $node = $form_state['comment']['node']; + $preview_mode = variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL); + + // No delete action on the comment form. + unset($element['delete']); + + // Only show the save button if comment previews are optional or if we are + // already previewing the submission. + $element['submit']['#access'] = ($comment->cid && user_access('administer comments')) || $preview_mode != DRUPAL_REQUIRED || isset($form_state['comment_preview']); + + $element['preview'] = array( + '#type' => 'submit', + '#value' => t('Preview'), + '#access' => $preview_mode != DRUPAL_DISABLED, + '#validate' => array( + array($this, 'validate'), + ), + '#submit' => array( + array($this, 'submit'), + array($this, 'preview'), + ), + ); + + $element['#weight'] = $form['comment_body']['#weight'] + 0.01; + + return $element; + } + + /** + * Overrides Drupal\entity\EntityFormController::validate(). + */ + public function validate(array $form, array &$form_state) { + parent::validate($form, $form_state); + + if (!empty($form_state['values']['cid'])) { + // Verify the name in case it is being changed from being anonymous. + $account = user_load_by_name($form_state['values']['name']); + $form_state['values']['uid'] = $account ? $account->uid : 0; + + if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) { + form_set_error('date', t('You have to specify a valid date.')); + } + if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) { + form_set_error('name', t('You have to specify a valid author.')); + } + } + elseif ($form_state['values']['is_anonymous']) { + // Validate anonymous comment author fields (if given). If the (original) + // author of this comment was an anonymous user, verify that no registered + // user with this name exists. + if ($form_state['values']['name']) { + $query = db_select('users', 'u'); + $query->addField('u', 'uid', 'uid'); + $taken = $query + ->condition('name', db_like($form_state['values']['name']), 'LIKE') + ->countQuery() + ->execute() + ->fetchField(); + if ($taken) { + form_set_error('name', t('The name you used belongs to a registered user.')); + } + } + } + } + + /** + * Overrides Drupal\entity\EntityFormController::submit(). + */ + public function submit(array $form, array &$form_state) { + $comment = parent::submit($form, $form_state); + + if (empty($comment->date)) { + $comment->date = 'now'; + } + $comment->created = strtotime($comment->date); + $comment->changed = REQUEST_TIME; + + // If the comment was posted by a registered user, assign the author's ID. + // @todo Too fragile. Should be prepared and stored in comment_form() + // already. + if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) { + $comment->uid = $account->uid; + } + // If the comment was posted by an anonymous user and no author name was + // required, use "Anonymous" by default. + if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) { + $comment->name = variable_get('anonymous', t('Anonymous')); + } + + // Validate the comment's subject. If not specified, extract from comment + // body. + if (trim($comment->subject) == '') { + // The body may be in any format, so: + // 1) Filter it into HTML + // 2) Strip out all HTML tags + // 3) Convert entities back to plain-text. + $comment_body = $comment->comment_body[LANGUAGE_NOT_SPECIFIED][0]; + if (isset($comment_body['format'])) { + $comment_text = check_markup($comment_body['value'], $comment_body['format']); + } + else { + $comment_text = check_plain($comment_body['value']); + } + $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE); + // Edge cases where the comment body is populated only by HTML tags will + // require a default subject. + if ($comment->subject == '') { + $comment->subject = t('(No subject)'); + } + } + + return $comment; + } + + /** + * Form submission handler for the 'preview' action. + * + * @param $form + * An associative array containing the structure of the form. + * @param $form_state + * A reference to a keyed array containing the current state of the form. + */ + public function preview(array $form, array &$form_state) { + $comment = $this->getEntity($form_state); + drupal_set_title(t('Preview comment'), PASS_THROUGH); + $form_state['comment_preview'] = comment_preview($comment); + $form_state['rebuild'] = TRUE; + } + + /** + * Overrides Drupal\entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $node = node_load($form_state['values']['nid']); + $comment = $this->getEntity($form_state); + + if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) { + // Save the anonymous user information to a cookie for reuse. + if (user_is_anonymous()) { + user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage')))); + } + + comment_save($comment); + $form_state['values']['cid'] = $comment->cid; + + // Add an entry to the watchdog log. + watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid))); + + // Explain the approval queue if necessary. + if ($comment->status == COMMENT_NOT_PUBLISHED) { + if (!user_access('administer comments')) { + drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.')); + } + } + else { + drupal_set_message(t('Your comment has been posted.')); + } + $query = array(); + // Find the current display page for this comment. + $page = comment_get_display_page($comment->cid, $node->type); + if ($page > 0) { + $query['page'] = $page; + } + // Redirect to the newly posted comment. + $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid)); + } + else { + watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING); + drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error'); + // Redirect the user to the node they are commenting on. + $redirect = 'node/' . $node->nid; + } + $form_state['redirect'] = $redirect; + // Clear the block and page caches so that anonymous users see the comment + // they have posted. + cache_invalidate(array('content' => TRUE)); + } +} diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module index 245c125..8619058 100644 --- a/core/modules/contact/contact.module +++ b/core/modules/contact/contact.module @@ -5,6 +5,8 @@ * Enables the use of personal and site-wide contact forms. */ +use Drupal\entity\EntityFormController; + /** * Implements hook_help(). */ @@ -221,7 +223,7 @@ function contact_form_user_profile_form_alter(&$form, &$form_state) { '#weight' => 5, '#collapsible' => TRUE, ); - $account = $form['#user']; + $account = $form_state['controller']->getEntity($form_state); $form['contact']['contact'] = array( '#type' => 'checkbox', '#title' => t('Personal contact form'), diff --git a/core/modules/entity/entity.api.php b/core/modules/entity/entity.api.php index 5359376..98e32b8 100644 --- a/core/modules/entity/entity.api.php +++ b/core/modules/entity/entity.api.php @@ -27,6 +27,13 @@ * The class has to implement the * Drupal\entity\EntityStorageControllerInterface interface. Leave blank * to use the Drupal\entity\DatabaseStorageController implementation. + * - form controller class: An associative array where the keys are the names + * of the different form operations (such as creation, editing or deletion) + * and the values are the names of the controller classes. To facilitate + * supporting the case where an entity form varies only slightly between + * different operations, the name of the operation is passed also to the + * constructor of the form controller class. This way, one class can be used + * for multiple entity forms. * - base table: (used by Drupal\entity\DatabaseStorageController) The * name of the entity type's base table. * - static cache: (used by Drupal\entity\DatabaseStorageController) @@ -139,6 +146,9 @@ function hook_entity_info() { 'label' => t('Node'), 'entity class' => 'Drupal\node\Node', 'controller class' => 'Drupal\node\NodeStorageController', + 'form controller class' => array( + 'default' => 'Drupal\node\NodeFormController', + ), 'base table' => 'node', 'revision table' => 'node_revision', 'uri callback' => 'node_uri', diff --git a/core/modules/entity/entity.module b/core/modules/entity/entity.module index dec1913..93e87f9 100644 --- a/core/modules/entity/entity.module +++ b/core/modules/entity/entity.module @@ -73,6 +73,9 @@ function entity_get_info($entity_type = NULL) { 'fieldable' => FALSE, 'entity class' => 'Drupal\entity\Entity', 'controller class' => 'Drupal\entity\DatabaseStorageController', + 'form controller class' => array( + 'default' => 'Drupal\entity\EntityFormController', + ), 'static cache' => TRUE, 'field cache' => TRUE, 'bundles' => array(), @@ -457,16 +460,123 @@ function entity_page_label(EntityInterface $entity, $langcode = NULL) { } /** - * Attaches field API validation to entity forms. + * Returns an entity form controller for the given operation. + * + * Since there might be different scenarios in which an entity is edited, + * multiple form controllers suitable to the different operations may be defined. + * If no controller is found for the default operation, the base class will be + * used. If a non-existing non-default operation is specified an exception will + * be thrown. + * + * @see hook_entity_info() + * + * @param $entity_type + * The type of the entity. + * @param $operation + * (optional) The name of an operation, such as creation, editing or deletion, + * identifying the controlled form. Defaults to 'default' which is the usual + * create/edit form. + * + * @return Drupal\entity\EntityFormControllerInterface + * An entity form controller instance. + */ +function entity_form_controller($entity_type, $operation = 'default') { + $info = entity_get_info($entity_type); + + // Check whether there is a form controller class for the specified operation. + if (!empty($info['form controller class'][$operation])) { + $class = $info['form controller class'][$operation]; + } + // If no controller is specified default to the base implementation. + elseif (empty($info['form controller class']) && $operation == 'default') { + $class = 'Drupal\entity\EntityFormController'; + } + // If a non-existing operation has been specified stop. + else { + throw new EntityMalformedException("Missing form controller for '$entity_type', operation '$operation'"); + } + + return new $class($operation); +} + +/** + * Returns the form id for the given entity and operation. + * + * @param EntityInterface $entity + * The entity to be created or edited. + * @param $operation + * (optional) The operation for the form to be processed. + * + * @return + * A string representing the entity form id. + */ +function entity_form_id(EntityInterface $entity, $operation = 'default') { + $entity_type = $entity->entityType(); + $bundle = $entity->bundle(); + $form_id = $entity_type; + if ($bundle != $entity_type) { + $form_id = $bundle . '_' . $form_id; + } + if ($operation != 'default') { + $form_id = $form_id . '_' . $operation; + } + return $form_id . '_form'; +} + +/** + * Returns the default form state for the given entity and operation. + * + * @param EntityInterface $entity + * The entity to be created or edited. + * @param $operation + * (optional) The operation identifying the form to be processed. + * + * @return + * A $form_state array already filled the entity form controller. + */ +function entity_form_state_defaults(EntityInterface $entity, $operation = 'default') { + $form_state = array(); + $controller = entity_form_controller($entity->entityType(), $operation); + $form_state['build_info']['callback'] = array($controller, 'build'); + $form_state['build_info']['base_form_id'] = $entity->entityType() . '_form'; + $form_state['build_info']['args'] = array($entity); + return $form_state; +} + +/** + * Retrieves, populates, and processes an entity form. + * + * @param EntityInterface $entity + * The entity to be created or edited. + * @param $operation + * (optional) The operation identifying the form to be submitted. + * @param $form_state + * (optional) A keyed array containing the current state of the form. + * + * @return + * A $form_state array already filled with the entity form controller. + */ +function entity_form_submit(EntityInterface $entity, $operation = 'default', &$form_state = array()) { + $form_state += entity_form_state_defaults($entity, $operation); + $form_id = entity_form_id($entity, $operation); + drupal_form_submit($form_id, $form_state); +} + +/** + * Returns the built and processed entity form for the given entity. + * + * @param EntityInterface $entity + * The entity to be created or edited. + * @param $operation + * (optional) The operation identifying the form variation to be returned. + * + * @return + * The processed form for the given entity and operation. */ -function entity_form_field_validate($entity_type, $form, &$form_state) { - // All field attach API functions act on an entity object, but during form - // validation, we don't have one. $form_state contains the entity as it was - // prior to processing the current form submission, and we must not update it - // until we have fully validated the submitted input. Therefore, for - // validation, act on a pseudo entity created out of the form values. - $pseudo_entity = (object) $form_state['values']; - field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state); +function entity_get_form(EntityInterface $entity, $operation = 'default') { + $form_state = entity_form_state_defaults($entity, $operation); + $form_id = entity_form_id($entity, $operation); + return drupal_build_form($form_id, $form_state); } /** diff --git a/core/modules/entity/lib/Drupal/entity/Entity.php b/core/modules/entity/lib/Drupal/entity/Entity.php index eeaeee8..e2afd2a 100644 --- a/core/modules/entity/lib/Drupal/entity/Entity.php +++ b/core/modules/entity/lib/Drupal/entity/Entity.php @@ -269,5 +269,4 @@ class Entity implements EntityInterface { public function isCurrentRevision() { return $this->isCurrentRevision; } - } diff --git a/core/modules/entity/lib/Drupal/entity/EntityFormController.php b/core/modules/entity/lib/Drupal/entity/EntityFormController.php new file mode 100644 index 0000000..db0050b --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/EntityFormController.php @@ -0,0 +1,256 @@ +operation = $operation; + } + + /** + * Implements Drupal\entity\EntityFormControllerInterface::build(). + */ + public function build(array $form, array &$form_state, EntityInterface $entity) { + + // During the initial form build, add the entity to the form state for use + // during form building and processing. During a rebuild, use what is in the + // form state. + if (!$this->getEntity($form_state)) { + $this->init($form_state, $entity); + } + + // Retrieve the form array using the possibly updated entity in form state. + $entity = $this->getEntity($form_state); + $form = $this->form($form, $form_state, $entity); + + // Retrieve and add the form actions array. + $actions = $this->actionsElement($form, $form_state); + if (!empty($actions)) { + $form['actions'] = $actions; + } + + return $form; + } + + /** + * Initialise the form state and the entity before the first form build. + */ + protected function init(array &$form_state, EntityInterface $entity) { + // Add the controller to the form state so it can be easily accessed by + // module-provided form handlers there. + $form_state['controller'] = $this; + $this->setEntity($entity, $form_state); + $this->prepareEntity($entity); + } + + /** + * Returns the actual form array to be built. + * + * @see Drupal\entity\EntityFormController::build() + */ + public function form(array $form, array &$form_state, EntityInterface $entity) { + // @todo Exploit the Property API to generate the default widgets for the + // entity properties. + $info = $entity->entityInfo(); + if (!empty($info['fieldable'])) { + field_attach_form($entity->entityType(), $entity, $form, $form_state, $this->getFormLangcode($form_state)); + } + return $form; + } + + /** + * Returns the action form element for the current entity form. + */ + protected function actionsElement(array $form, array &$form_state) { + $element = $this->actions($form, $form_state); + + // We cannot delete an entity that has not been created yet. + if ($this->getEntity($form_state)->isNew()) { + unset($element['delete']); + } + elseif (isset($element['delete'])) { + // Move the delete action as last one, unless weights are explicitly + // provided. + $delete = $element['delete']; + unset($element['delete']); + $element['delete'] = $delete; + } + + $count = 0; + foreach (element_children($element) as $action) { + $element[$action] += array( + '#type' => 'submit', + '#weight' => ++$count * 5, + ); + } + + if (!empty($element)) { + $element['#type'] = 'actions'; + } + + return $element; + } + + /** + * Returns an array of supported actions for the current entity form. + */ + protected function actions(array $form, array &$form_state) { + return array( + // @todo Rename the action key from submit to save. + 'submit' => array( + '#value' => t('Save'), + '#validate' => array( + array($this, 'validate'), + ), + '#submit' => array( + array($this, 'submit'), + array($this, 'save'), + ), + ), + 'delete' => array( + '#value' => t('Delete'), + // No need to validate the form when deleting the entity. + '#submit' => array( + array($this, 'delete'), + ), + ), + // @todo Consider introducing a 'preview' action here, since it is used by + // many entity types. + ); + } + + /** + * Implements Drupal\entity\EntityFormControllerInterface::validate(). + */ + public function validate(array $form, array &$form_state) { + // @todo Exploit the Property API to validate the values submitted for the + // entity properties. + $entity = $this->buildEntity($form, $form_state); + $info = $entity->entityInfo(); + + if (!empty($info['fieldable'])) { + field_attach_form_validate($entity->entityType(), $entity, $form, $form_state); + } + + // @todo Remove this. + // Execute legacy global validation handlers. + unset($form_state['validate_handlers']); + form_execute_handlers('validate', $form, $form_state); + } + + /** + * Implements Drupal\entity\EntityFormControllerInterface::submit(). + * + * This is the default entity object builder function. It is called before any + * other submit handler to build the new entity object to be passed to the + * following submit handlers. At this point of the form workflow the entity is + * validated and the form state can be updated, this way the subsequently + * invoked handlers can retrieve a regular entity object to act on. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * A reference to a keyed array containing the current state of the form. + */ + public function submit(array $form, array &$form_state) { + $entity = $this->buildEntity($form, $form_state); + $this->setEntity($entity, $form_state); + return $entity; + } + + /** + * Form submission handler for the 'save' action. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * A reference to a keyed array containing the current state of the form. + */ + public function save(array $form, array &$form_state) { + // @todo Perform common save operations. + } + + /** + * Form submission handler for the 'delete' action. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * A reference to a keyed array containing the current state of the form. + */ + public function delete(array $form, array &$form_state) { + // @todo Perform common delete operations. + } + + /** + * Implements Drupal\entity\EntityFormControllerInterface::getFormLangcode(). + */ + public function getFormLangcode($form_state) { + // @todo Introduce a new form language type (see hook_language_types_info()) + // to be used as the default active form language, should it be missing, so + // so that entity forms can be used to submit multilingual values. + $language = $this->getEntity($form_state)->language(); + return !empty($language->langcode) ? $language->langcode : NULL; + } + + /** + * Implements Drupal\entity\EntityFormControllerInterface::buildEntity(). + */ + public function buildEntity(array $form, array &$form_state) { + $entity = clone $this->getEntity($form_state); + // @todo Move entity_form_submit_build_entity() here. + // @todo Exploit the Property API to process the submitted entity property. + entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state); + return $entity; + } + + /** + * Implements Drupal\entity\EntityFormControllerInterface::getEntity(). + */ + public function getEntity(array $form_state) { + return isset($form_state['entity']) ? $form_state['entity'] : NULL; + } + + /** + * Implements Drupal\entity\EntityFormControllerInterface::setEntity(). + */ + public function setEntity(EntityInterface $entity, array &$form_state) { + $form_state['entity'] = $entity; + } + + /** + * Prepares the entity object before the form is built first. + */ + protected function prepareEntity(EntityInterface $entity) { + // @todo Perform common prepare operations and add a hook. + } + + /** + * Implements Drupal\entity\EntityFormControllerInterface::getOperation(). + */ + public function getOperation() { + return $this->operation; + } +} diff --git a/core/modules/entity/lib/Drupal/entity/EntityFormControllerInterface.php b/core/modules/entity/lib/Drupal/entity/EntityFormControllerInterface.php new file mode 100644 index 0000000..6765d85 --- /dev/null +++ b/core/modules/entity/lib/Drupal/entity/EntityFormControllerInterface.php @@ -0,0 +1,134 @@ +get('vocabulary'); if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) { $form['help_forum_vocab'] = array( @@ -610,9 +610,9 @@ function forum_form_taxonomy_form_vocabulary_alter(&$form, &$form_state, $form_i } /** - * Implements hook_form_FORM_ID_alter() for taxonomy_form_term(). + * Implements hook_form_FORM_ID_alter() for taxonomy_term_form(). */ -function forum_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) { +function forum_form_taxonomy_term_form_alter(&$form, &$form_state, $form_id) { $vid = config('forum.settings')->get('vocabulary'); if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) { // Hide multiple parents select from forum terms. diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module index 7984fe0..b612b60 100644 --- a/core/modules/menu/menu.module +++ b/core/modules/menu/menu.module @@ -11,7 +11,10 @@ * URLs to be added to the main site navigation menu. */ +use Drupal\entity\EntityFormController; + use Drupal\node\Node; + use Symfony\Component\HttpFoundation\JsonResponse; /** @@ -629,8 +632,9 @@ function _menu_parent_depth_limit($item) { function menu_form_node_form_alter(&$form, $form_state) { // Generate a list of possible parents (not including this link or descendants). // @todo This must be handled in a #process handler. - $link = $form['#node']->menu; - $type = $form['#node']->type; + $node = $form_state['controller']->getEntity($form_state); + $link = $node->menu; + $type = $node->type; // menu_parent_options() is goofy and can actually handle either a menu link // or a node type both as second argument. Pick based on whether there is // a link already (menu_node_prepare() sets mlid default to 0). diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php new file mode 100644 index 0000000..ccf8daa --- /dev/null +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -0,0 +1,433 @@ +type, array('status', 'promote')); + // If this is a new node, fill in the default values. + if (!isset($node->nid) || isset($node->is_new)) { + foreach (array('status', 'promote', 'sticky') as $key) { + // Multistep node forms might have filled in something already. + if (!isset($node->$key)) { + $node->$key = (int) in_array($key, $node_options); + } + } + global $user; + $node->uid = $user->uid; + $node->created = REQUEST_TIME; + } + else { + $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O'); + // Remove the log message from the original node entity. + $node->log = NULL; + } + // Always use the default revision setting. + $node->revision = in_array('revision', $node_options); + + node_invoke($node, 'prepare'); + module_invoke_all('node_prepare', $node); + } + + /** + * Overrides Drupal\entity\EntityFormController::form(). + */ + public function form(array $form, array &$form_state, EntityInterface $node) { + // Some special stuff when previewing a node. + if (isset($form_state['node_preview'])) { + $form['#prefix'] = $form_state['node_preview']; + $node->in_preview = TRUE; + } + else { + unset($node->in_preview); + } + + // Override the default CSS class name, since the user-defined node type + // name in 'TYPE-node-form' potentially clashes with third-party class + // names. + $form['#attributes']['class'][0] = drupal_html_class('node-' . $node->type . '-form'); + + // Basic node information. + // These elements are just values so they are not even sent to the client. + foreach (array('nid', 'vid', 'uid', 'created', 'type') as $key) { + $form[$key] = array( + '#type' => 'value', + '#value' => isset($node->$key) ? $node->$key : NULL, + ); + } + + // Changed must be sent to the client, for later overwrite error checking. + $form['changed'] = array( + '#type' => 'hidden', + '#default_value' => isset($node->changed) ? $node->changed : NULL, + ); + + // Invoke hook_form() to get the node-specific bits. Can't use node_invoke() + // because hook_form() needs to be able to receive $form_state by reference. + // @todo hook_form() implementations are unable to add #validate or #submit + // handlers to the form buttons below. Remove hook_form() entirely. + $function = node_type_get_base($node) . '_form'; + if (function_exists($function) && ($extra = $function($node, $form_state))) { + $form = array_merge_recursive($form, $extra); + } + // If the node type has a title, and the node type form defined no special + // weight for it, we default to a weight of -5 for consistency. + if (isset($form['title']) && !isset($form['title']['#weight'])) { + $form['title']['#weight'] = -5; + } + + if (module_exists('language')) { + $languages = language_list(LANGUAGE_ALL); + $language_options = array(); + foreach ($languages as $langcode => $language) { + // Make locked languages appear special in the list. + $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name; + } + + $form['langcode'] = array( + '#type' => 'select', + '#title' => t('Language'), + '#default_value' => $node->langcode, + '#options' => $language_options, + '#access' => !variable_get('node_type_language_hidden_' . $node->type, TRUE), + ); + } + else { + $form['langcode'] = array( + '#type' => 'value', + '#value' => $node->langcode, + ); + } + + $form['additional_settings'] = array( + '#type' => 'vertical_tabs', + '#weight' => 99, + ); + + // Add a log field if the "Create new revision" option is checked, or if the + // current user has the ability to check that option. + $form['revision_information'] = array( + '#type' => 'fieldset', + '#title' => t('Revision information'), + '#collapsible' => TRUE, + // Collapsed by default when "Create new revision" is unchecked + '#collapsed' => !$node->revision, + '#group' => 'additional_settings', + '#attributes' => array( + 'class' => array('node-form-revision-information'), + ), + '#attached' => array( + 'js' => array(drupal_get_path('module', 'node') . '/node.js'), + ), + '#weight' => 20, + '#access' => $node->revision || user_access('administer nodes'), + ); + + $form['revision_information']['revision'] = array( + '#type' => 'checkbox', + '#title' => t('Create new revision'), + '#default_value' => $node->revision, + '#access' => user_access('administer nodes'), + ); + + // Check the revision log checkbox when the log textarea is filled in. + // This must not happen if "Create new revision" is enabled by default, + // since the state would auto-disable the checkbox otherwise. + if (!$node->revision) { + $form['revision_information']['revision']['#states'] = array( + 'checked' => array( + 'textarea[name="log"]' => array('empty' => FALSE), + ), + ); + } + + $form['revision_information']['log'] = array( + '#type' => 'textarea', + '#title' => t('Revision log message'), + '#rows' => 4, + '#default_value' => !empty($node->log) ? $node->log : '', + '#description' => t('Briefly describe the changes you have made.'), + ); + + // Node author information for administrators. + $form['author'] = array( + '#type' => 'fieldset', + '#access' => user_access('administer nodes'), + '#title' => t('Authoring information'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#group' => 'additional_settings', + '#attributes' => array( + 'class' => array('node-form-author'), + ), + '#attached' => array( + 'js' => array( + drupal_get_path('module', 'node') . '/node.js', + array( + 'type' => 'setting', + 'data' => array('anonymous' => variable_get('anonymous', t('Anonymous'))), + ), + ), + ), + '#weight' => 90, + ); + + $form['author']['name'] = array( + '#type' => 'textfield', + '#title' => t('Authored by'), + '#maxlength' => 60, + '#autocomplete_path' => 'user/autocomplete', + '#default_value' => !empty($node->name) ? $node->name : '', + '#weight' => -1, + '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))), + ); + + $form['author']['date'] = array( + '#type' => 'textfield', + '#title' => t('Authored on'), + '#maxlength' => 25, + '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => !empty($node->date) ? date_format(date_create($node->date), 'Y-m-d H:i:s O') : format_date($node->created, 'custom', 'Y-m-d H:i:s O'), '%timezone' => !empty($node->date) ? date_format(date_create($node->date), 'O') : format_date($node->created, 'custom', 'O'))), + '#default_value' => !empty($node->date) ? $node->date : '', + ); + + // Node options for administrators. + $form['options'] = array( + '#type' => 'fieldset', + '#access' => user_access('administer nodes'), + '#title' => t('Publishing options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#group' => 'additional_settings', + '#attributes' => array( + 'class' => array('node-form-options'), + ), + '#attached' => array( + 'js' => array(drupal_get_path('module', 'node') . '/node.js'), + ), + '#weight' => 95, + ); + + $form['options']['status'] = array( + '#type' => 'checkbox', + '#title' => t('Published'), + '#default_value' => $node->status, + ); + + $form['options']['promote'] = array( + '#type' => 'checkbox', + '#title' => t('Promoted to front page'), + '#default_value' => $node->promote, + ); + + $form['options']['sticky'] = array( + '#type' => 'checkbox', + '#title' => t('Sticky at top of lists'), + '#default_value' => $node->sticky, + ); + + // This form uses a button-level #submit handler for the form's main submit + // action. node_form_submit() manually invokes all form-level #submit + // handlers of the form. Without explicitly setting #submit, Form API would + // auto-detect node_form_submit() as submit handler, but that is the + // button-level #submit handler for the 'Save' action. + $form += array('#submit' => array()); + + return parent::form($form, $form_state, $node); + } + + /** + * Overrides Drupal\entity\EntityFormController::actions(). + */ + protected function actions(array $form, array &$form_state) { + $element = parent::actions($form, $form_state); + $node = $this->getEntity($form_state); + $preview_mode = variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL); + + $element['preview'] = array( + '#access' => $preview_mode != DRUPAL_DISABLED, + '#value' => t('Preview'), + '#validate' => array( + array($this, 'validate'), + ), + '#submit' => array( + array($this, 'submit'), + array($this, 'preview'), + ), + ); + + $element['submit']['#access'] = $preview_mode != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview'])); + $element['delete']['#access'] = node_access('delete', $node); + + return $element; + } + + /** + * Overrides Drupal\entity\EntityFormController::validate(). + */ + public function validate(array $form, array &$form_state) { + $node = $this->buildEntity($form, $form_state); + + if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) { + form_set_error('changed', t('The content on this page has either been modified by another user, or you have already submitted modifications using this form. As a result, your changes cannot be saved.')); + } + + // Validate the "authored by" field. + if (!empty($node->name) && !($account = user_load_by_name($node->name))) { + // The use of empty() is mandatory in the context of usernames + // as the empty string denotes the anonymous user. In case we + // are dealing with an anonymous user we set the user ID to 0. + form_set_error('name', t('The username %name does not exist.', array('%name' => $node->name))); + } + + // Validate the "authored on" field. + if (!empty($node->date) && strtotime($node->date) === FALSE) { + form_set_error('date', t('You have to specify a valid date.')); + } + + // Invoke hook_validate() for node type specific validation and + // hook_node_validate() for miscellaneous validation needed by modules. + // Can't use node_invoke() or module_invoke_all(), because $form_state must + // be receivable by reference. + $function = node_type_get_base($node) . '_validate'; + if (function_exists($function)) { + $function($node, $form, $form_state); + } + foreach (module_implements('node_validate') as $module) { + $function = $module . '_node_validate'; + $function($node, $form, $form_state); + } + + parent::validate($form, $form_state); + } + + /** + * Updates the node object by processing the submitted values. + * + * This function can be called by a "Next" button of a wizard to update the + * form state's entity with the current step's values before proceeding to the + * next step. + * + * Overrides Drupal\entity\EntityFormController::submit(). + */ + public function submit(array $form, array &$form_state) { + $this->submitNodeLanguage($form, $form_state); + + // Build the node object from the submitted values. + $node = parent::submit($form, $form_state); + + node_submit($node); + foreach (module_implements('node_submit') as $module) { + $function = $module . '_node_submit'; + $function($node, $form, $form_state); + } + + return $node; + } + + /** + * Handle possible node language changes. + */ + protected function submitNodeLanguage(array $form, array &$form_state) { + if (field_has_translation_handler('node', 'node')) { + $bundle = $form_state['values']['type']; + $node_language = $form_state['values']['langcode']; + + foreach (field_info_instances('node', $bundle) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + $previous_langcode = $form[$field_name]['#language']; + + // Handle a possible language change: new language values are inserted, + // previous ones are deleted. + if ($field['translatable'] && $previous_langcode != $node_language) { + $form_state['values'][$field_name][$node_language] = $form_state['values'][$field_name][$previous_langcode]; + $form_state['values'][$field_name][$previous_langcode] = array(); + } + } + } + } + + /** + * Form submission handler for the 'preview' action. + * + * @param $form + * An associative array containing the structure of the form. + * @param $form_state + * A reference to a keyed array containing the current state of the form. + */ + public function preview(array $form, array &$form_state) { + drupal_set_title(t('Preview'), PASS_THROUGH); + $form_state['node_preview'] = node_preview($this->getEntity($form_state)); + $form_state['rebuild'] = TRUE; + } + + /** + * Overrides Drupal\entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $node = $this->getEntity($form_state); + $insert = empty($node->nid); + $node->save(); + $node_link = l(t('view'), 'node/' . $node->nid); + $watchdog_args = array('@type' => $node->type, '%title' => $node->label()); + $t_args = array('@type' => node_type_get_name($node), '%title' => $node->label()); + + if ($insert) { + watchdog('content', '@type: added %title.', $watchdog_args, WATCHDOG_NOTICE, $node_link); + drupal_set_message(t('@type %title has been created.', $t_args)); + } + else { + watchdog('content', '@type: updated %title.', $watchdog_args, WATCHDOG_NOTICE, $node_link); + drupal_set_message(t('@type %title has been updated.', $t_args)); + } + + if ($node->nid) { + $form_state['values']['nid'] = $node->nid; + $form_state['nid'] = $node->nid; + $form_state['redirect'] = 'node/' . $node->nid; + } + else { + // In the unlikely case something went wrong on save, the node will be + // rebuilt and node form redisplayed the same way as in preview. + drupal_set_message(t('The post could not be saved.'), 'error'); + $form_state['rebuild'] = TRUE; + } + + // Clear the page and block caches. + cache_invalidate(array('content' => TRUE)); + } + + /** + * Overrides Drupal\entity\EntityFormController::delete(). + */ + public function delete(array $form, array &$form_state) { + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + unset($_GET['destination']); + } + $node = $this->getEntity($form_state); + $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination)); + } +} diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index f4fb5f5..e5a8518 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -636,8 +636,8 @@ function hook_node_access($node, $op, $account, $langcode) { /** * Act on a node object about to be shown on the add/edit form. * - * This hook is invoked from node_object_prepare() after the type-specific - * hook_prepare() is invoked. + * This hook is invoked from NodeFormController::prepareEntity() after the + * type-specific hook_prepare() is invoked. * * @param Drupal\node\Node $node * The node that is about to be shown on the add/edit form. @@ -740,10 +740,10 @@ function hook_node_update_index(Drupal\node\Node $node) { /** * Perform node validation before a node is created or updated. * - * This hook is invoked from node_validate(), after a user has has finished - * editing the node and is previewing or submitting it. It is invoked at the - * end of all the standard validation steps, and after the type-specific - * hook_validate() is invoked. + * This hook is invoked from NodeFormController::validate(), after a user has + * has finished editing the node and is previewing or submitting it. It is + * invoked at the end of all the standard validation steps, and after the + * type-specific hook_validate() is invoked. * * To indicate a validation error, use form_set_error(). * @@ -1052,8 +1052,8 @@ function hook_delete(Drupal\node\Node $node) { * This hook is invoked only on the module that defines the node's content type * (use hook_node_prepare() to act on all node preparations). * - * This hook is invoked from node_object_prepare() before the general - * hook_node_prepare() is invoked. + * This hook is invoked from NodeFormController::prepareEntity() before the + * general hook_node_prepare() is invoked. * * @param Drupal\node\Node $node * The node that is about to be shown on the add/edit form. @@ -1219,10 +1219,10 @@ function hook_update(Drupal\node\Node $node) { * This hook is invoked only on the module that defines the node's content type * (use hook_node_validate() to act on all node validations). * - * This hook is invoked from node_validate(), after a user has finished - * editing the node and is previewing or submitting it. It is invoked at the end - * of all the standard validation steps, and before hook_node_validate() is - * invoked. + * This hook is invoked from NodeFormController::validate(), after a user has + * finished editing the node and is previewing or submitting it. It is invoked + * at the end of all the standard validation steps, and before + * hook_node_validate() is invoked. * * To indicate a validation error, use form_set_error(). * diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 2c94427..eb790ae 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -193,8 +193,11 @@ function node_entity_info() { $return = array( 'node' => array( 'label' => t('Node'), - 'controller class' => 'Drupal\node\NodeStorageController', 'entity class' => 'Drupal\node\Node', + 'controller class' => 'Drupal\node\NodeStorageController', + 'form controller class' => array( + 'default' => 'Drupal\node\NodeFormController', + ), 'base table' => 'node', 'revision table' => 'node_revision', 'uri callback' => 'node_uri', @@ -1026,81 +1029,6 @@ function node_load($nid = NULL, $vid = NULL, $reset = FALSE) { } /** - * Prepares a node entity for editing. - * - * Fills in a few default values, and then invokes hook_prepare() on the node - * type module, and hook_node_prepare() on all modules. - * - * @param Drupal\node\Node $node - * The node entity. - */ -function node_object_prepare(Node $node) { - // Set up default values, if required. - $node_options = variable_get('node_options_' . $node->type, array('status', 'promote')); - // If this is a new node, fill in the default values. - if (!isset($node->nid) || isset($node->is_new)) { - foreach (array('status', 'promote', 'sticky') as $key) { - // Multistep node forms might have filled in something already. - if (!isset($node->$key)) { - $node->$key = (int) in_array($key, $node_options); - } - } - global $user; - $node->uid = $user->uid; - $node->created = REQUEST_TIME; - } - else { - $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O'); - // Remove the log message from the original node entity. - $node->log = NULL; - } - // Always use the default revision setting. - $node->revision = in_array('revision', $node_options); - - node_invoke($node, 'prepare'); - module_invoke_all('node_prepare', $node); -} - -/** - * Performs validation checks on the given node. - * - * @see node_form_validate() - */ -function node_validate(Node $node, $form, &$form_state) { - $type = node_type_load($node->type); - - if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) { - form_set_error('changed', t('The content on this page has either been modified by another user, or you have already submitted modifications using this form. As a result, your changes cannot be saved.')); - } - - // Validate the "authored by" field. - if (!empty($node->name) && !($account = user_load_by_name($node->name))) { - // The use of empty() is mandatory in the context of usernames - // as the empty string denotes the anonymous user. In case we - // are dealing with an anonymous user we set the user ID to 0. - form_set_error('name', t('The username %name does not exist.', array('%name' => $node->name))); - } - - // Validate the "authored on" field. - if (!empty($node->date) && strtotime($node->date) === FALSE) { - form_set_error('date', t('You have to specify a valid date.')); - } - - // Invoke hook_validate() for node type specific validation and - // hook_node_validate() for miscellaneous validation needed by modules. Can't - // use node_invoke() or module_invoke_all(), because $form_state must be - // receivable by reference. - $function = node_type_get_base($node) . '_validate'; - if (function_exists($function)) { - $function($node, $form, $form_state); - } - foreach (module_implements('node_validate') as $module) { - $function = $module . '_node_validate'; - $function($node, $form, $form_state); - } -} - -/** * Prepares a node for saving by populating the author and creation date. */ function node_submit($node) { @@ -3653,21 +3581,6 @@ function node_content_form(Node $node, $form_state) { */ /** - * Implements hook_forms(). - * - * All node forms share the same form handler. - */ -function node_forms() { - $forms = array(); - if ($types = node_type_get_types()) { - foreach (array_keys($types) as $type) { - $forms[$type . '_node_form']['callback'] = 'node_form'; - } - } - return $forms; -} - -/** * Implements hook_action_info(). */ function node_action_info() { diff --git a/core/modules/node/node.pages.inc b/core/modules/node/node.pages.inc index 76fd8b2..19b0cb7 100644 --- a/core/modules/node/node.pages.inc +++ b/core/modules/node/node.pages.inc @@ -9,6 +9,8 @@ * @see node_menu() */ +use Drupal\entity\EntityFormController; + use Drupal\node\Node; /** @@ -19,7 +21,7 @@ use Drupal\node\Node; function node_page_edit($node) { $type_name = node_type_get_name($node); drupal_set_title(t('Edit @type @title', array('@type' => $type_name, '@title' => $node->label())), PASS_THROUGH); - return drupal_get_form($node->type . '_node_form', $node); + return entity_get_form($node); } /** @@ -96,311 +98,12 @@ function node_add($node_type) { 'langcode' => node_type_get_default_langcode($type) )); drupal_set_title(t('Create @name', array('@name' => $node_type->name)), PASS_THROUGH); - $output = drupal_get_form($type . '_node_form', $node); + $output = entity_get_form($node); return $output; } /** - * Form validation handler for node_form(). - * - * @see node_form_delete_submit() - * @see node_form_build_preview() - * @see node_form_submit() - * @see node_form_submit_build_node() - */ -function node_form_validate($form, &$form_state) { - // $form_state['node'] contains the actual entity being edited, but we must - // not update it with form values that have not yet been validated, so we - // create a pseudo-entity to use during validation. - $node = clone $form_state['node']; - foreach ($form_state['values'] as $key => $value) { - $node->{$key} = $value; - } - node_validate($node, $form, $form_state); - entity_form_field_validate('node', $form, $form_state); -} - -/** - * Form constructor for the node add/edit form. - * - * @see node_form_delete_submit() - * @see node_form_build_preview() - * @see node_form_validate() - * @see node_form_submit() - * @see node_form_submit_build_node() - * @ingroup forms - */ -function node_form($form, &$form_state, Node $node) { - global $user; - - // During initial form build, add the node entity to the form state for use - // during form building and processing. During a rebuild, use what is in the - // form state. - if (!isset($form_state['node'])) { - node_object_prepare($node); - $form_state['node'] = $node; - } - else { - $node = $form_state['node']; - } - - // Some special stuff when previewing a node. - if (isset($form_state['node_preview'])) { - $form['#prefix'] = $form_state['node_preview']; - $node->in_preview = TRUE; - } - else { - unset($node->in_preview); - } - - // Override the default CSS class name, since the user-defined node type name - // in 'TYPE-node-form' potentially clashes with third-party class names. - $form['#attributes']['class'][0] = drupal_html_class('node-' . $node->type . '-form'); - - // Basic node information. - // These elements are just values so they are not even sent to the client. - foreach (array('nid', 'vid', 'uid', 'created', 'type') as $key) { - $form[$key] = array( - '#type' => 'value', - '#value' => isset($node->$key) ? $node->$key : NULL, - ); - } - - // Changed must be sent to the client, for later overwrite error checking. - $form['changed'] = array( - '#type' => 'hidden', - '#default_value' => isset($node->changed) ? $node->changed : NULL, - ); - // Invoke hook_form() to get the node-specific bits. Can't use node_invoke(), - // because hook_form() needs to be able to receive $form_state by reference. - // @todo hook_form() implementations are unable to add #validate or #submit - // handlers to the form buttons below. Remove hook_form() entirely. - $function = node_type_get_base($node) . '_form'; - if (function_exists($function) && ($extra = $function($node, $form_state))) { - $form = array_merge_recursive($form, $extra); - } - // If the node type has a title, and the node type form defined no special - // weight for it, we default to a weight of -5 for consistency. - if (isset($form['title']) && !isset($form['title']['#weight'])) { - $form['title']['#weight'] = -5; - } - // @todo D8: Remove. Modules should access the node using $form_state['node']. - $form['#node'] = $node; - - if (module_exists('language')) { - $languages = language_list(LANGUAGE_ALL); - $language_options = array(); - foreach ($languages as $langcode => $language) { - // Make locked languages appear special in the list. - $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name; - } - $form['langcode'] = array( - '#type' => 'select', - '#title' => t('Language'), - '#default_value' => $node->langcode, - '#options' => $language_options, - '#access' => !variable_get('node_type_language_hidden_' . $node->type, TRUE), - ); - } - else { - $form['langcode'] = array( - '#type' => 'value', - '#value' => $node->langcode, - ); - } - - $form['additional_settings'] = array( - '#type' => 'vertical_tabs', - '#weight' => 99, - ); - - // Add a log field if the "Create new revision" option is checked, or if the - // current user has the ability to check that option. - $form['revision_information'] = array( - '#type' => 'fieldset', - '#title' => t('Revision information'), - '#collapsible' => TRUE, - // Collapsed by default when "Create new revision" is unchecked - '#collapsed' => !$node->revision, - '#group' => 'additional_settings', - '#attributes' => array( - 'class' => array('node-form-revision-information'), - ), - '#attached' => array( - 'js' => array(drupal_get_path('module', 'node') . '/node.js'), - ), - '#weight' => 20, - '#access' => $node->revision || user_access('administer nodes'), - ); - $form['revision_information']['revision'] = array( - '#type' => 'checkbox', - '#title' => t('Create new revision'), - '#default_value' => $node->revision, - '#access' => user_access('administer nodes'), - ); - // Check the revision log checkbox when the log textarea is filled in. - // This must not happen if "Create new revision" is enabled by default, since - // the state would auto-disable the checkbox otherwise. - if (!$node->revision) { - $form['revision_information']['revision']['#states'] = array( - 'checked' => array( - 'textarea[name="log"]' => array('empty' => FALSE), - ), - ); - } - $form['revision_information']['log'] = array( - '#type' => 'textarea', - '#title' => t('Revision log message'), - '#rows' => 4, - '#default_value' => !empty($node->log) ? $node->log : '', - '#description' => t('Briefly describe the changes you have made.'), - ); - - // Node author information for administrators - $form['author'] = array( - '#type' => 'fieldset', - '#access' => user_access('administer nodes'), - '#title' => t('Authoring information'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#group' => 'additional_settings', - '#attributes' => array( - 'class' => array('node-form-author'), - ), - '#attached' => array( - 'js' => array( - drupal_get_path('module', 'node') . '/node.js', - array( - 'type' => 'setting', - 'data' => array('anonymous' => variable_get('anonymous', t('Anonymous'))), - ), - ), - ), - '#weight' => 90, - ); - $form['author']['name'] = array( - '#type' => 'textfield', - '#title' => t('Authored by'), - '#maxlength' => 60, - '#autocomplete_path' => 'user/autocomplete', - '#default_value' => !empty($node->name) ? $node->name : '', - '#weight' => -1, - '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))), - ); - $form['author']['date'] = array( - '#type' => 'textfield', - '#title' => t('Authored on'), - '#maxlength' => 25, - '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => !empty($node->date) ? date_format(date_create($node->date), 'Y-m-d H:i:s O') : format_date($node->created, 'custom', 'Y-m-d H:i:s O'), '%timezone' => !empty($node->date) ? date_format(date_create($node->date), 'O') : format_date($node->created, 'custom', 'O'))), - '#default_value' => !empty($node->date) ? $node->date : '', - ); - - // Node options for administrators - $form['options'] = array( - '#type' => 'fieldset', - '#access' => user_access('administer nodes'), - '#title' => t('Publishing options'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#group' => 'additional_settings', - '#attributes' => array( - 'class' => array('node-form-options'), - ), - '#attached' => array( - 'js' => array(drupal_get_path('module', 'node') . '/node.js'), - ), - '#weight' => 95, - ); - $form['options']['status'] = array( - '#type' => 'checkbox', - '#title' => t('Published'), - '#default_value' => $node->status, - ); - $form['options']['promote'] = array( - '#type' => 'checkbox', - '#title' => t('Promoted to front page'), - '#default_value' => $node->promote, - ); - $form['options']['sticky'] = array( - '#type' => 'checkbox', - '#title' => t('Sticky at top of lists'), - '#default_value' => $node->sticky, - ); - - // Add the buttons. - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview'])), - '#value' => t('Save'), - '#weight' => 5, - '#submit' => array('node_form_submit'), - ); - $form['actions']['preview'] = array( - '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED, - '#type' => 'submit', - '#value' => t('Preview'), - '#weight' => 10, - '#submit' => array('node_form_build_preview'), - ); - if (!empty($node->nid) && node_access('delete', $node)) { - $form['actions']['delete'] = array( - '#type' => 'submit', - '#value' => t('Delete'), - '#weight' => 15, - '#submit' => array('node_form_delete_submit'), - ); - } - // This form uses a button-level #submit handler for the form's main submit - // action. node_form_submit() manually invokes all form-level #submit handlers - // of the form. Without explicitly setting #submit, Form API would auto-detect - // node_form_submit() as submit handler, but that is the button-level #submit - // handler for the 'Save' action. To maintain backwards compatibility, a - // #submit handler is auto-suggested for custom node type modules. - $form['#validate'][] = 'node_form_validate'; - if (!isset($form['#submit']) && function_exists($node->type . '_node_form_submit')) { - $form['#submit'][] = $node->type . '_node_form_submit'; - } - $form += array('#submit' => array()); - - field_attach_form('node', $node, $form, $form_state, $node->langcode); - return $form; -} - -/** - * Form submission handler for the 'Delete' button for node_form(). - * - * @see node_form_build_preview() - * @see node_form_validate() - * @see node_form_submit() - * @see node_form_submit_build_node() - */ -function node_form_delete_submit($form, &$form_state) { - $destination = array(); - if (isset($_GET['destination'])) { - $destination = drupal_get_destination(); - unset($_GET['destination']); - } - $node = $form['#node']; - $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination)); -} - -/** - * Form submission handler for the 'Preview' button for node_form(). - * - * @see node_form_delete_submit() - * @see node_form_validate() - * @see node_form_submit() - * @see node_form_submit_build_node() - */ -function node_form_build_preview($form, &$form_state) { - $node = node_form_submit_build_node($form, $form_state); - $form_state['node_preview'] = node_preview($node); - $form_state['rebuild'] = TRUE; -} - -/** * Generates a node preview. * * @param Drupal\node\Node $node @@ -442,7 +145,6 @@ function node_preview(Node $node) { $output = theme('node_preview', array('node' => $node)); unset($node->in_preview); } - drupal_set_title(t('Preview'), PASS_THROUGH); return $output; } @@ -455,7 +157,7 @@ function node_preview(Node $node) { * An associative array containing: * - node: The node entity which is being previewed. * - * @see node_preview() + * @see NodeFormController::preview() * @ingroup themeable */ function theme_node_preview($variables) { @@ -486,115 +188,11 @@ function theme_node_preview($variables) { } /** - * Form submission handler that saves the node for node_form(). - * - * @see node_form_delete_submit() - * @see node_form_build_preview() - * @see node_form_validate() - * @see node_form_submit_build_node() - */ -function node_form_submit($form, &$form_state) { - // Handle possible field translations first and then build the node from the - // submitted values. - node_field_language_form_submit($form, $form_state); - $node = node_form_submit_build_node($form, $form_state); - $insert = empty($node->nid); - $node->save(); - $node_link = l(t('view'), 'node/' . $node->nid); - $watchdog_args = array('@type' => $node->type, '%title' => $node->label()); - $t_args = array('@type' => node_type_get_name($node), '%title' => $node->label()); - - if ($insert) { - watchdog('content', '@type: added %title.', $watchdog_args, WATCHDOG_NOTICE, $node_link); - drupal_set_message(t('@type %title has been created.', $t_args)); - } - else { - watchdog('content', '@type: updated %title.', $watchdog_args, WATCHDOG_NOTICE, $node_link); - drupal_set_message(t('@type %title has been updated.', $t_args)); - } - if ($node->nid) { - $form_state['values']['nid'] = $node->nid; - $form_state['nid'] = $node->nid; - $form_state['redirect'] = 'node/' . $node->nid; - } - else { - // In the unlikely case something went wrong on save, the node will be - // rebuilt and node form redisplayed the same way as in preview. - drupal_set_message(t('The post could not be saved.'), 'error'); - $form_state['rebuild'] = TRUE; - } - // Clear the page and block caches. - cache_invalidate(array('content' => TRUE)); -} - -/** - * Handles possible node language changes. - * - */ -function node_field_language_form_submit($form, &$form_state) { - if (field_has_translation_handler('node', 'node')) { - $bundle = $form_state['values']['type']; - $node_language = $form_state['values']['langcode']; - - foreach (field_info_instances('node', $bundle) as $instance) { - $field_name = $instance['field_name']; - $field = field_info_field($field_name); - $previous_langcode = $form[$field_name]['#language']; - - // Handle a possible language change: New language values are inserted, - // previous ones are deleted. - if ($field['translatable'] && $previous_langcode != $node_language) { - $form_state['values'][$field_name][$node_language] = $form_state['values'][$field_name][$previous_langcode]; - $form_state['values'][$field_name][$previous_langcode] = array(); - } - } - } -} - -/** - * Updates the form state's node entity by processing this submission's values. - * - * This is the default builder function for the node form. It is called - * during the "Save" and "Preview" submit handlers to retrieve the entity to - * save or preview. This function can also be called by a "Next" button of a - * wizard to update the form state's entity with the current step's values - * before proceeding to the next step. - * - * @see node_form() - * @see node_form_delete_submit() - * @see node_form_build_preview() - * @see node_form_validate() - * @see node_form_submit() - */ -function node_form_submit_build_node($form, &$form_state) { - // @todo Legacy support for modules that extend the node form with form-level - // submit handlers that adjust $form_state['values'] prior to those values - // being used to update the entity. Module authors are encouraged to instead - // adjust the node directly within a hook_node_submit() implementation. For - // Drupal 8, evaluate whether the pattern of triggering form-level submit - // handlers during button-level submit processing is worth supporting - // properly, and if so, add a Form API function for doing so. - unset($form_state['submit_handlers']); - form_execute_handlers('submit', $form, $form_state); - - $node = $form_state['node']; - entity_form_submit_build_entity('node', $node, $form, $form_state); - - node_submit($node); - foreach (module_implements('node_submit') as $module) { - $function = $module . '_node_submit'; - $function($node, $form, $form_state); - } - return $node; -} - -/** * Page callback: Form constructor for node deletion confirmation form. * * @see node_menu() */ function node_delete_confirm($form, &$form_state, $node) { - $form['#node'] = $node; // Always provide entity id in the same form key as in the entity edit form. $form['nid'] = array('#type' => 'value', '#value' => $node->nid); return confirm_form($form, diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module index 5d557cc..af02c79 100644 --- a/core/modules/node/tests/modules/node_access_test/node_access_test.module +++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module @@ -7,8 +7,10 @@ * a special 'node test view' permission. */ -use Drupal\node\Node; use Drupal\entity\EntityFieldQuery; +use Drupal\entity\EntityFormController; + +use Drupal\node\Node; /** * Implements hook_node_grants(). @@ -179,11 +181,12 @@ function node_access_entity_test_page() { function node_access_test_form_node_form_alter(&$form, $form_state) { // Only show this checkbox for NodeAccessBaseTableTestCase. if (variable_get('node_access_test_private')) { + $node = $form_state['controller']->getEntity($form_state); $form['private'] = array( '#type' => 'checkbox', '#title' => t('Private'), '#description' => t('Check here if this content should be set private and only shown to privileged users.'), - '#default_value' => isset($form['#node']->private) ? $form['#node']->private : FALSE, + '#default_value' => isset($node->private) ? $node->private : FALSE, ); } } diff --git a/core/modules/openid/openid.module b/core/modules/openid/openid.module index 034c7c2..bad9f3a 100644 --- a/core/modules/openid/openid.module +++ b/core/modules/openid/openid.module @@ -5,6 +5,8 @@ * Implement OpenID Relying Party support for Drupal */ +use Drupal\entity\EntityFormController; + /** * Implements hook_menu(). */ @@ -231,7 +233,8 @@ function openid_form_user_register_form_alter(&$form, &$form_state) { $timezone = current($ax_timezone_values); } if (in_array($timezone, timezone_identifiers_list())) { - $form['#user']->timezone = $timezone; + $account = $form_state['controller']->getEntity($form_state); + $account->timezone = $timezone; } $language = FALSE; @@ -732,7 +735,8 @@ function openid_authentication($response) { $form_state['values'] = array(); $form_state['values']['op'] = t('Create new account'); - drupal_form_submit('user_register_form', $form_state); + $account = entity_create('user', array()); + entity_form_submit($account, 'register', $form_state); if (empty($form_state['user'])) { module_invoke_all('openid_response', $response, NULL); diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module index acfcc2c..bad2065 100644 --- a/core/modules/overlay/overlay.module +++ b/core/modules/overlay/overlay.module @@ -5,6 +5,8 @@ * Displays the Drupal administration interface in an overlay. */ +use Drupal\entity\EntityFormController; + use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -85,7 +87,7 @@ function overlay_theme() { * Implements hook_form_FORM_ID_alter(). */ function overlay_form_user_profile_form_alter(&$form, &$form_state) { - $account = $form['#user']; + $account = $form_state['controller']->getEntity($form_state); if (user_access('access overlay', $account)) { $form['overlay_control'] = array( '#type' => 'fieldset', diff --git a/core/modules/path/path.module b/core/modules/path/path.module index d01c623..e5ca124 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -5,7 +5,10 @@ * Enables users to rename URLs. */ +use Drupal\entity\EntityFormController; + use Drupal\node\Node; + use Drupal\taxonomy\Term; /** @@ -99,11 +102,12 @@ function path_menu() { * @see path_form_element_validate() */ function path_form_node_form_alter(&$form, $form_state) { + $node = $form_state['controller']->getEntity($form_state); $path = array(); - if (!empty($form['#node']->nid)) { - $conditions = array('source' => 'node/' . $form['#node']->nid); - if ($form['#node']->langcode != LANGUAGE_NOT_SPECIFIED) { - $conditions['langcode'] = $form['#node']->langcode; + if (!empty($node->nid)) { + $conditions = array('source' => 'node/' . $node->nid); + if ($node->langcode != LANGUAGE_NOT_SPECIFIED) { + $conditions['langcode'] = $node->langcode; } $path = path_load($conditions); if ($path === FALSE) { @@ -112,9 +116,9 @@ function path_form_node_form_alter(&$form, $form_state) { } $path += array( 'pid' => NULL, - 'source' => isset($form['#node']->nid) ? 'node/' . $form['#node']->nid : NULL, + 'source' => isset($node->nid) ? 'node/' . $node->nid : NULL, 'alias' => '', - 'langcode' => isset($form['#node']->langcode) ? $form['#node']->langcode : LANGUAGE_NOT_SPECIFIED, + 'langcode' => isset($node->langcode) ? $node->langcode : LANGUAGE_NOT_SPECIFIED, ); $form['path'] = array( @@ -232,18 +236,19 @@ function path_node_predelete(Node $node) { } /** - * Implements hook_form_FORM_ID_alter() for taxonomy_form_term(). + * Implements hook_form_FORM_ID_alter() for taxonomy_term_form(). */ -function path_form_taxonomy_form_term_alter(&$form, $form_state) { +function path_form_taxonomy_term_form_alter(&$form, $form_state) { // Make sure this does not show up on the delete confirmation form. if (empty($form_state['confirm_delete'])) { - $path = (isset($form['#term']['tid']) ? path_load('taxonomy/term/' . $form['#term']['tid']) : array()); + $term = $form_state['controller']->getEntity($form_state); + $path = (isset($term->tid) ? path_load('taxonomy/term/' . $term->tid) : array()); if ($path === FALSE) { $path = array(); } $path += array( 'pid' => NULL, - 'source' => isset($form['#term']['tid']) ? 'taxonomy/term/' . $form['#term']['tid'] : NULL, + 'source' => isset($term->tid) ? 'taxonomy/term/' . $term->tid : NULL, 'alias' => '', 'langcode' => LANGUAGE_NOT_SPECIFIED, ); diff --git a/core/modules/poll/poll.module b/core/modules/poll/poll.module index 259ff14..38800e5 100644 --- a/core/modules/poll/poll.module +++ b/core/modules/poll/poll.module @@ -6,6 +6,8 @@ * choice questions. */ +use Drupal\entity\EntityFormController; + use Drupal\node\Node; /** @@ -336,6 +338,8 @@ function poll_form(Node $node, &$form_state) { drupal_get_path('module', 'poll') . '/poll.admin.css', ); + $form['#entity_builders'][] = 'poll_node_form_submit'; + return $form; } @@ -355,9 +359,10 @@ function poll_more_choices_submit($form, &$form_state) { } // Renumber the choices. This invalidates the corresponding key/value // associations in $form_state['input'], so clear that out. This requires - // poll_form() to rebuild the choices with the values in - // $form_state['node']->choice, which it does. - $form_state['node']->choice = array_values($form_state['values']['choice']); + // poll_form() to rebuild the choices with the values in $node->choice, which + // it does. + $node = $form_state['controller']->getEntity($form_state); + $node->choice = array_values($form_state['values']['choice']); unset($form_state['input']['choice']); $form_state['rebuild'] = TRUE; } @@ -419,14 +424,14 @@ function poll_choice_js($form, $form_state) { } /** - * Form submit handler for node_form(). + * Entity builder for node_form(). * * Upon preview and final submission, we need to renumber poll choices and * create a teaser output. */ -function poll_node_form_submit(&$form, &$form_state) { +function poll_node_form_submit($entity_type, $entity, &$form, &$form_state) { // Renumber choices. - $form_state['values']['choice'] = array_values($form_state['values']['choice']); + $entity->choice = array_values($form_state['values']['choice']); $form_state['values']['teaser'] = poll_teaser((object) $form_state['values']); } diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 142a366..c057156 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -1184,7 +1184,8 @@ function hook_page_alter(&$page) { * Perform alterations before a form is rendered. * * One popular use of this hook is to add form elements to the node form. When - * altering a node form, the node entity can be accessed at $form['#node']. + * altering a node form, the node entity can be retrieved by invoking + * $form_state['controller']->getEntity($form_state). * * In addition to hook_form_alter(), which is called for all forms, there are * two more specific form hooks available. The first, diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 983c317..8371a10 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -5,7 +5,10 @@ * Configuration system that lets administrators modify the workings of the site. */ +use Drupal\entity\EntityFormController; + use Drupal\Core\Utility\ModuleInfo; + use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -2254,7 +2257,7 @@ function system_user_login(&$edit, $account) { function system_user_timezone(&$form, &$form_state) { global $user; - $account = $form['#user']; + $account = $form_state['controller']->getEntity($form_state); $form['timezone'] = array( '#type' => 'fieldset', '#title' => t('Locale settings'), diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module index b89426c..44fc0eb 100644 --- a/core/modules/system/tests/modules/form_test/form_test.module +++ b/core/modules/system/tests/modules/form_test/form_test.module @@ -2132,8 +2132,8 @@ function form_test_two_instances() { 'langcode' => LANGUAGE_NOT_SPECIFIED, )); $node2 = clone($node1); - $return['node_form_1'] = drupal_get_form('page_node_form', $node1); - $return['node_form_2'] = drupal_get_form('page_node_form', $node2); + $return['node_form_1'] = entity_get_form($node1); + $return['node_form_2'] = entity_get_form($node2); return $return; } diff --git a/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module b/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module index 0ec6da6..b4c750e 100644 --- a/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module +++ b/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module @@ -5,6 +5,8 @@ * Test module for Taxonomy hooks and functions not used in core. */ +use Drupal\entity\EntityFormController; + use Drupal\taxonomy\Term; /** @@ -57,18 +59,17 @@ function taxonomy_test_taxonomy_term_delete(Term $term) { } /** - * Implements hook_form_alter(). + * Implements hook_form_FORM_ID_alter(). */ -function taxonomy_test_form_alter(&$form, $form_state, $form_id) { - if ($form_id == 'taxonomy_form_term') { - $antonym = taxonomy_test_get_antonym($form['#term']['tid']); - $form['advanced']['antonym'] = array( - '#type' => 'textfield', - '#title' => t('Antonym'), - '#default_value' => !empty($antonym) ? $antonym : '', - '#description' => t('Antonym of this term.') - ); - } +function taxonomy_test_form_taxonomy_term_form_alter(&$form, $form_state, $form_id) { + $term = $form_state['controller']->getEntity($form_state); + $antonym = taxonomy_test_get_antonym($term->tid); + $form['advanced']['antonym'] = array( + '#type' => 'textfield', + '#title' => t('Antonym'), + '#default_value' => !empty($antonym) ? $antonym : '', + '#description' => t('Antonym of this term.') + ); } /** diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php new file mode 100644 index 0000000..fa853b3 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php @@ -0,0 +1,203 @@ +vid); + + $parent = array_keys(taxonomy_term_load_parents($term->tid)); + $form_state['taxonomy']['parent'] = $parent; + $form_state['taxonomy']['vocabulary'] = $vocabulary; + + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Name'), + '#default_value' => $term->name, + '#maxlength' => 255, + '#required' => TRUE, + '#weight' => -5, + ); + + $form['description'] = array( + '#type' => 'text_format', + '#title' => t('Description'), + '#default_value' => $term->description, + '#format' => $term->format, + '#weight' => 0, + ); + + $form['vocabulary_machine_name'] = array( + '#type' => 'value', + '#value' => isset($term->vocabulary_machine_name) ? $term->vocabulary_machine_name : $vocabulary->name, + ); + + $form['relations'] = array( + '#type' => 'fieldset', + '#title' => t('Relations'), + '#collapsible' => TRUE, + '#collapsed' => ($vocabulary->hierarchy != TAXONOMY_HIERARCHY_MULTIPLE), + '#weight' => 10, + ); + + // taxonomy_get_tree and taxonomy_term_load_parents may contain large numbers of + // items so we check for taxonomy_override_selector before loading the + // full vocabulary. Contrib modules can then intercept before + // hook_form_alter to provide scalable alternatives. + if (!variable_get('taxonomy_override_selector', FALSE)) { + $parent = array_keys(taxonomy_term_load_parents($term->tid)); + $children = taxonomy_get_tree($vocabulary->vid, $term->tid); + + // A term can't be the child of itself, nor of its children. + foreach ($children as $child) { + $exclude[] = $child->tid; + } + $exclude[] = $term->tid; + + $tree = taxonomy_get_tree($vocabulary->vid); + $options = array('<' . t('root') . '>'); + if (empty($parent)) { + $parent = array(0); + } + foreach ($tree as $item) { + if (!in_array($item->tid, $exclude)) { + $options[$item->tid] = str_repeat('-', $item->depth) . $item->name; + } + } + + $form['relations']['parent'] = array( + '#type' => 'select', + '#title' => t('Parent terms'), + '#options' => $options, + '#default_value' => $parent, + '#multiple' => TRUE, + ); + } + + $form['relations']['weight'] = array( + '#type' => 'textfield', + '#title' => t('Weight'), + '#size' => 6, + '#default_value' => $term->weight, + '#description' => t('Terms are displayed in ascending order by weight.'), + '#required' => TRUE, + ); + + $form['vid'] = array( + '#type' => 'value', + '#value' => $vocabulary->vid, + ); + + $form['tid'] = array( + '#type' => 'value', + '#value' => $term->tid, + ); + + if (empty($term->tid)) { + $form_state['redirect'] = current_path(); + } + + return parent::form($form, $form_state, $term); + } + + /** + * Overrides Drupal\entity\EntityFormController::validate(). + */ + public function validate(array $form, array &$form_state) { + parent::validate($form, $form_state); + + // Ensure numeric values. + if (isset($form_state['values']['weight']) && !is_numeric($form_state['values']['weight'])) { + form_set_error('weight', t('Weight value must be numeric.')); + } + } + + /** + * Overrides Drupal\entity\EntityFormController::submit(). + */ + public function submit(array $form, array &$form_state) { + $term = parent::submit($form, $form_state); + + // Prevent leading and trailing spaces in term names. + $term->name = trim($term->name); + + // Convert text_format field into values expected by taxonomy_term_save(). + $description = $form_state['values']['description']; + $term->description = $description['value']; + $term->format = $description['format']; + + return $term; + } + + /** + * Overrides Drupal\entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $term = $this->getEntity($form_state); + + $status = taxonomy_term_save($term); + switch ($status) { + case SAVED_NEW: + drupal_set_message(t('Created new term %term.', array('%term' => $term->name))); + watchdog('taxonomy', 'Created new term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit')); + break; + case SAVED_UPDATED: + drupal_set_message(t('Updated term %term.', array('%term' => $term->name))); + watchdog('taxonomy', 'Updated term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit')); + // Clear the page and block caches to avoid stale data. + cache_invalidate(array('content' => TRUE)); + break; + } + + $current_parent_count = count($form_state['values']['parent']); + $previous_parent_count = count($form_state['taxonomy']['parent']); + // Root doesn't count if it's the only parent. + if ($current_parent_count == 1 && isset($form_state['values']['parent'][0])) { + $current_parent_count = 0; + $form_state['values']['parent'] = array(); + } + + // If the number of parents has been reduced to one or none, do a check on the + // parents of every term in the vocabulary value. + if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) { + taxonomy_check_vocabulary_hierarchy($form_state['taxonomy']['vocabulary'], $form_state['values']); + } + // If we've increased the number of parents and this is a single or flat + // hierarchy, update the vocabulary immediately. + elseif ($current_parent_count > $previous_parent_count && $form_state['taxonomy']['vocabulary']->hierarchy != TAXONOMY_HIERARCHY_MULTIPLE) { + $form_state['taxonomy']['vocabulary']->hierarchy = $current_parent_count == 1 ? TAXONOMY_HIERARCHY_SINGLE : TAXONOMY_HIERARCHY_MULTIPLE; + taxonomy_vocabulary_save($form_state['taxonomy']['vocabulary']); + } + + $form_state['values']['tid'] = $term->tid; + $form_state['tid'] = $term->tid; + } + + /** + * Overrides Drupal\entity\EntityFormController::delete(). + */ + public function delete(array $form, array &$form_state) { + $destination = array(); + if (isset($_GET['destination'])) { + $destination = drupal_get_destination(); + unset($_GET['destination']); + } + $term = $this->getEntity($form_state); + $form_state['redirect'] = array('taxonomy/term/' . $term->tid . '/delete', array('query' => $destination)); + } +} diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php index d041d16..1f34e66 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php @@ -278,7 +278,7 @@ class TermTest extends TaxonomyTestBase { 'description[value]' => $this->randomName(100), ); // Explicitly set the parents field to 'root', to ensure that - // taxonomy_form_term_submit() handles the invalid term ID correctly. + // TermFormController::save() handles the invalid term ID correctly. $edit['parent[]'] = array(0); // Create the term to edit. @@ -329,7 +329,7 @@ class TermTest extends TaxonomyTestBase { $this->drupalGet('taxonomy/term/' . $term->tid . '/feed'); // Check that the term edit page does not try to interpret additional path - // components as arguments for taxonomy_form_term(). + // components as arguments for taxonomy_term_form(). $this->drupalGet('taxonomy/term/' . $term->tid . '/edit/' . $this->randomName()); // Delete the term. diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php new file mode 100644 index 0000000..1a1f882 --- /dev/null +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php @@ -0,0 +1,139 @@ + 'textfield', + '#title' => t('Name'), + '#default_value' => $vocabulary->name, + '#maxlength' => 255, + '#required' => TRUE, + ); + $form['machine_name'] = array( + '#type' => 'machine_name', + '#default_value' => $vocabulary->machine_name, + '#maxlength' => 255, + '#machine_name' => array( + 'exists' => 'taxonomy_vocabulary_machine_name_load', + ), + ); + $form['description'] = array( + '#type' => 'textfield', + '#title' => t('Description'), + '#default_value' => $vocabulary->description, + ); + // Set the hierarchy to "multiple parents" by default. This simplifies the + // vocabulary form and standardizes the term form. + $form['hierarchy'] = array( + '#type' => 'value', + '#value' => '0', + ); + + if (isset($vocabulary->vid)) { + $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid); + } + + return parent::form($form, $form_state, $vocabulary); + } + + /** + * Returns an array of supported actions for the current entity form. + */ + protected function actions(array $form, array &$form_state) { + // If we are displaying the delete confirmation skip the regular actions. + if (empty($form_state['confirm_delete'])) { + $actions = parent::actions($form, $form_state); + array_unshift($actions['delete']['#submit'], array($this, 'submit')); + return $actions; + } + else { + return array(); + } + } + + /** + * Overrides Drupal\entity\EntityFormController::validate(). + */ + public function validate(array $form, array &$form_state) { + parent::validate($form, $form_state); + + // Make sure that the machine name of the vocabulary is not in the + // disallowed list (names that conflict with menu items, such as 'list' + // and 'add'). + // During the deletion there is no 'machine_name' key. + if (isset($form_state['values']['machine_name'])) { + // Do not allow machine names to conflict with taxonomy path arguments. + $machine_name = $form_state['values']['machine_name']; + $disallowed = array('add', 'list'); + if (in_array($machine_name, $disallowed)) { + form_set_error('machine_name', t('The machine-readable name cannot be "add" or "list".')); + } + } + } + + /** + * Overrides Drupal\entity\EntityFormController::submit(). + */ + public function submit(array $form, array &$form_state) { + // @todo We should not be calling taxonomy_vocabulary_confirm_delete() from + // within the form builder. + if ($form_state['triggering_element']['#value'] == t('Delete')) { + // Rebuild the form to confirm vocabulary deletion. + $form_state['rebuild'] = TRUE; + $form_state['confirm_delete'] = TRUE; + return NULL; + } + else { + return parent::submit($form, $form_state); + } + } + + /** + * Overrides Drupal\entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $vocabulary = $this->getEntity($form_state); + + // Prevent leading and trailing spaces in vocabulary names. + $vocabulary->name = trim($vocabulary->name); + + switch (taxonomy_vocabulary_save($vocabulary)) { + case SAVED_NEW: + drupal_set_message(t('Created new vocabulary %name.', array('%name' => $vocabulary->name))); + watchdog('taxonomy', 'Created new vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit')); + $form_state['redirect'] = 'admin/structure/taxonomy/' . $vocabulary->machine_name; + break; + + case SAVED_UPDATED: + drupal_set_message(t('Updated vocabulary %name.', array('%name' => $vocabulary->name))); + watchdog('taxonomy', 'Updated vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit')); + $form_state['redirect'] = 'admin/structure/taxonomy'; + break; + } + + $form_state['values']['vid'] = $vocabulary->vid; + $form_state['vid'] = $vocabulary->vid; + } +} diff --git a/core/modules/taxonomy/taxonomy.admin.inc b/core/modules/taxonomy/taxonomy.admin.inc index be020a6..4624a01 100644 --- a/core/modules/taxonomy/taxonomy.admin.inc +++ b/core/modules/taxonomy/taxonomy.admin.inc @@ -7,6 +7,7 @@ use Drupal\taxonomy\Term; use Drupal\taxonomy\Vocabulary; +use Drupal\entity\EntityFormController; /** * Form builder to list and manage vocabularies. @@ -102,140 +103,16 @@ function theme_taxonomy_overview_vocabularies($variables) { } /** - * Form builder for the vocabulary editing form. - * - * @param Drupal\taxonomy\Vocabulary|null $vocabulary - * (optional) The taxonomy vocabulary entity to edit. If NULL or omitted, the - * form creates a new vocabulary. - * - * @ingroup forms - * @see taxonomy_form_vocabulary_submit() - * @see taxonomy_form_vocabulary_validate() + * Page callback: provides the vocabulary creation form. */ -function taxonomy_form_vocabulary($form, &$form_state, Vocabulary $vocabulary = NULL) { - // During initial form build, add the entity to the form state for use - // during form building and processing. During a rebuild, use what is in the - // form state. - if (!isset($form_state['vocabulary'])) { - // Create a new Vocabulary entity for the add form. - if (!isset($vocabulary)) { - $vocabulary = entity_create('taxonomy_vocabulary', array( - // Default the new vocabulary to the site's default language. This is - // the most likely default value until we have better flexible settings. - // @todo See http://drupal.org/node/258785 and followups. - 'langcode' => language_default()->langcode, - )); - } - $form_state['vocabulary'] = $vocabulary; - } - else { - $vocabulary = $form_state['vocabulary']; - } - - // @todo Legacy support. Modules are encouraged to access the entity using - // $form_state. Remove in Drupal 8. - $form['#vocabulary'] = $form_state['vocabulary']; - - // Check whether we need a deletion confirmation form. - if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) { - return taxonomy_vocabulary_confirm_delete($form, $form_state, $form_state['values']['vid']); - } - $form['name'] = array( - '#type' => 'textfield', - '#title' => t('Name'), - '#default_value' => $vocabulary->name, - '#maxlength' => 255, - '#required' => TRUE, - ); - $form['machine_name'] = array( - '#type' => 'machine_name', - '#default_value' => $vocabulary->machine_name, - '#maxlength' => 255, - '#machine_name' => array( - 'exists' => 'taxonomy_vocabulary_machine_name_load', - ), - ); - $form['description'] = array( - '#type' => 'textfield', - '#title' => t('Description'), - '#default_value' => $vocabulary->description, - ); - // Set the hierarchy to "multiple parents" by default. This simplifies the - // vocabulary form and standardizes the term form. - $form['hierarchy'] = array( - '#type' => 'value', - '#value' => '0', - ); - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save')); - if (isset($vocabulary->vid)) { - $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete')); - $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid); - } - $form['#validate'][] = 'taxonomy_form_vocabulary_validate'; - - return $form; -} - -/** - * Form validation handler for taxonomy_form_vocabulary(). - * - * Makes sure that the machine name of the vocabulary is not in the - * disallowed list (names that conflict with menu items, such as 'list' - * and 'add'). - * - * @see taxonomy_form_vocabulary() - * @see taxonomy_form_vocabulary_submit() - */ -function taxonomy_form_vocabulary_validate($form, &$form_state) { - // During the deletion there is no 'machine_name' key - if (isset($form_state['values']['machine_name'])) { - // Do not allow machine names to conflict with taxonomy path arguments. - $machine_name = $form_state['values']['machine_name']; - $disallowed = array('add', 'list'); - if (in_array($machine_name, $disallowed)) { - form_set_error('machine_name', t('The machine-readable name cannot be "add" or "list".')); - } - } -} - -/** - * Form submission handler for taxonomy_form_vocabulary(). - * - * @see taxonomy_form_vocabulary() - * @see taxonomy_form_vocabulary_validate() - */ -function taxonomy_form_vocabulary_submit($form, &$form_state) { - if ($form_state['triggering_element']['#value'] == t('Delete')) { - // Rebuild the form to confirm vocabulary deletion. - $form_state['rebuild'] = TRUE; - $form_state['confirm_delete'] = TRUE; - return; - } - - $vocabulary = $form_state['vocabulary']; - entity_form_submit_build_entity('taxonomy_vocabulary', $vocabulary, $form, $form_state); - - // Prevent leading and trailing spaces in vocabulary names. - $vocabulary->name = trim($vocabulary->name); - - switch (taxonomy_vocabulary_save($vocabulary)) { - case SAVED_NEW: - drupal_set_message(t('Created new vocabulary %name.', array('%name' => $vocabulary->name))); - watchdog('taxonomy', 'Created new vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit')); - $form_state['redirect'] = 'admin/structure/taxonomy/' . $vocabulary->machine_name; - break; - - case SAVED_UPDATED: - drupal_set_message(t('Updated vocabulary %name.', array('%name' => $vocabulary->name))); - watchdog('taxonomy', 'Updated vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit')); - $form_state['redirect'] = 'admin/structure/taxonomy'; - break; - } - - $form_state['values']['vid'] = $vocabulary->vid; - $form_state['vid'] = $vocabulary->vid; +function taxonomy_vocabulary_add() { + $vocabulary = entity_create('taxonomy_vocabulary', array( + // Default the new vocabulary to the site's default language. This is the + // most likely default value until we have better flexible settings. + // @todo See http://drupal.org/node/258785 and followups. + 'langcode' => language_default()->langcode, + )); + return entity_get_form($vocabulary); } /** @@ -259,7 +136,7 @@ function taxonomy_overview_terms($form, &$form_state, Vocabulary $vocabulary) { return taxonomy_vocabulary_confirm_reset_alphabetical($form, $form_state, $vocabulary->vid); } - $form['#vocabulary'] = $vocabulary; + $form_state['taxonomy']['vocabulary'] = $vocabulary; $form['#tree'] = TRUE; $form['#parent_fields'] = FALSE; @@ -461,7 +338,7 @@ function taxonomy_overview_terms_submit($form, &$form_state) { // Sort term order based on weight. uasort($form_state['values'], 'drupal_sort_weight'); - $vocabulary = $form['#vocabulary']; + $vocabulary = $form_state['taxonomy']['vocabulary']; // Update the current hierarchy type as we go. $hierarchy = TAXONOMY_HIERARCHY_DISABLED; @@ -643,247 +520,11 @@ function theme_taxonomy_overview_terms($variables) { } /** - * Form function for the term edit form. - * - * @param Drupal\taxonomy\Term|null $term - * (optional) The taxonomy term entity to edit. If NULL or omitted, the form - * creates a new term. - * @param Drupal\taxonomy\Vocabulary|null $vocabulary - * (optional) A taxonomy vocabulary entity to create the term in. Required if - * the term is omitted. - * - * @ingroup forms - * @see taxonomy_form_term_validate() - * @see taxonomy_form_term_submit() - */ -function taxonomy_form_term($form, &$form_state, Term $term = NULL, Vocabulary $vocabulary = NULL) { - // During initial form build, add the term entity to the form state for use - // during form building and processing. During a rebuild, use what is in the - // form state. - if (!isset($form_state['term'])) { - // Create a new Term entity for the add form. - if (!isset($term)) { - $term = entity_create('taxonomy_term', array( - 'vid' => $vocabulary->vid, - 'vocabulary_machine_name' => $vocabulary->machine_name, - // Default the new vocabulary to the site's default language. This is - // the most likely default value until we have better flexible settings. - // @todo See http://drupal.org/node/258785 and followups. - 'langcode' => language_default()->langcode, - )); - } - if (!isset($vocabulary) && isset($term->vid)) { - $vocabulary = taxonomy_vocabulary_load($term->vid); - } - $form_state['term'] = $term; - } - else { - $term = $form_state['term']; - if (!isset($vocabulary) && isset($term->vid)) { - $vocabulary = taxonomy_vocabulary_load($term->vid); - } - } - - $parent = array_keys(taxonomy_term_load_parents($term->tid)); - $form['#term'] = (array) $term; - $form['#term']['parent'] = $parent; - $form['#vocabulary'] = $vocabulary; - - $form['name'] = array( - '#type' => 'textfield', - '#title' => t('Name'), - '#default_value' => $term->name, - '#maxlength' => 255, - '#required' => TRUE, - '#weight' => -5, - ); - $form['description'] = array( - '#type' => 'text_format', - '#title' => t('Description'), - '#default_value' => $term->description, - '#format' => $term->format, - '#weight' => 0, - ); - - $form['vocabulary_machine_name'] = array( - '#type' => 'value', - '#value' => isset($term->vocabulary_machine_name) ? $term->vocabulary_machine_name : $vocabulary->name, - ); - - field_attach_form('taxonomy_term', $term, $form, $form_state); - - $form['relations'] = array( - '#type' => 'fieldset', - '#title' => t('Relations'), - '#collapsible' => TRUE, - '#collapsed' => ($vocabulary->hierarchy != TAXONOMY_HIERARCHY_MULTIPLE), - '#weight' => 10, - ); - - // taxonomy_get_tree and taxonomy_term_load_parents may contain large numbers of - // items so we check for taxonomy_override_selector before loading the - // full vocabulary. Contrib modules can then intercept before - // hook_form_alter to provide scalable alternatives. - if (!variable_get('taxonomy_override_selector', FALSE)) { - $parent = array_keys(taxonomy_term_load_parents($term->tid)); - $children = taxonomy_get_tree($vocabulary->vid, $term->tid); - - // A term can't be the child of itself, nor of its children. - foreach ($children as $child) { - $exclude[] = $child->tid; - } - $exclude[] = $term->tid; - - $tree = taxonomy_get_tree($vocabulary->vid); - $options = array('<' . t('root') . '>'); - if (empty($parent)) { - $parent = array(0); - } - foreach ($tree as $item) { - if (!in_array($item->tid, $exclude)) { - $options[$item->tid] = str_repeat('-', $item->depth) . $item->name; - } - } - $form['relations']['parent'] = array( - '#type' => 'select', - '#title' => t('Parent terms'), - '#options' => $options, - '#default_value' => $parent, - '#multiple' => TRUE, - ); - - } - $form['relations']['weight'] = array( - '#type' => 'textfield', - '#title' => t('Weight'), - '#size' => 6, - '#default_value' => $term->weight, - '#description' => t('Terms are displayed in ascending order by weight.'), - '#required' => TRUE, - ); - $form['vid'] = array( - '#type' => 'value', - '#value' => $vocabulary->vid, - ); - $form['tid'] = array( - '#type' => 'value', - '#value' => $term->tid, - ); - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Save'), - '#weight' => 5, - ); - - if ($term->tid) { - $form['actions']['delete'] = array( - '#type' => 'submit', - '#value' => t('Delete'), - '#access' => taxonomy_term_access('delete', $term), - '#weight' => 10, - '#submit' => array('taxonomy_form_term_delete_submit'), - ); - } - else { - $form_state['redirect'] = current_path(); - } - - return $form; -} - -/** - * Validation handler for the term form. - * - * @see taxonomy_form_term() + * Returns a rendered edit form to create a new term associated to the given vocabulary. */ -function taxonomy_form_term_validate($form, &$form_state) { - entity_form_field_validate('taxonomy_term', $form, $form_state); - - // Ensure numeric values. - if (isset($form_state['values']['weight']) && !is_numeric($form_state['values']['weight'])) { - form_set_error('weight', t('Weight value must be numeric.')); - } -} - -/** - * Submit handler to insert or update a term. - * - * @see taxonomy_form_term() - */ -function taxonomy_form_term_submit($form, &$form_state) { - $term = taxonomy_form_term_submit_build_taxonomy_term($form, $form_state); - - $status = taxonomy_term_save($term); - switch ($status) { - case SAVED_NEW: - drupal_set_message(t('Created new term %term.', array('%term' => $term->name))); - watchdog('taxonomy', 'Created new term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit')); - break; - case SAVED_UPDATED: - drupal_set_message(t('Updated term %term.', array('%term' => $term->name))); - watchdog('taxonomy', 'Updated term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit')); - // Clear the page and block caches to avoid stale data. - cache_invalidate(array('content' => TRUE)); - break; - } - - $current_parent_count = count($form_state['values']['parent']); - $previous_parent_count = count($form['#term']['parent']); - // Root doesn't count if it's the only parent. - if ($current_parent_count == 1 && isset($form_state['values']['parent'][0])) { - $current_parent_count = 0; - $form_state['values']['parent'] = array(); - } - - // If the number of parents has been reduced to one or none, do a check on the - // parents of every term in the vocabulary value. - if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) { - taxonomy_check_vocabulary_hierarchy($form['#vocabulary'], $form_state['values']); - } - // If we've increased the number of parents and this is a single or flat - // hierarchy, update the vocabulary immediately. - elseif ($current_parent_count > $previous_parent_count && $form['#vocabulary']->hierarchy != TAXONOMY_HIERARCHY_MULTIPLE) { - $form['#vocabulary']->hierarchy = $current_parent_count == 1 ? TAXONOMY_HIERARCHY_SINGLE : TAXONOMY_HIERARCHY_MULTIPLE; - taxonomy_vocabulary_save($form['#vocabulary']); - } - - $form_state['values']['tid'] = $term->tid; - $form_state['tid'] = $term->tid; -} - -/** - * Updates the form state's term entity by processing this submission's values. - */ -function taxonomy_form_term_submit_build_taxonomy_term($form, &$form_state) { - $term = $form_state['term']; - entity_form_submit_build_entity('taxonomy_term', $term, $form, $form_state); - - // Prevent leading and trailing spaces in term names. - $term->name = trim($term->name); - - // Convert text_format field into values expected by taxonomy_term_save(). - $description = $form_state['values']['description']; - $term->description = $description['value']; - $term->format = $description['format']; - return $term; -} - -/** - * Form submission handler for the 'Delete' button for taxonomy_form_term(). - * - * @see taxonomy_form_term_validate() - * @see taxonomy_form_term_submit() - */ -function taxonomy_form_term_delete_submit($form, &$form_state) { - $destination = array(); - if (isset($_GET['destination'])) { - $destination = drupal_get_destination(); - unset($_GET['destination']); - } - $term = $form['#term']; - $form_state['redirect'] = array('taxonomy/term/' . $term['tid'] . '/delete', array('query' => $destination)); +function taxonomy_term_add($vocabulary) { + $term = entity_create('taxonomy_term', array('vid' => $vocabulary->vid, 'vocabulary_machine_name' => $vocabulary->machine_name)); + return entity_get_form($term); } /** @@ -896,8 +537,7 @@ function taxonomy_term_confirm_delete($form, &$form_state, $term) { // Always provide entity id in the same form key as in the entity edit form. $form['tid'] = array('#type' => 'value', '#value' => $term->tid); - $form['#term'] = $term; - $form['#vocabulary'] = taxonomy_vocabulary_load($term->vid);; + $form_state['taxonomy']['vocabulary'] = taxonomy_vocabulary_load($term->vid);; $form['type'] = array('#type' => 'value', '#value' => 'term'); $form['name'] = array('#type' => 'value', '#value' => $term->name); $form['vocabulary_machine_name'] = array('#type' => 'value', '#value' => $term->vocabulary_machine_name); @@ -918,7 +558,7 @@ function taxonomy_term_confirm_delete($form, &$form_state, $term) { */ function taxonomy_term_confirm_delete_submit($form, &$form_state) { taxonomy_term_delete($form_state['values']['tid']); - taxonomy_check_vocabulary_hierarchy($form['#vocabulary'], $form_state['values']); + taxonomy_check_vocabulary_hierarchy($form_state['taxonomy']['vocabulary'], $form_state['values']); drupal_set_message(t('Deleted term %name.', array('%name' => $form_state['values']['name']))); watchdog('taxonomy', 'Deleted term %name.', array('%name' => $form_state['values']['name']), WATCHDOG_NOTICE); if (!isset($_GET['destination'])) { @@ -940,7 +580,7 @@ function taxonomy_vocabulary_confirm_delete($form, &$form_state, $vid) { // Always provide entity id in the same form key as in the entity edit form. $form['vid'] = array('#type' => 'value', '#value' => $vid); - $form['#vocabulary'] = $vocabulary; + $form_state['taxonomy']['vocabulary'] = $vocabulary; $form['#id'] = 'taxonomy_vocabulary_confirm_delete'; $form['type'] = array('#type' => 'value', '#value' => 'vocabulary'); $form['name'] = array('#type' => 'value', '#value' => $vocabulary->name); diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 1377b27..3c09b3d 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -113,6 +113,9 @@ function taxonomy_entity_info() { 'label' => t('Taxonomy term'), 'entity class' => 'Drupal\taxonomy\Term', 'controller class' => 'Drupal\taxonomy\TermStorageController', + 'form controller class' => array( + 'default' => 'Drupal\taxonomy\TermFormController', + ), 'base table' => 'taxonomy_term_data', 'uri callback' => 'taxonomy_term_uri', 'fieldable' => TRUE, @@ -150,6 +153,9 @@ function taxonomy_entity_info() { 'label' => t('Taxonomy vocabulary'), 'entity class' => 'Drupal\taxonomy\Vocabulary', 'controller class' => 'Drupal\taxonomy\VocabularyStorageController', + 'form controller class' => array( + 'default' => 'Drupal\taxonomy\VocabularyFormController', + ), 'base table' => 'taxonomy_vocabulary', 'entity keys' => array( 'id' => 'vid', @@ -295,8 +301,7 @@ function taxonomy_menu() { ); $items['admin/structure/taxonomy/add'] = array( 'title' => 'Add vocabulary', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('taxonomy_form_vocabulary'), + 'page callback' => 'taxonomy_vocabulary_add', 'access arguments' => array('administer taxonomy'), 'type' => MENU_LOCAL_ACTION, 'file' => 'taxonomy.admin.inc', @@ -317,10 +322,10 @@ function taxonomy_menu() { ); $items['taxonomy/term/%taxonomy_term/edit'] = array( 'title' => 'Edit', - 'page callback' => 'drupal_get_form', + 'page callback' => 'entity_get_form', // Pass a NULL argument to ensure that additional path components are not - // passed to taxonomy_form_term() as the vocabulary machine name argument. - 'page arguments' => array('taxonomy_form_term', 2, NULL), + // passed to taxonomy_term_form() as the vocabulary machine name argument. + 'page arguments' => array(2), 'access callback' => 'taxonomy_term_access', 'access arguments' => array('edit', 2), 'type' => MENU_LOCAL_TASK, @@ -370,8 +375,8 @@ function taxonomy_menu() { ); $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/edit'] = array( 'title' => 'Edit', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('taxonomy_form_vocabulary', 3), + 'page callback' => 'entity_get_form', + 'page arguments' => array(3), 'access arguments' => array('administer taxonomy'), 'type' => MENU_LOCAL_TASK, 'weight' => -10, @@ -380,8 +385,8 @@ function taxonomy_menu() { $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/add'] = array( 'title' => 'Add term', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('taxonomy_form_term', NULL, 3), + 'page callback' => 'taxonomy_term_add', + 'page arguments' => array(3), 'access arguments' => array('administer taxonomy'), 'type' => MENU_LOCAL_ACTION, 'file' => 'taxonomy.admin.inc', diff --git a/core/modules/translation/translation.module b/core/modules/translation/translation.module index ba5c406..8eb602e 100644 --- a/core/modules/translation/translation.module +++ b/core/modules/translation/translation.module @@ -19,6 +19,8 @@ * date (0) or needs to be updated (1). */ +use Drupal\entity\EntityFormController; + use Drupal\node\Node; /** @@ -153,9 +155,8 @@ function translation_node_type_language_translation_enabled_validate($element, & * @see node_form() */ function translation_form_node_form_alter(&$form, &$form_state) { - if (translation_supported_type($form['#node']->type)) { - $node = $form['#node']; - + $node = $form_state['controller']->getEntity($form_state); + if (translation_supported_type($node->type)) { if (!empty($node->translation_source)) { // We are creating a translation. Add values and lock language field. $form['translation_source'] = array('#type' => 'value', '#value' => $node->translation_source); @@ -379,10 +380,11 @@ function translation_node_update(Node $node) { * * Ensures that duplicate translations can't be created for the same source. */ -function translation_node_validate(Node $node, $form) { +function translation_node_validate(Node $node, $form, &$form_state) { // Only act on translatable nodes with a tnid or translation_source. - if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form['#node']->translation_source->nid))) { - $tnid = !empty($node->tnid) ? $node->tnid : $form['#node']->translation_source->nid; + $form_node = $form_state['controller']->getEntity($form_state); + if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form_node->translation_source->nid))) { + $tnid = !empty($node->tnid) ? $node->tnid : $form_node->translation_source->nid; $translations = translation_node_get_translations($tnid); if (isset($translations[$node->langcode]) && $translations[$node->langcode]->nid != $node->nid) { form_set_error('langcode', t('There is already a translation in this language.')); diff --git a/core/modules/user/lib/Drupal/user/AccountFormController.php b/core/modules/user/lib/Drupal/user/AccountFormController.php new file mode 100644 index 0000000..31acf0d --- /dev/null +++ b/core/modules/user/lib/Drupal/user/AccountFormController.php @@ -0,0 +1,330 @@ +uid); + $admin = user_access('administer users'); + + // Account information. + $form['account'] = array( + '#type' => 'container', + '#weight' => -10, + ); + + // Only show name field on registration form or user can change own username. + $form['account']['name'] = array( + '#type' => 'textfield', + '#title' => t('Username'), + '#maxlength' => USERNAME_MAX_LENGTH, + '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.'), + '#required' => TRUE, + '#attributes' => array('class' => array('username'), 'autocomplete' => 'off'), + '#default_value' => (!$register ? $account->name : ''), + '#access' => ($register || ($user->uid == $account->uid && user_access('change own username')) || $admin), + '#weight' => -10, + ); + + // The mail field is NOT required if account originally had no mail set + // and the user performing the edit has 'administer users' permission. + // This allows users without e-mail address to be edited and deleted. + $form['account']['mail'] = array( + '#type' => 'email', + '#title' => t('E-mail address'), + '#description' => t('A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'), + '#required' => !(empty($account->mail) && user_access('administer users')), + '#default_value' => (!$register ? $account->mail : ''), + '#attributes' => array('autocomplete' => 'off'), + ); + + // Display password field only for existing users or when user is allowed to + // assign a password during registration. + if (!$register) { + $form['account']['pass'] = array( + '#type' => 'password_confirm', + '#size' => 25, + '#description' => t('To change the current user password, enter the new password in both fields.'), + ); + + // To skip the current password field, the user must have logged in via a + // one-time link and have the token in the URL. + $pass_reset = isset($_SESSION['pass_reset_' . $account->uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] == $_SESSION['pass_reset_' . $account->uid]); + $protected_values = array(); + $current_pass_description = ''; + + // The user may only change their own password without their current + // password if they logged in via a one-time login link. + if (!$pass_reset) { + $protected_values['mail'] = $form['account']['mail']['#title']; + $protected_values['pass'] = t('Password'); + $request_new = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.')))); + $current_pass_description = t('Required if you want to change the %mail or %pass below. !request_new.', array('%mail' => $protected_values['mail'], '%pass' => $protected_values['pass'], '!request_new' => $request_new)); + } + + // The user must enter their current password to change to a new one. + if ($user->uid == $account->uid) { + $form['account']['current_pass_required_values'] = array( + '#type' => 'value', + '#value' => $protected_values, + ); + + $form['account']['current_pass'] = array( + '#type' => 'password', + '#title' => t('Current password'), + '#size' => 25, + '#access' => !empty($protected_values), + '#description' => $current_pass_description, + '#weight' => -5, + '#attributes' => array('autocomplete' => 'off'), + ); + + $form_state['user'] = $account; + $form['#validate'][] = 'user_validate_current_pass'; + } + } + elseif (!variable_get('user_email_verification', TRUE) || $admin) { + $form['account']['pass'] = array( + '#type' => 'password_confirm', + '#size' => 25, + '#description' => t('Provide a password for the new account in both fields.'), + '#required' => TRUE, + ); + } + + if ($admin) { + $status = isset($account->status) ? $account->status : 1; + } + else { + $status = $register ? variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) == USER_REGISTER_VISITORS : $account->status; + } + + $form['account']['status'] = array( + '#type' => 'radios', + '#title' => t('Status'), + '#default_value' => $status, + '#options' => array(t('Blocked'), t('Active')), + '#access' => $admin, + ); + + $roles = array_map('check_plain', user_roles(TRUE)); + // The disabled checkbox subelement for the 'authenticated user' role + // must be generated separately and added to the checkboxes element, + // because of a limitation in Form API not supporting a single disabled + // checkbox within a set of checkboxes. + // @todo This should be solved more elegantly. See issue #119038. + $checkbox_authenticated = array( + '#type' => 'checkbox', + '#title' => $roles[DRUPAL_AUTHENTICATED_RID], + '#default_value' => TRUE, + '#disabled' => TRUE, + ); + unset($roles[DRUPAL_AUTHENTICATED_RID]); + + $form['account']['roles'] = array( + '#type' => 'checkboxes', + '#title' => t('Roles'), + '#default_value' => (!$register && isset($account->roles) ? array_keys($account->roles) : array()), + '#options' => $roles, + '#access' => $roles && user_access('administer permissions'), + DRUPAL_AUTHENTICATED_RID => $checkbox_authenticated, + ); + + $form['account']['notify'] = array( + '#type' => 'checkbox', + '#title' => t('Notify user of new account'), + '#access' => $register && $admin, + ); + + // Signature. + $form['signature_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Signature settings'), + '#weight' => 1, + '#access' => (!$register && variable_get('user_signatures', 0)), + ); + + $form['signature_settings']['signature'] = array( + '#type' => 'text_format', + '#title' => t('Signature'), + '#default_value' => isset($account->signature) ? $account->signature : '', + '#description' => t('Your signature will be publicly displayed at the end of your comments.'), + '#format' => isset($account->signature_format) ? $account->signature_format : NULL, + ); + + // Picture/avatar. + $form['picture'] = array( + '#type' => 'fieldset', + '#title' => t('Picture'), + '#weight' => 1, + '#access' => (!$register && variable_get('user_pictures', 0)), + ); + + $form['picture']['picture'] = array( + '#type' => 'value', + '#value' => isset($account->picture) ? $account->picture : NULL, + ); + + $form['picture']['picture_current'] = array( + '#markup' => theme('user_picture', array('account' => $account)), + ); + + $form['picture']['picture_delete'] = array( + '#type' => 'checkbox', + '#title' => t('Delete picture'), + '#access' => !empty($account->picture->fid), + '#description' => t('Check this box to delete your current picture.'), + ); + + $form['picture']['picture_upload'] = array( + '#type' => 'file', + '#title' => t('Upload picture'), + '#size' => 48, + '#description' => t('Your virtual face or picture. Pictures larger than @dimensions pixels will be scaled down.', array('@dimensions' => variable_get('user_picture_dimensions', '85x85'))) . ' ' . filter_xss_admin(variable_get('user_picture_guidelines', '')), + ); + + $form['#validate'][] = 'user_validate_picture'; + + if (module_exists('language') && language_multilingual()) { + $languages = language_list(); + + // If the user is being created, we set the user language to the page language. + $user_preferred_language = $register ? $language_interface : user_preferred_language($account); + + $names = array(); + foreach ($languages as $langcode => $item) { + $names[$langcode] = $item->name; + } + + // Is default the interface language? + $interface_language_is_default = language_negotiation_method_get_first(LANGUAGE_TYPE_INTERFACE) != LANGUAGE_NEGOTIATION_DEFAULT; + $form['language'] = array( + '#type' => 'fieldset', + '#title' => t('Language settings'), + // Display language selector when either creating a user on the admin + // interface or editing a user account. + '#access' => !$register || user_access('administer users'), + ); + + $form['language']['preferred_langcode'] = array( + '#type' => (count($names) <= 5 ? 'radios' : 'select'), + '#title' => t('Language'), + '#default_value' => $user_preferred_language->langcode, + '#options' => $names, + '#description' => $interface_language_is_default ? t("This account's preferred language for e-mails and site presentation.") : t("This account's preferred language for e-mails."), + ); + } + else { + $form['language'] = array( + '#type' => 'container', + ); + + $form['language']['preferred_langcode'] = array( + '#type' => 'value', + '#value' => language_default()->langcode, + ); + } + + // User entities contain both a langcode property (for identifying the + // language of the entity data) and a preferred_langcode property (see + // above). Rather than provide a UI forcing the user to choose both + // separately, assume that the user profile data is in the user's preferred + // language. This element provides that synchronization. For use-cases where + // this synchronization is not desired, a module can alter or remove this + // element. + $form['language']['langcode'] = array( + '#type' => 'value', + '#value_callback' => '_user_language_selector_langcode_value', + // For the synchronization to work, this element must have a larger weight + // than the preferred_langcode element. Set a large weight here in case + // a module alters the weight of the other element. + '#weight' => 100, + ); + + return parent::form($form, $form_state, $account); + } + + /** + * Overrides Drupal\entity\EntityFormController::submit(). + */ + public function validate(array $form, array &$form_state) { + parent::validate($form, $form_state); + + $account = $this->getEntity($form_state); + // Validate new or changing username. + if (isset($form_state['values']['name'])) { + if ($error = user_validate_name($form_state['values']['name'])) { + form_set_error('name', $error); + } + // Cast the user ID as an integer. It might have been set to NULL, which + // could lead to unexpected results. + else { + $name_taken = (bool) db_select('users') + ->fields('users', array('uid')) + ->condition('uid', (int) $account->uid, '<>') + ->condition('name', db_like($form_state['values']['name']), 'LIKE') + ->range(0, 1) + ->execute() + ->fetchField(); + + if ($name_taken) { + form_set_error('name', t('The name %name is already taken.', array('%name' => $form_state['values']['name']))); + } + } + } + + $mail = $form_state['values']['mail']; + + if (!empty($mail)) { + $mail_taken = (bool) db_select('users') + ->fields('users', array('uid')) + ->condition('uid', (int) $account->uid, '<>') + ->condition('mail', db_like($mail), 'LIKE') + ->range(0, 1) + ->execute() + ->fetchField(); + + if ($mail_taken) { + // Format error message dependent on whether the user is logged in or not. + if ($GLOBALS['user']->uid) { + form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => $mail))); + } + else { + form_set_error('mail', t('The e-mail address %email is already registered. Have you forgotten your password?', array('%email' => $mail, '@password' => url('user/password')))); + } + } + } + + // Make sure the signature isn't longer than the size of the database field. + // Signatures are disabled by default, so make sure it exists first. + if (isset($form_state['values']['signature'])) { + // Move text format for user signature into 'signature_format'. + $form_state['values']['signature_format'] = $form_state['values']['signature']['format']; + // Move text value for user signature into 'signature'. + $form_state['values']['signature'] = $form_state['values']['signature']['value']; + + $user_schema = drupal_get_schema('users'); + if (drupal_strlen($form_state['values']['signature']) > $user_schema['fields']['signature']['length']) { + form_set_error('signature', t('The signature is too long: it must be %max characters or less.', array('%max' => $user_schema['fields']['signature']['length']))); + } + } + } +} diff --git a/core/modules/user/lib/Drupal/user/ProfileFormController.php b/core/modules/user/lib/Drupal/user/ProfileFormController.php new file mode 100644 index 0000000..d32cf4a --- /dev/null +++ b/core/modules/user/lib/Drupal/user/ProfileFormController.php @@ -0,0 +1,62 @@ +getEntity($form_state); + + // @todo Actually the cancel action can be assimilated to the delete one: we + // should alter it instead of providing a new one. + unset($element['delete']); + + $element['cancel'] = array( + '#type' => 'submit', + '#value' => t('Cancel account'), + '#submit' => array('user_edit_cancel_submit'), + '#access' => $account->uid > 1 && (($account->uid == $GLOBALS['user']->uid && user_access('cancel account')) || user_access('administer users')), + ); + + return $element; + } + + /** + * Overrides Drupal\entity\EntityFormController::submit(). + */ + public function submit(array $form, array &$form_state) { + // @todo Consider moving this into the parent method. + // Remove unneeded values. + form_state_values_clean($form_state); + parent::submit($form, $form_state); + } + + /** + * Overrides Drupal\entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $account = $this->getEntity($form_state); + $account->save(); + $form_state['values']['uid'] = $account->id(); + + // Clear the page cache because pages can contain usernames and/or profile + // information: + cache_invalidate(array('content' => TRUE)); + + drupal_set_message(t('The changes have been saved.')); + } +} diff --git a/core/modules/user/lib/Drupal/user/RegisterFormController.php b/core/modules/user/lib/Drupal/user/RegisterFormController.php new file mode 100644 index 0000000..9c88aec --- /dev/null +++ b/core/modules/user/lib/Drupal/user/RegisterFormController.php @@ -0,0 +1,155 @@ + 'value', + '#value' => $admin, + ); + + // If we aren't admin but already logged on, go to the user page instead. + if (!$admin && $user->uid) { + drupal_goto('user/' . $user->uid); + } + + $form['#attached']['library'][] = array('system', 'jquery.cookie'); + $form['#attributes']['class'][] = 'user-info-from-cookie'; + + // Start with the default user account fields. + $form = parent::form($form, $form_state, $account); + + // Attach field widgets, and hide the ones where the 'user_register_form' + // setting is not on. + field_attach_form('user', $account, $form, $form_state); + foreach (field_info_instances('user', 'user') as $field_name => $instance) { + if (empty($instance['settings']['user_register_form'])) { + $form[$field_name]['#access'] = FALSE; + } + } + + if ($admin) { + // Redirect back to page which initiated the create request; usually + // admin/people/create. + $form_state['redirect'] = current_path(); + } + + return $form; + } + + /** + * Overrides Drupal\entity\EntityFormController::actions(). + */ + protected function actions(array $form, array &$form_state) { + $element = parent::actions($form, $form_state); + $element['submit']['#value'] = t('Create new account'); + return $element; + } + + /** + * Overrides Drupal\entity\EntityFormController::submit(). + */ + public function submit(array $form, array &$form_state) { + $admin = $form_state['values']['administer_users']; + + if (!variable_get('user_email_verification', TRUE) || $admin) { + $pass = $form_state['values']['pass']; + } + else { + $pass = user_password(); + } + + // Remove unneeded values. + form_state_values_clean($form_state); + + $form_state['values']['pass'] = $pass; + $form_state['values']['init'] = $form_state['values']['mail']; + + parent::submit($form, $form_state); + } + + /** + * Overrides Drupal\entity\EntityFormController::submit(). + */ + public function save(array $form, array &$form_state) { + $account = $this->getEntity($form_state); + $pass = $account->pass; + $admin = $form_state['values']['administer_users']; + $notify = !empty($form_state['values']['notify']); + + $account->save(); + + // Terminate if an error occurred while saving the account. + if ($status =! SAVED_NEW) { + drupal_set_message(t("Error saving user account."), 'error'); + $form_state['redirect'] = ''; + return; + } + $form_state['user'] = $account; + $form_state['values']['uid'] = $account->uid; + + watchdog('user', 'New user: %name (%email).', array('%name' => $form_state['values']['name'], '%email' => $form_state['values']['mail']), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit')); + + // Add plain text password into user account to generate mail tokens. + $account->password = $pass; + + // New administrative account without notification. + $uri = entity_uri('user', $account); + if ($admin && !$notify) { + drupal_set_message(t('Created a new user account for %name. No e-mail has been sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name))); + } + // No e-mail verification required; log in user immediately. + elseif (!$admin && !variable_get('user_email_verification', TRUE) && $account->status) { + _user_mail_notify('register_no_approval_required', $account); + $form_state['uid'] = $account->uid; + user_login_submit(array(), $form_state); + drupal_set_message(t('Registration successful. You are now logged in.')); + $form_state['redirect'] = ''; + } + // No administrator approval required. + elseif ($account->status || $notify) { + if (empty($account->mail) && $notify) { + drupal_set_message(t('The new user %name was created without an email address, so no welcome message was sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name))); + } + else { + $op = $notify ? 'register_admin_created' : 'register_no_approval_required'; + _user_mail_notify($op, $account); + if ($notify) { + drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user %name.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name))); + } + else { + drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.')); + $form_state['redirect'] = ''; + } + } + } + // Administrator approval required. + else { + _user_mail_notify('register_pending_approval', $account); + drupal_set_message(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.
In the meantime, a welcome message with further instructions has been sent to your e-mail address.')); + $form_state['redirect'] = ''; + } + } +} diff --git a/core/modules/user/tests/user_form_test.module b/core/modules/user/tests/user_form_test.module index 4e907f3..5702c53 100644 --- a/core/modules/user/tests/user_form_test.module +++ b/core/modules/user/tests/user_form_test.module @@ -27,7 +27,7 @@ function user_form_test_menu() { */ function user_form_test_current_password($form, &$form_state, $account) { $account->user_form_test_field = ''; - $form['#user'] = $account; + $form_state['user'] = $account; $form['user_form_test_field'] = array( '#type' => 'textfield', @@ -35,7 +35,7 @@ function user_form_test_current_password($form, &$form_state, $account) { '#description' => t('A field that would require a correct password to change.'), '#required' => TRUE, ); - + $form['current_pass'] = array( '#type' => 'password', '#title' => t('Current password'), diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc index 58b0218..fac9723 100644 --- a/core/modules/user/user.admin.inc +++ b/core/modules/user/user.admin.inc @@ -11,7 +11,8 @@ function user_admin($callback_arg = '') { switch ($op) { case t('Create new account'): case 'create': - $build['user_register'] = drupal_get_form('user_register_form'); + $account = entity_create('user', array()); + $build['user_register'] = entity_get_form($account, 'register'); break; default: if (!empty($_POST['accounts']) && isset($_POST['operation']) && ($_POST['operation'] == 'cancel')) { diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 1f72aa6..d798e1a 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1,5 +1,7 @@ array( 'label' => t('User'), 'controller class' => 'Drupal\user\UserStorageController', + 'form controller class' => array( + 'profile' => 'Drupal\user\ProfileFormController', + 'register' => 'Drupal\user\RegisterFormController', + ), 'base table' => 'users', 'uri callback' => 'user_uri', 'label callback' => 'user_label', @@ -399,7 +405,7 @@ function user_validate_name($name) { /** * Validates an image uploaded by a user. * - * @see user_account_form() + * @see AccountFormController::form() */ function user_validate_picture(&$form, &$form_state) { // If required, validate the uploaded picture. @@ -701,242 +707,6 @@ function user_user_view($account) { } /** - * Helper function to add default user account fields to user registration and edit form. - * - * @see user_account_form_validate() - * @see user_validate_current_pass() - * @see user_validate_picture() - * @see user_validate_mail() - */ -function user_account_form(&$form, &$form_state) { - global $user; - $language_interface = language(LANGUAGE_TYPE_INTERFACE); - - $account = $form['#user']; - $register = ($form['#user']->uid > 0 ? FALSE : TRUE); - - $admin = user_access('administer users'); - - $form['#validate'][] = 'user_account_form_validate'; - - // Account information. - $form['account'] = array( - '#type' => 'container', - '#weight' => -10, - ); - // Only show name field on registration form or user can change own username. - $form['account']['name'] = array( - '#type' => 'textfield', - '#title' => t('Username'), - '#maxlength' => USERNAME_MAX_LENGTH, - '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.'), - '#required' => TRUE, - '#attributes' => array('class' => array('username'), 'autocomplete' => 'off'), - '#default_value' => (!$register ? $account->name : ''), - '#access' => ($register || ($user->uid == $account->uid && user_access('change own username')) || $admin), - '#weight' => -10, - ); - - // The mail field is NOT required if account originally had no mail set - // and the user performing the edit has 'administer users' permission. - // This allows users without e-mail address to be edited and deleted. - $form['account']['mail'] = array( - '#type' => 'email', - '#title' => t('E-mail address'), - '#description' => t('A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'), - '#required' => !(empty($account->mail) && user_access('administer users')), - '#default_value' => (!$register ? $account->mail : ''), - '#attributes' => array('autocomplete' => 'off'), - ); - - // Display password field only for existing users or when user is allowed to - // assign a password during registration. - if (!$register) { - $form['account']['pass'] = array( - '#type' => 'password_confirm', - '#size' => 25, - '#description' => t('To change the current user password, enter the new password in both fields.'), - ); - // To skip the current password field, the user must have logged in via a - // one-time link and have the token in the URL. - $pass_reset = isset($_SESSION['pass_reset_' . $account->uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] == $_SESSION['pass_reset_' . $account->uid]); - $protected_values = array(); - $current_pass_description = ''; - // The user may only change their own password without their current - // password if they logged in via a one-time login link. - if (!$pass_reset) { - $protected_values['mail'] = $form['account']['mail']['#title']; - $protected_values['pass'] = t('Password'); - $request_new = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.')))); - $current_pass_description = t('Required if you want to change the %mail or %pass below. !request_new.', array('%mail' => $protected_values['mail'], '%pass' => $protected_values['pass'], '!request_new' => $request_new)); - } - // The user must enter their current password to change to a new one. - if ($user->uid == $account->uid) { - $form['account']['current_pass_required_values'] = array( - '#type' => 'value', - '#value' => $protected_values, - ); - $form['account']['current_pass'] = array( - '#type' => 'password', - '#title' => t('Current password'), - '#size' => 25, - '#access' => !empty($protected_values), - '#description' => $current_pass_description, - '#weight' => -5, - '#attributes' => array('autocomplete' => 'off'), - ); - $form['#validate'][] = 'user_validate_current_pass'; - } - } - elseif (!variable_get('user_email_verification', TRUE) || $admin) { - $form['account']['pass'] = array( - '#type' => 'password_confirm', - '#size' => 25, - '#description' => t('Provide a password for the new account in both fields.'), - '#required' => TRUE, - ); - } - - if ($admin) { - $status = isset($account->status) ? $account->status : 1; - } - else { - $status = $register ? variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) == USER_REGISTER_VISITORS : $account->status; - } - $form['account']['status'] = array( - '#type' => 'radios', - '#title' => t('Status'), - '#default_value' => $status, - '#options' => array(t('Blocked'), t('Active')), - '#access' => $admin, - ); - - $roles = array_map('check_plain', user_roles(TRUE)); - // The disabled checkbox subelement for the 'authenticated user' role - // must be generated separately and added to the checkboxes element, - // because of a limitation in Form API not supporting a single disabled - // checkbox within a set of checkboxes. - // @todo This should be solved more elegantly. See issue #119038. - $checkbox_authenticated = array( - '#type' => 'checkbox', - '#title' => $roles[DRUPAL_AUTHENTICATED_RID], - '#default_value' => TRUE, - '#disabled' => TRUE, - ); - unset($roles[DRUPAL_AUTHENTICATED_RID]); - $form['account']['roles'] = array( - '#type' => 'checkboxes', - '#title' => t('Roles'), - '#default_value' => (!$register && isset($account->roles) ? array_keys($account->roles) : array()), - '#options' => $roles, - '#access' => $roles && user_access('administer permissions'), - DRUPAL_AUTHENTICATED_RID => $checkbox_authenticated, - ); - - $form['account']['notify'] = array( - '#type' => 'checkbox', - '#title' => t('Notify user of new account'), - '#access' => $register && $admin, - ); - - // Signature. - $form['signature_settings'] = array( - '#type' => 'fieldset', - '#title' => t('Signature settings'), - '#weight' => 1, - '#access' => (!$register && variable_get('user_signatures', 0)), - ); - - $form['signature_settings']['signature'] = array( - '#type' => 'text_format', - '#title' => t('Signature'), - '#default_value' => isset($account->signature) ? $account->signature : '', - '#description' => t('Your signature will be publicly displayed at the end of your comments.'), - '#format' => isset($account->signature_format) ? $account->signature_format : NULL, - ); - - // Picture/avatar. - $form['picture'] = array( - '#type' => 'fieldset', - '#title' => t('Picture'), - '#weight' => 1, - '#access' => (!$register && variable_get('user_pictures', 0)), - ); - $form['picture']['picture'] = array( - '#type' => 'value', - '#value' => isset($account->picture) ? $account->picture : NULL, - ); - $form['picture']['picture_current'] = array( - '#markup' => theme('user_picture', array('account' => $account)), - ); - $form['picture']['picture_delete'] = array( - '#type' => 'checkbox', - '#title' => t('Delete picture'), - '#access' => !empty($account->picture->fid), - '#description' => t('Check this box to delete your current picture.'), - ); - $form['picture']['picture_upload'] = array( - '#type' => 'file', - '#title' => t('Upload picture'), - '#size' => 48, - '#description' => t('Your virtual face or picture. Pictures larger than @dimensions pixels will be scaled down.', array('@dimensions' => variable_get('user_picture_dimensions', '85x85'))) . ' ' . filter_xss_admin(variable_get('user_picture_guidelines', '')), - ); - $form['#validate'][] = 'user_validate_picture'; - - if (module_exists('language') && language_multilingual()) { - $languages = language_list(); - - // If the user is being created, we set the user language to the page language. - $user_preferred_language = $register ? $language_interface : user_preferred_language($account); - - $names = array(); - foreach ($languages as $langcode => $item) { - $names[$langcode] = $item->name; - } - // Is default the interface language? - $interface_language_is_default = language_negotiation_method_get_first(LANGUAGE_TYPE_INTERFACE) != LANGUAGE_NEGOTIATION_DEFAULT; - $form['language'] = array( - '#type' => 'fieldset', - '#title' => t('Language settings'), - // Display language selector when either creating a user on the admin - // interface or editing a user account. - '#access' => !$register || user_access('administer users'), - ); - $form['language']['preferred_langcode'] = array( - '#type' => (count($names) <= 5 ? 'radios' : 'select'), - '#title' => t('Language'), - '#default_value' => $user_preferred_language->langcode, - '#options' => $names, - '#description' => $interface_language_is_default ? t("This account's preferred language for e-mails and site presentation.") : t("This account's preferred language for e-mails."), - ); - } - else { - $form['language'] = array( - '#type' => 'container', - ); - $form['language']['preferred_langcode'] = array( - '#type' => 'value', - '#value' => language_default()->langcode, - ); - } - - // User entities contain both a langcode property (for identifying the - // language of the entity data) and a preferred_langcode property (see above). - // Rather than provide a UI forcing the user to choose both separately, - // assume that the user profile data is in the user's preferred language. This - // element provides that synchronization. For use-cases where this - // synchronization is not desired, a module can alter or remove this element. - $form['language']['langcode'] = array( - '#type' => 'value', - '#value_callback' => '_user_language_selector_langcode_value', - // For the synchronization to work, this element must have a larger weight - // than the preferred_langcode element. Set a large weight here in case - // a module alters the weight of the other element. - '#weight' => 100, - ); -} - -/** * Sets the value of the user register and profile forms' langcode element. */ function _user_language_selector_langcode_value($element, $input, &$form_state) { @@ -948,12 +718,12 @@ function _user_language_selector_langcode_value($element, $input, &$form_state) } /** - * Form validation handler for the current password on the user_account_form(). + * Form validation handler for the current password on the user account form. * - * @see user_account_form() + * @see AccountFormController::form() */ function user_validate_current_pass(&$form, &$form_state) { - $account = $form['#user']; + $account = $form_state['user']; foreach ($form_state['values']['current_pass_required_values'] as $key => $name) { // This validation only works for required textfields (like mail) or // form values like password_confirm that have their own validation @@ -971,72 +741,6 @@ function user_validate_current_pass(&$form, &$form_state) { } } -/** - * Form validation handler for user_account_form(). - * - * @see user_account_form() - */ -function user_account_form_validate($form, &$form_state) { - $account = $form['#user']; - // Validate new or changing username. - if (isset($form_state['values']['name'])) { - if ($error = user_validate_name($form_state['values']['name'])) { - form_set_error('name', $error); - } - // Cast the user ID as an integer. It might have been set to NULL, which - // could lead to unexpected results. - else { - $name_taken = (bool) db_select('users') - ->fields('users', array('uid')) - ->condition('uid', (int) $account->uid, '<>') - ->condition('name', db_like($form_state['values']['name']), 'LIKE') - ->range(0, 1) - ->execute() - ->fetchField(); - - if ($name_taken) { - form_set_error('name', t('The name %name is already taken.', array('%name' => $form_state['values']['name']))); - } - } - } - - $mail = $form_state['values']['mail']; - - if (!empty($mail)) { - $mail_taken = (bool) db_select('users') - ->fields('users', array('uid')) - ->condition('uid', (int) $account->uid, '<>') - ->condition('mail', db_like($mail), 'LIKE') - ->range(0, 1) - ->execute() - ->fetchField(); - - if ($mail_taken) { - // Format error message dependent on whether the user is logged in or not. - if ($GLOBALS['user']->uid) { - form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => $mail))); - } - else { - form_set_error('mail', t('The e-mail address %email is already registered. Have you forgotten your password?', array('%email' => $mail, '@password' => url('user/password')))); - } - } - } - - // Make sure the signature isn't longer than the size of the database field. - // Signatures are disabled by default, so make sure it exists first. - if (isset($form_state['values']['signature'])) { - // Move text format for user signature into 'signature_format'. - $form_state['values']['signature_format'] = $form_state['values']['signature']['format']; - // Move text value for user signature into 'signature'. - $form_state['values']['signature'] = $form_state['values']['signature']['value']; - - $user_schema = drupal_get_schema('users'); - if (drupal_strlen($form_state['values']['signature']) > $user_schema['fields']['signature']['length']) { - form_set_error('signature', t('The signature is too long: it must be %max characters or less.', array('%max' => $user_schema['fields']['signature']['length']))); - } - } -} - function user_login_block($form) { $form['#action'] = url(current_path(), array('query' => drupal_get_destination(), 'external' => FALSE)); $form['#id'] = 'user-login-form'; @@ -1419,6 +1123,11 @@ function user_is_logged_in() { return (bool) $GLOBALS['user']->uid; } +function user_register() { + $account = entity_create('user', array()); + return entity_get_form($account, 'register'); +} + function user_register_access() { return user_is_anonymous() && variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL); } @@ -1498,8 +1207,7 @@ function user_menu() { $items['user/register'] = array( 'title' => 'Create new account', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('user_register_form'), + 'page callback' => 'user_register', 'access callback' => 'user_register_access', 'type' => MENU_LOCAL_TASK, ); @@ -1652,8 +1360,8 @@ function user_menu() { $items['user/%user/edit'] = array( 'title' => 'Edit', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('user_profile_form', 1), + 'page callback' => 'entity_get_form', + 'page arguments' => array(1, 'profile'), 'access callback' => 'user_edit_access', 'access arguments' => array(1), 'type' => MENU_LOCAL_TASK, @@ -3496,157 +3204,6 @@ function user_form_field_ui_field_edit_form_submit($form, &$form_state) { } /** - * Form builder; the user registration form. - * - * @ingroup forms - * @see user_account_form() - * @see user_account_form_validate() - * @see user_register_submit() - */ -function user_register_form($form, &$form_state) { - global $user; - - $admin = user_access('administer users'); - - // Pass access information to the submit handler. Running an access check - // inside the submit function interferes with form processing and breaks - // hook_form_alter(). - $form['administer_users'] = array( - '#type' => 'value', - '#value' => $admin, - ); - - // If we aren't admin but already logged on, go to the user page instead. - if (!$admin && $user->uid) { - drupal_goto('user/' . $user->uid); - } - - $form['#user'] = entity_create('user', array()); - - $form['#attached']['library'][] = array('system', 'jquery.cookie'); - $form['#attributes']['class'][] = 'user-info-from-cookie'; - - // Start with the default user account fields. - user_account_form($form, $form_state); - - // Attach field widgets, and hide the ones where the 'user_register_form' - // setting is not on. - field_attach_form('user', $form['#user'], $form, $form_state); - foreach (field_info_instances('user', 'user') as $field_name => $instance) { - if (empty($instance['settings']['user_register_form'])) { - $form[$field_name]['#access'] = FALSE; - } - } - - if ($admin) { - // Redirect back to page which initiated the create request; - // usually admin/people/create. - $form_state['redirect'] = current_path(); - } - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Create new account'), - ); - - $form['#validate'][] = 'user_register_validate'; - // Add the final user registration form submit handler. - $form['#submit'][] = 'user_register_submit'; - - return $form; -} - -/** - * Validation function for the user registration form. - */ -function user_register_validate($form, &$form_state) { - entity_form_field_validate('user', $form, $form_state); -} - -/** - * Submit handler for the user registration form. - * - * This function is shared by the installation form and the normal registration form, - * which is why it can't be in the user.pages.inc file. - * - * @see user_register_form() - */ -function user_register_submit($form, &$form_state) { - $admin = $form_state['values']['administer_users']; - - if (!variable_get('user_email_verification', TRUE) || $admin) { - $pass = $form_state['values']['pass']; - } - else { - $pass = user_password(); - } - $notify = !empty($form_state['values']['notify']); - - // Remove unneeded values. - form_state_values_clean($form_state); - - $form_state['values']['pass'] = $pass; - $form_state['values']['init'] = $form_state['values']['mail']; - - $account = $form['#user']; - - entity_form_submit_build_entity('user', $account, $form, $form_state); - $status = $account->save(); - - // Terminate if an error occurred while saving the account. - if ($status =! SAVED_NEW) { - drupal_set_message(t("Error saving user account."), 'error'); - $form_state['redirect'] = ''; - return; - } - $form_state['user'] = $account; - $form_state['values']['uid'] = $account->uid; - - watchdog('user', 'New user: %name (%email).', array('%name' => $form_state['values']['name'], '%email' => $form_state['values']['mail']), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit')); - - // Add plain text password into user account to generate mail tokens. - $account->password = $pass; - - // New administrative account without notification. - $uri = entity_uri('user', $account); - if ($admin && !$notify) { - drupal_set_message(t('Created a new user account for %name. No e-mail has been sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name))); - } - // No e-mail verification required; log in user immediately. - elseif (!$admin && !variable_get('user_email_verification', TRUE) && $account->status) { - _user_mail_notify('register_no_approval_required', $account); - $form_state['uid'] = $account->uid; - user_login_submit(array(), $form_state); - drupal_set_message(t('Registration successful. You are now logged in.')); - $form_state['redirect'] = ''; - } - // No administrator approval required. - elseif ($account->status || $notify) { - if (empty($account->mail) && $notify) { - drupal_set_message(t('The new user %name was created without an email address, so no welcome message was sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name))); - } - else { - $op = $notify ? 'register_admin_created' : 'register_no_approval_required'; - _user_mail_notify($op, $account); - if ($notify) { - drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user %name.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name))); - } - else { - drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.')); - $form_state['redirect'] = ''; - } - } - } - // Administrator approval required. - else { - _user_mail_notify('register_pending_approval', $account); - drupal_set_message(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.
In the meantime, a welcome message with further instructions has been sent to your e-mail address.')); - $form_state['redirect'] = ''; - } -} - -/** * Implements hook_modules_installed(). */ function user_modules_installed($modules) { diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc index 434de72..0ac062b 100644 --- a/core/modules/user/user.pages.inc +++ b/core/modules/user/user.pages.inc @@ -5,6 +5,8 @@ * User page callback file for the user module. */ +use Drupal\entity\EntityFormController; + use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -204,86 +206,6 @@ function template_preprocess_user_profile(&$variables) { } /** - * Form builder; edit a user account. - * - * @ingroup forms - * @see user_account_form() - * @see user_account_form_validate() - * @see user_profile_form_validate() - * @see user_profile_form_submit() - * @see user_cancel_confirm_form_submit() - */ -function user_profile_form($form, &$form_state, $account) { - global $user; - - // During initial form build, add the entity to the form state for use during - // form building and processing. During a rebuild, use what is in the form - // state. - if (!isset($form_state['user'])) { - $form_state['user'] = $account; - } - else { - $account = $form_state['user']; - } - - // @todo Legacy support. Modules are encouraged to access the entity using - // $form_state. Remove in Drupal 8. - $form['#user'] = $account; - - - user_account_form($form, $form_state); - // Attach field widgets. - field_attach_form('user', $account, $form, $form_state); - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Save'), - ); - $form['actions']['cancel'] = array( - '#type' => 'submit', - '#value' => t('Cancel account'), - '#submit' => array('user_edit_cancel_submit'), - '#access' => $account->uid > 1 && (($account->uid == $user->uid && user_access('cancel account')) || user_access('administer users')), - ); - - $form['#validate'][] = 'user_profile_form_validate'; - // Add the final user profile form submit handler. - $form['#submit'][] = 'user_profile_form_submit'; - - return $form; -} - -/** - * Validation function for the user account and profile editing form. - */ -function user_profile_form_validate($form, &$form_state) { - entity_form_field_validate('user', $form, $form_state); -} - -/** - * Submit function for the user account and profile editing form. - */ -function user_profile_form_submit($form, &$form_state) { - $account = $form_state['user']; - // Remove unneeded values. - form_state_values_clean($form_state); - - entity_form_submit_build_entity('user', $account, $form, $form_state); - $account->save(); - $form_state['values']['uid'] = $account->uid; - - if (!empty($edit['pass'])) { - // Remove the password reset tag since a new password was saved. - unset($_SESSION['pass_reset_'. $account->uid]); - } - // Clear the page cache because pages can contain usernames and/or profile information: - cache_invalidate(array('content' => TRUE)); - - drupal_set_message(t('The changes have been saved.')); -} - -/** * Submit function for the 'Cancel account' button on the user edit form. */ function user_edit_cancel_submit($form, &$form_state) { @@ -293,7 +215,8 @@ function user_edit_cancel_submit($form, &$form_state) { unset($_GET['destination']); } // Note: We redirect from user/uid/edit to user/uid/cancel to make the tabs disappear. - $form_state['redirect'] = array("user/" . $form['#user']->uid . "/cancel", array('query' => $destination)); + $account = $form_state['controller']->getEntity($form_state); + $form_state['redirect'] = array("user/" . $account->uid . "/cancel", array('query' => $destination)); } /**