Subversion Repositories web.active

Rev

Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * Handles import/export for ProcessField
 * 
 * ProcessWire 3.x, Copyright 2017 by Ryan Cramer
 * https://processwire.com
 *
 * @method InputfieldForm buildExport()
 * @method InputfieldForm buildImport()
 * @method InputfieldForm buildInputDataForm()
 * @method void processImport()
 * 
 */

class ProcessFieldExportImport extends Wire {
    
    public function __construct() {
        set_time_limit(600); 
    }

    /**
     * Return export data for all given $exportFields
     * 
     * @param array $exportFields field names
     * @return array
     * 
     */
    protected function getExportData(array $exportFields) {
        $data = array();
        foreach($this->wire('fields') as $field) {
            if(!in_array($field->name, $exportFields)) continue;
            $a = $field->getExportData();
            $data[$field->name] = $a;
        }
        return $data; 
    }
    
    /**
     * Execute export
     *
     * @return InputfieldForm
     *
     */
    public function ___buildExport() {

        /** @var InputfieldForm $form */
        $form = $this->wire('modules')->get('InputfieldForm');
        $form->action = './';
        $form->method = 'post';

        $exportFields = $this->wire('input')->post('export_fields');

        if(empty($exportFields)) {

            $f = $this->wire('modules')->get('InputfieldSelectMultiple');
            $f->attr('id+name', 'export_fields');
            $f->label = $this->_('Select the fields that you want to export');
            $f->icon = 'copy';

            $maxName = 0;
            $maxLabel = 0;
            $numFields = 0;
            
            foreach($this->wire('fields') as $field) {
                if(strlen($field->name) > $maxName) $maxName = strlen($field->name);
                $label = $field->getLabel();
                if(strlen($label) > $maxLabel) $maxLabel = strlen($label);
                $numFields++;
            }
        
            $fieldName = $this->_('NAME');
            $fieldLabel = $this->_('LABEL');
            $fieldType = $this->_('TYPE'); 
            
            $label = $fieldName . ' ' . str_repeat('.', $maxName - strlen($fieldName) + 3) . ' ' .
                $fieldLabel . str_repeat('.', $maxLabel - strlen($fieldLabel) + 3) . ' ' .
                $fieldType;
            
            $f->addOption(0, $label, array('disabled' => 'disabled'));
            
            foreach($this->wire('fields') as $field) {
                $fieldLabel = $field->getLabel();
                $label = $field->name . ' ' . str_repeat('.', $maxName - strlen($field->name) + 3) . ' ' .
                    $fieldLabel . str_repeat('.', $maxLabel - strlen($fieldLabel) + 3) . ' ' .
                    str_replace('Fieldtype', '', $field->type);
                $f->addOption($field->name, $label);
            }
            
            $f->notes = $this->_('Shift+Click to select multiple in sequence. Ctrl+Click (or Cmd+Click) to select multiple individually. Ctrl+A (or Cmd+A) to select all.');
            $f->attr('size', $numFields+1);
            $form->add($f);

            $f = $this->wire('modules')->get('InputfieldSubmit');
            $f->attr('name', 'submit_export');
            $f->attr('value', $this->_x('Export', 'button'));
            $form->add($f);

        } else {

            $form = $this->wire('modules')->get('InputfieldForm');
            $f = $this->wire('modules')->get('InputfieldTextarea');
            $f->attr('id+name', 'export_data');
            $f->label = $this->_('Export Data');
            $f->description = $this->_('Copy and paste this data into the "Import" box of another installation.');
            $f->notes = $this->_('Click anywhere in the box to select all export data. Once selected, copy the data with CTRL-C or CMD-C.');
            $f->attr('value', wireEncodeJSON($this->getExportData($exportFields), true, true));
            $form->add($f);

            $f = $this->wire('modules')->get('InputfieldButton');
            $f->href = './';
            $f->value = $this->_x('Ok', 'button'); 
            $form->add($f);
        }

        return $form;
    }

    /**
     * Build Textarea input form to pass JSON data into
     * 
     * @return InputfieldForm
     * 
     */
    protected function ___buildInputDataForm() {
    
        /** @var InputfieldForm $form */
        $form = $this->modules->get('InputfieldForm');
        $form->action = './';
        $form->method = 'post';
        $form->attr('id', 'import_form');
    
        /** @var InputfieldTextarea $f */
        $f = $this->modules->get('InputfieldTextarea');
        $f->attr('name', 'import_data');
        $f->label = $this->_x('Import', 'button');
        $f->icon = 'paste';
        $f->description = $this->_('Paste in the data from an export.');
        $f->description .= "\n**Experimental/beta feature: database backup recommended for safety.**";
        $f->notes = $this->_('Copy the export data from another installation and then paste into the box above with CTRL-V or CMD-V.');
        $form->add($f);

        /** @var InputfieldSubmit $f */
        $f = $this->wire('modules')->get('InputfieldSubmit') ;
        $f->attr('name', 'submit_import');
        $f->attr('value', $this->_('Preview'));
        $form->add($f);
        
        return $form; 
    }

    /**
     * Execute import
     *
     * @return InputfieldForm
     * @throws WireException if given invalid import data
     *
     */
    public function ___buildImport() {
    
        /** @var InputfieldForm $form */
        $form = $this->modules->get('InputfieldForm');
        $form->action = './';
        $form->method = 'post';
        $form->attr('id', 'import_form');

        if($this->input->post('submit_commit')) {
            $this->processImport();
            return $form;
        }
        
        $verify = (int) $this->input->get('verify');
        if($verify) {
            $json = $this->session->get('FieldImportData');
        } else {
            $json = $this->input->post('import_data');
        }

        if(!$json) return $this->buildInputDataForm();
        $data = is_array($json) ? $json : wireDecodeJSON($json);
        if(!$data) throw new WireException("Invalid import data");

        $numChangesTotal = 0;
        $numErrors = 0;
        $numExistingFields = 0;
        $numNewFields = 0;
        $notices = $this->wire('notices');
        
        if(!$verify) $notices->removeAll();

        // iterate through data for each field
        foreach($data as $name => $fieldData) {

            unset($fieldData['id']);
            $new = false;
            $name = $this->wire('sanitizer')->fieldName($name);
            $field = $this->wire('fields')->get($name);
            $numChangesField = 0;
            /** @var InputfieldFieldset $fieldset */
            $fieldset = $this->modules->get('InputfieldFieldset');
            $fieldset->label = $name;
            $form->add($fieldset);

            if(!$field) {
                $new = true;
                $field = new Field();
                $this->wire($field); 
                $field->name = $name;
                $fieldset->icon = 'sun-o';
                $fieldset->label .= " [" . $this->_('new') . "]";
            } else {
                $fieldset->icon = 'moon-o';
            }

            /** @var InputfieldMarkup $markup */
            $markup = $this->modules->get('InputfieldMarkup');
            $markup->addClass('InputfieldCheckboxes');
            $markup->value = "";
            $fieldset->add($markup);
            
            $savedFieldData = $field->getExportData();
            try {
                $changes = $field->setImportData($fieldData);
            } catch(\Exception $e) {
                $this->error($e->getMessage());
                $changes = array();
            }
            $field->setImportData($savedFieldData); // restore

            /** @var InputfieldCheckboxes $f */
            $f = $this->wire('modules')->get('InputfieldCheckboxes');
            $f->attr('name', "field_$name");
            $f->label = $this->_('Changes');
            $f->table = true;
            $f->thead = $this->_('Property') . '|';
            if(!$new) $f->thead .= $this->_('Old Value') . '|';
            $f->thead .= $this->_('New Value');

            foreach($changes as $property => $info) {
                
                if(!$new && $property == 'type') {
                    $this->error(sprintf($this->_('We recommend changing the type of this field to "%s" manually, then coming back here to apply additional changes.'), $info['new']));
                }
                
                $oldValue = str_replace('|', ' ', $info['old']);
                $newValue = str_replace('|', ' ', $info['new']);
                $numChangesField++;
                $numChangesTotal++;
                
                if($info['error']) {
                    $this->error("$name.$property: $info[error]");
                    $attr = array();
                } else {
                    $attr = array('checked' => 'checked');
                }
                
                if($new) $optionValue = "$property|$newValue";
                    else $optionValue = "$property|$oldValue|$newValue";
                
                $f->addOption($property, $optionValue, $attr);
            }

            $errors = array();
            foreach($notices as $notice) {
                if(!$notice instanceof NoticeError) continue;
                $errors[] = $this->wire('sanitizer')->entities1($notice->text);
            }
            
            if(count($errors)) {
                $icon = "<i class='fa fa-exclamation-triangle'></i>";
                $markup->value .= "<ul class='ui-state-error-text'><li>$icon " . implode("</li><li>$icon ", $errors) . '</li></ul>';
                $fieldset->label .= ' (' . sprintf($this->_n('%d error', '%d errors', count($errors)), count($errors)) . ')';
                $numErrors++;
            }
            
            if(!$verify) $notices->removeAll();

            if($numChangesField) {
                $fieldset->description = sprintf($this->_n('Found %d property to apply.', 'Found %d properties to apply.', $numChangesField), $numChangesField);
                if($new) $numNewFields++;
                    else $numExistingFields++;
            } else {
                $fieldset->description = $this->_('No changes pending.');
            }

            if(count($errors) || !$numChangesField) {
                $no = ' checked';
                $yes = '';
            } else {
                $yes = ' checked';
                $no = '';
            }

            $importLabel = $this->_('Modify this field?');
            if($new) $importLabel = $this->_('Create this field?');
            
            $markup->value .=
                "<p class='import_toggle'>$importLabel " .
                "<label><input$yes type='radio' name='import_field_$name' value='1' /> " . $this->_x('Yes', 'yes-import') . "</label>" .
                "<label><input$no type='radio' name='import_field_$name' value='0' /> " . $this->_x('No', 'no-import') . "</label>" .
                ($no && $numChangesField ? "<span class='detail'>(" . $this->_('click yes to show changes') . ")</span>" : "") .
                "</p>";

            $f->renderReady();
            $markup->value .= $f->render();
            $data[$name] = $fieldData;
        }

        if($numChangesTotal) {
            
            if($verify) {
                $form->description = $this->_('Sometimes it may take two commits before all changes are applied. Please review any pending changes below and commit them as needed.');
            } else {
                $form->description = $this->_('Please review the changes below and commit them when ready. If there are any changes that you do not want applied, uncheck the boxes where appropriate.');
            }
        
            /** @var InputfieldSubmit $f */
            $f = $this->modules->get('InputfieldSubmit');
            $f->attr('name', 'submit_commit');
            $f->attr('value', $this->_('Commit Changes'));
            $f->showInHeader();
            $form->add($f);
            
        } else { 
            
            if($verify) {
                $form->description = $this->_('Your changes have been applied!');
            } else {
                $form->description = $this->_('No changes were found');
            }
            
            $f = $this->modules->get('InputfieldButton');
            $f->href = './';
            $f->value = $this->_x('Ok', 'button'); 
            $form->add($f);
        }

        $this->session->set('FieldImportData', $data);
        if($numErrors) $this->error(sprintf($this->_n('Errors were found in %d field', 'Errors were found in %d fields', $numErrors), $numErrors));
        if($numNewFields) $this->message(sprintf($this->_n('Found %d new field to add', 'Found %d new fields to add', $numNewFields), $numNewFields));
        if($numExistingFields) $this->message(sprintf($this->_n('Found %d existing field to update', 'Found %d existing fields to update', $numExistingFields), $numExistingFields));
        
        return $form;
    }
    
    /**
     * Commit changed field data 
     *
     */
    protected function ___processImport() {

        $data = $this->session->get('FieldImportData');
        if(!$data) throw new WireException("Invalid import data");
        
        $numChangedFields = 0;
        $numAddedFields = 0;
        $skipFieldNames = array();

        // iterate through data for each field
        foreach($data as $name => $fieldData) {

            $name = $this->wire('sanitizer')->fieldName($name);
            
            if(!$this->input->post("import_field_$name")) {
                $skipFieldNames[] = $name;
                unset($data[$name]);
                continue;
            }
            
            $field = $this->wire('fields')->get($name);

            if(!$field) {
                $new = true;
                $field = new Field();
                $field->name = $name;
            } else {
                $new = false;
            }

            unset($fieldData['id']);
            foreach($fieldData as $property => $value) {
                if(!in_array($property, $this->input->post("field_$name"))) {
                    unset($fieldData[$property]); 
                }
            }
            
            try {
                $changes = $field->setImportData($fieldData);
                foreach($changes as $key => $info) $this->message($this->_('Saved:') . " $name.$key => $info[new]");
                $field->save();
                if($new) {
                    $numAddedFields++;
                    $this->message($this->_('Added field') . ' - ' . $name); 
                } else {
                    $numChangedFields++;
                    $this->message($this->_('Modified field') . ' - ' . $name); 
                }
            } catch(\Exception $e) {
                $this->error($e->getMessage());
            }
            
            $data[$name] = $fieldData;
        }
        
        $this->session->set('FieldImportSkipNames', $skipFieldNames);
        $this->session->set('FieldImportData', $data); 
        $numSkippedFields = count($skipFieldNames); 
        if($numAddedFields) $this->message(sprintf($this->_n('Added %d field', 'Added %d fields', $numAddedFields), $numAddedFields));
        if($numChangedFields) $this->message(sprintf($this->_n('Modified %d field', 'Modified %d fields', $numChangedFields), $numChangedFields));
        if($numSkippedFields) $this->message(sprintf($this->_n('Skipped %d field', 'Skipped %d fields', $numSkippedFields), $numSkippedFields));
        $this->session->redirect("./?verify=1");
    }


}