. */ /** * Forms panel. * * @package Admin\Form */ use Textpattern\Skin\Skin; use Textpattern\Skin\Form; if (!defined('txpinterface')) { die('txpinterface is undefined.'); } if ($event == 'form') { require_privs('form'); $instance = Txp::get('Textpattern\Skin\Form'); /** * List of essential forms. * * @global array $essential_forms */ $essential_forms = $instance->getEssential('name'); /** * List of form types. * * @global array $form_types */ $form_types = array(); foreach ($instance->getTypes() as $type) { $form_types[$type] = gTxt($type); } bouncer($step, array( 'form_edit' => false, 'form_create' => false, 'form_multi_edit' => true, 'form_save' => true, 'form_skin_change' => true, 'tagbuild' => false, )); switch (strtolower($step)) { case '': case 'form_edit': case 'form_create': form_edit(); break; case 'form_multi_edit': form_multi_edit(); break; case 'form_save': form_save(); break; case 'form_skin_change': $instance->selectEdit(); form_edit(); break; case 'tagbuild': echo form_tagbuild(); break; } } /** * Renders a list of form templates. * * This function returns a list of form templates, wrapped in a multi-edit * form widget. * * @param array $current The selected form info * @return string HTML */ function form_list($current) { global $essential_forms, $form_types, $txp_sections; $criteria = "skin = '" . doSlash($current['skin']) . "'"; $criteria .= callback_event('admin_criteria', 'form_list', 0, $criteria); $rs = safe_rows_start( "name, type", 'txp_form', "$criteria ORDER BY FIELD(type, ".quote_list(array_keys($form_types), ',').") ASC, name ASC" ); if ($rs) { $sections = array_keys(array_filter(array_column($txp_sections, 'skin', 'name'), function($v) use ($current) {return $v === $current['skin'];})); $sections = quote_list($sections, ','); $forms_in_use = !$sections ? array() : safe_column('override_form', 'textpattern', "override_form != '' AND Section IN($sections) GROUP BY override_form"); $prev_type = null; // Add a hidden field, in case only one skin is in use and multi-edit is the // first action performed. This way, the value is propagated and saved, even // if the skin select list is not rendered or a Form is not saved first. $out[] = hInput('skin', $current['skin']); while ($a = nextRow($rs)) { extract($a); $active = ($current['name'] === $name); if ($prev_type !== $type) { if ($prev_type !== null) { $group_out = tag(n.join(n, $group_out).n, 'ul', array('class' => 'switcher-list')); $label = isset($form_types[$prev_type]) ? $form_types[$prev_type] : $prev_type; $out[] = wrapRegion($prev_type.'_forms_group', $group_out, 'form_'.$prev_type, $label, 'form_'.$prev_type); } $prev_type = $type; $group_out = array(); } $editlink = eLink('form', 'form_edit', 'name', $name, $name); $in_use = isset($forms_in_use[$name]) ? sp.tag(gTxt('status_in_use'), 'small', array('class' => 'alert-block alert-pill success')) : ''; if (!in_array($name, $essential_forms)) { $modbox = span( checkbox('selected_forms[]', txpspecialchars($name), false), array('class' => 'switcher-action')); } else { $modbox = ''; } $group_out[] = tag(n.$modbox.$editlink.$in_use.n, 'li', array('class' => $active ? 'active' : '')); } if ($prev_type !== null) { $group_out = tag(n.join(n, $group_out).n, 'ul', array('class' => 'switcher-list')); $out[] = wrapRegion($prev_type.'_forms_group', $group_out, 'form_'.$prev_type, $form_types[$prev_type], 'form_'.$prev_type); } $out = tag(implode('', $out), 'div', array( 'id' => 'allforms_form_sections', 'role' => 'region', )); $methods = array( 'changetype' => array( 'label' => gTxt('changetype'), 'html' => formTypes('', false, 'changetype'), ), 'replace' => array( 'label' => gTxt('override'), 'html' => tag(gTxt('form'), 'label', array('for' => 'changeform')). form_pop($current['skin'], 'changeform'), ), 'delete' => gTxt('delete') ); $out .= multi_edit($methods, 'form', 'form_multi_edit'); return form($out, '', '', 'post', '', '', 'allforms_form'); } } /** * Processes multi-edit actions. */ function form_multi_edit() { $method = ps('edit_method'); $forms = ps('selected_forms'); $skin = ps('skin'); $affected = array(); $message = ''; $skin = Txp::get('Textpattern\Skin\Skin')->setName($skin)->setEditing(); if ($forms && is_array($forms)) { if ($method == 'delete') { $affected = form_delete($forms, $skin); callback_event('forms_deleted', '', 0, compact('affected', 'skin')); update_lastmod('form_deleted', $affected); $message = gTxt('form_deleted', array('{list}' => join(', ', $affected))); } elseif ($method == 'changetype') { $new_type = ps('type'); foreach ($forms as $name) { if (form_set_type($name, $new_type)) { $affected[] = $name; } } } elseif ($method == 'replace' && form_replace($forms, $skin, ps('override_form'))) { callback_event('forms_replaced', '', 0, compact('forms', 'skin')); update_lastmod('form_replaced', $forms); $message = gTxt('form_updated', array('{list}' => join(', ', $forms))); } } form_edit($message); } /** * Creates a new form. * * Directs requests back to the main editor panel, armed with a * 'form_create' step. */ function form_create() { form_edit(); } /** * Renders the main Form editor panel. * * @param string|array $message The activity message * @param bool $refresh_partials Whether to refresh partial contents */ function form_edit($message = '', $refresh_partials = false) { global $instance, $event, $step; /* $partials is an array of: $key => array ( 'mode' => {PARTIAL_STATIC | PARTIAL_VOLATILE | PARTIAL_VOLATILE_VALUE}, 'selector' => $DOM_selector or array($selector, $fragment, $script) of $DOM_selectors, 'cb' => $callback_function, 'html' => $return_value_of_callback_function (need not be initialised here) ) */ $partials = array( // Form list. 'list' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '#allforms_form_sections', 'cb' => 'form_list', ), // Name field. 'name' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => 'div.name', 'cb' => 'form_partial_name', ), // Name value. 'name_value' => array( 'mode' => PARTIAL_VOLATILE_VALUE, 'selector' => '#new_form,input[name=name]', 'cb' => 'form_partial_name_value', ), // Type field. 'type' => array( 'mode' => PARTIAL_VOLATILE, 'selector' => '.type', 'cb' => 'form_partial_type', ), // Type value. 'type_value' => array( 'mode' => PARTIAL_VOLATILE_VALUE, 'selector' => '[name=type]', 'cb' => 'form_partial_type_value', ), // Textarea. 'template' => array( 'mode' => PARTIAL_STATIC, 'selector' => 'div.template', 'cb' => 'form_partial_template', ), ); extract(array_map('assert_string', gpsa(array( 'copy', 'save_error', 'savenew', 'skin', )))); $name = assert_string(gps('name')); $type = assert_string(gps('type')); $newname = Form::sanitize(assert_string(gps('newname'))); $skin = ($skin !== '') ? $skin : null; $class = 'async'; $thisSkin = Txp::get('Textpattern\Skin\Skin'); $skin = $thisSkin->setName($skin)->setEditing(); if ($step == 'form_delete' || empty($name) && $step != 'form_create' && !$savenew) { $name = get_pref('last_form_saved', 'default'); } elseif ((($copy || $savenew) && $newname) && !$save_error) { $name = $newname; } elseif ((($newname && ($newname != $name)) || $step === 'form_create') && !$save_error) { $name = $newname; $class = ''; } elseif ($savenew && $save_error) { $class = ''; } $Form = gps('Form'); if (!$save_error) { if (!extract(safe_row('*', 'txp_form', "name = '".doSlash($name)."' AND skin = '" . doSlash($skin) . "'"))) { $name = ''; } } $actionsExtras = ''; if ($name) { $actionsExtras .= sLink('form', 'form_create', ' '.gTxt('create_form'), 'txp-new') .href(' '.gTxt('duplicate'), '#', array( 'class' => 'txp-clone', 'data-form' => 'form_form', )); } $actions = graf( $actionsExtras, array('class' => 'txp-actions txp-actions-inline') ); $skinBlock = n.$instance->setSkin($thisSkin)->getSelectEdit(); $buttons = graf( (!is_writable($instance->getDirPath()) ? '' : span( checkbox2('export', gps('export'), 0, 'export'). n.tag(gTxt('export_to_disk'), 'label', array('for' => 'export')) , array('class' => 'txp-save-export')) ). n.tag_void('input', array( 'class' => 'publish', 'type' => 'submit', 'method' => 'post', 'value' => gTxt('save'), )), ' class="txp-save"' ); $listActions = graf( href(' '.gTxt('expand_all'), '#', array( 'class' => 'txp-expand-all', 'aria-controls' => 'allforms_form', )). href(' '.gTxt('collapse_all'), '#', array( 'class' => 'txp-collapse-all', 'aria-controls' => 'allforms_form', )), array('class' => 'txp-actions') ); $rs = array( 'name' => $name, 'newname' => $newname, 'type' => $type, 'skin' => $skin, 'form' => $Form, ); // Get content for volatile partials. $partials = updatePartials($partials, $rs, array(PARTIAL_VOLATILE, PARTIAL_VOLATILE_VALUE)); if ($refresh_partials) { $response[] = announce($message); $response = array_merge($response, updateVolatilePartials($partials)); send_script_response(join(";\n", $response)); // Bail out. return; } // Get content for static partials. $partials = updatePartials($partials, $rs, PARTIAL_STATIC); pagetop(gTxt('tab_forms'), $message); echo n.'
'. n.tag( hed(gTxt('tab_forms').popHelp('forms_overview'), 1, array('class' => 'txp-heading')), 'div', array('class' => 'txp-layout-1col') ); // Forms create/switcher column. echo n.tag( $skinBlock.$listActions.n. $partials['list']['html'].n, 'div', array( 'class' => 'txp-layout-4col-alt', 'id' => 'content_switcher', 'role' => 'region', ) ); // Forms code column. echo n.tag( form( $actions. $partials['name']['html']. $partials['type']['html']. $partials['template']['html']. $buttons, '', '', 'post', $class, '', 'form_form'), 'div', array( 'class' => 'txp-layout-4col-3span', 'id' => 'main_content', 'role' => 'region', ) ); // Tag builder dialog placeholder. echo n.tag( ' ', 'div', array( 'class' => 'txp-tagbuilder-content', 'id' => 'tagbuild_links', 'aria-label' => gTxt('tagbuilder'), 'title' => gTxt('tagbuilder'), )); echo n.'
'; // End of .txp-layout. } /** * Saves a form template. */ function form_save() { global $essential_forms, $form_types, $app_mode, $instance; extract(doSlash(array_map('assert_string', psa(array( 'savenew', 'Form', 'type', 'copy', 'skin', ))))); $passedName = assert_string(ps('name')); $name = Form::sanitize($passedName); $newname = Form::sanitize(assert_string(ps('newname'))); $skin = Txp::get('Textpattern\Skin\Skin')->setName($skin)->setEditing(); $save_error = false; $message = ''; if (in_array($name, $essential_forms)) { $newname = $passedName = $name; $type = safe_field('type', 'txp_form', "name = '".doSlash($newname)."' AND skin = '".doSlash($skin)."'"); $_POST['newname'] = $newname; } if (!$newname) { $message = array(gTxt('form_name_invalid'), E_ERROR); $save_error = true; } else { if (!isset($form_types[$type])) { $message = array(gTxt('form_type_missing'), E_ERROR); $save_error = true; } else { if ($copy && $name === $newname) { $newname .= '_copy'; $passedName = $name; $_POST['newname'] = $newname; } $exists = safe_field("name", 'txp_form', "name = '".doSlash($newname)."' AND skin = '".doSlash($skin)."'"); if ($newname !== $name && $exists !== false) { $message = array(gTxt('form_already_exists', array('{name}' => $newname)), E_ERROR); if ($savenew) { $_POST['newname'] = ''; } $save_error = true; } else { $safe_skin = doSlash($skin); if ($savenew or $copy) { if ($newname) { if (safe_insert( 'txp_form', "Form = '$Form', type = '$type', skin = '$safe_skin', name = '".doSlash($newname)."'" )) { update_lastmod('form_created', compact('newname', 'name', 'type', 'Form')); $message = gTxt('form_created', array('{list}' => $newname)); // If form name has been auto-sanitized, throw a warning. if ($passedName !== $name) { $message = array($message, E_WARNING); } callback_event($copy ? 'form_duplicated' : 'form_created', '', 0, $name, $newname); } else { $message = array(gTxt('form_save_failed'), E_ERROR); $save_error = true; } } else { $message = array(gTxt('form_name_invalid'), E_ERROR); $save_error = true; } } else { if (safe_update( 'txp_form', "Form = '$Form', type = '$type', skin = '$safe_skin', name = '".doSlash($newname)."'", "name = '".doSlash($passedName)."' AND skin = '$safe_skin'" )) { update_lastmod('form_saved', compact('newname', 'name', 'type', 'Form')); $message = gTxt('form_updated', array('{list}' => $newname)); // If form name has been auto-sanitized, throw a warning. if ($passedName !== $name) { $message = array($message, E_WARNING); } callback_event('form_updated', '', 0, $name, $newname); } else { $message = array(gTxt('form_save_failed'), E_ERROR); $save_error = true; } } } } } if ($save_error === true) { $_POST['save_error'] = '1'; } else { if (gps('export')) { $instance->setNames(array($newname))->export()->getMessage(); } set_pref('last_form_saved', $newname, 'form', PREF_HIDDEN, 'text_input', 0, PREF_PRIVATE); callback_event('form_saved', '', 0, $name, $newname); } form_edit($message, ($app_mode === 'async') ? true : false); } /** * Deletes a form template with the given name. * * @param string $name The form template * @param string $skin The form skin in use * @return bool FALSE on error */ function form_delete($name, $skin) { global $prefs, $essential_forms, $txp_sections; $sections = quote_list(array_keys(array_filter(array_column($txp_sections, 'skin', 'name'), function($v) use ($skin) {return $v === $skin;})), ','); $last_form = get_pref('last_form_saved'); $skin = doSlash($skin); $deleted = array(); foreach ((array)$name as $form) { if ($form == '' || in_array($form, $essential_forms)) { continue; } elseif ($form === $last_form) { unset($prefs['last_form_saved']); remove_pref('last_form_saved', 'form'); } $safe_form = doSlash($form); if (safe_delete("txp_form", "name = '$safe_form' AND skin = '$skin'")) { $deleted[] = $form; } } if ($sections && $deleted) { $safe_form = quote_list($deleted, ','); safe_update('textpattern', "override_form=''", "override_form IN($safe_form) AND Section IN($sections)"); } return is_array($name) ? $deleted : !empty($deleted); } /** * Replaces a form template in articles. * * @param string $name The form template * @param string $skin The form skin in use * @param string $newform The form skin in use * @return bool FALSE on error */ function form_replace($name, $skin, $newform = '') { global $txp_sections; $sections = quote_list(array_keys(array_filter(array_column($txp_sections, 'skin', 'name'), function($v) use ($skin) {return $v === $skin;})), ','); if (!$sections) { return; } $forms = quote_list((array)$name, ','); $newform = doSlash($newform); return safe_update('textpattern', "override_form='$newform'", "override_form IN($forms) AND Section IN($sections)"); } /** * Changes the skin in which styles are being edited. * * Keeps track of which skin is being edited from panel to panel. * * @param string $skin Optional skin name. Read from GET/POST otherwise * @deprecated in 4.7.0 */ function form_skin_change($skin = null) { Txp::get('Textpattern\Skin\Form')->selectEdit($skin); return true; } /** * Changes a form template's type. * * @param string $name The form template * @param string $type The new type * @return bool FALSE on error */ function form_set_type($name, $type) { global $essential_forms, $form_types; if (in_array($name, $essential_forms) || !isset($form_types[$type])) { return false; } $name = doSlash($name); $type = doSlash($type); $skin = doSlash(get_pref('skin_editing', 'default')); return safe_update('txp_form', "type = '$type'", "name = '$name' AND skin = '$skin'"); } /** * Renders a <select> input listing all form types. * * @param string $type The selected option * @param bool $blank_first If TRUE, the list defaults to an empty selection * @param string $id HTML id attribute value * @param bool $disabled If TRUE renders the select disabled * @return string HTML * @access private */ function formTypes($type, $blank_first = true, $id = 'type', $disabled = false) { global $form_types; return selectInput('type', $form_types, $type, $blank_first, '', $id, false, $disabled); } /** * Renders 'override form' field. * * @param string $id HTML id to apply to the input control * @param string $skin The theme that is currently in use * @return string HTML <select> input */ function form_pop($skin, $id) { static $skinforms = null; if (!isset($skinforms)) { $form_types = get_pref('override_form_types', 'article'); $safe_skin = doSlash($skin); $rs = safe_column('name', 'txp_form', "type IN ('".implode("','", do_list($form_types))."') AND name != 'default' AND skin='$safe_skin' ORDER BY name"); $skinforms = $rs ? array_combine($rs, $rs) : false; } if ($skinforms) { return selectInput('override_form', $skinforms, '', true, '', $id); } } /** * Renders form name field. * * @param array $rs Record set * @return string HTML */ function form_partial_name($rs) { global $essential_forms, $form_types; $name = $rs['name']; $skin = $rs['skin']; $type = $rs['type']; $nameRegex = '^(?=[^.\s])[^\x00-\x1f\x22\x26\x27\x2a\x2f\x3a\x3c\x3e\x3f\x5c\x7c\x7f]+'; if (in_array($name, $essential_forms) || $type && !isset($form_types[$type])) { $nameInput = fInput('text', array('name' => 'newname', 'pattern' => $nameRegex), $name, 'input-medium', '', '', INPUT_MEDIUM, '', 'new_form', true); } else { $nameInput = fInput('text', array('name' => 'newname', 'pattern' => $nameRegex), $name, 'input-medium', '', '', INPUT_MEDIUM, '', 'new_form', false, true); } $name_widgets = inputLabel( 'new_form', $nameInput, 'form_name', array('', 'instructions_form_name'), array('class' => 'txp-form-field name') ); if ($name === '') { $name_widgets .= hInput('savenew', 'savenew'); } else { $name_widgets .= hInput('name', $name); } $name_widgets .= hInput('skin', $skin). eInput('form').sInput('form_save'); return $name_widgets; } /** * Renders form type field. * * @param array $rs Record set * @return string HTML */ function form_partial_type($rs) { global $essential_forms, $form_types; $name = $rs['name']; $type = $rs['type']; $type_widgets = ''; if ($type && !isset($form_types[$type])) { $typeInput = tag_void('input', array( 'id' => 'types', 'name' => 'type', 'type' => 'text', 'value' => $type, 'disabled' => true )); $type_widgets .= hInput('type', $type); } elseif (in_array($name, $essential_forms)) { $typeInput = formTypes($type, false, 'type', true); $type_widgets .= hInput('type', $type); } else { $typeInput = formTypes($type, false); } $type_widgets .= inputLabel( 'type', $typeInput, 'form_type', array('', 'instructions_form_type'), array('class' => 'txp-form-field type') ); return $type_widgets; } /** * Renders form name value. * * @param array $rs Record set * @return string HTML */ function form_partial_name_value($rs) { return $rs['name']; } /** * Renders form type value. * * @param array $rs Record set * @return string HTML */ function form_partial_type_value($rs) { return $rs['type']; } /** * Renders form textarea field. * * @param array $rs Record set * @return string HTML */ function form_partial_template($rs) { global $event; $out = inputLabel( 'form', '', array( 'form_code', n.span( (has_privs('tag') ? href( span(null, array('class' => 'ui-icon ui-extra-icon-code')).' '.gTxt('tagbuilder'), array('event' => 'tag', 'panel' => $event), array('class' => 'txp-tagbuilder-dialog') ) : '' ), array('class' => 'txp-textarea-options') ) ), array('', 'instructions_form_code'), array('class' => 'txp-form-field template'), array('div', 'div') ); return $out; }