1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-25 15:51:37 +02:00

Continued major updates to the main PagesExportImport class

This commit is contained in:
Ryan Cramer
2017-08-11 10:45:46 -04:00
parent d000117e4b
commit af6ab99d87
2 changed files with 699 additions and 159 deletions

View File

@@ -154,7 +154,7 @@ class PagesExportImport extends Wire {
foreach($items as $item) { foreach($items as $item) {
$exportItem = $this->pageToArray($item, $options); $exportItem = $this->pageToArray($item, $options);
$a['pages'][] = $exportItem; $a['pages'][$exportItem['path']] = $exportItem;
// include information about field settings so that warnings can be generated at // include information about field settings so that warnings can be generated at
// import time if there are applicable differences in the field settings // import time if there are applicable differences in the field settings
@@ -170,6 +170,7 @@ class PagesExportImport extends Wire {
} else { } else {
$a['fields'][$fieldName] = array( $a['fields'][$fieldName] = array(
'type' => $field->type->className(), 'type' => $field->type->className(),
'label' => $field->label,
'version' => $moduleInfo['versionStr'] 'version' => $moduleInfo['versionStr']
); );
} }
@@ -193,6 +194,10 @@ class PagesExportImport extends Wire {
} }
} }
// sort by path to ensure parents are created before their children
ksort($a['pages']);
$a['pages'] = array_values($a['pages']);
if($options['verbose']) $a['templates'] = $templates; if($options['verbose']) $a['templates'] = $templates;
if($languages) $languages->unsetDefault(); if($languages) $languages->unsetDefault();
@@ -256,25 +261,36 @@ class PagesExportImport extends Wire {
'template' => $page->template->name, 'template' => $page->template->name,
'settings' => $settings, 'settings' => $settings,
'data' => array(), 'data' => array(),
'warnings' => array(), // 'warnings' => array(),
);
$exportValueOptions = array(
'system' => true,
'caller' => $this,
'FieldtypeFile' => array(
'noJSON' => true
),
'FieldtypeImage' => array(
'variations' => true,
),
); );
// iterate all fields and export value from each // iterate all fields and export value from each
foreach($page->template->fieldgroup as $field) { foreach($page->template->fieldgroup as $field) {
/** @var Field $field */
if(!empty($options['fieldNames']) && !in_array($field->name, $options['fieldNames'])) continue; if(!empty($options['fieldNames']) && !in_array($field->name, $options['fieldNames'])) continue;
$info = $this->getFieldInfo($field); $info = $this->getFieldInfo($field);
if(!$info['exportable']) { if(!$info['exportable']) {
$a['warnings'][$field->name] = $info['reason']; // $a['warnings'][$field->name] = $info['reason'];
// $this->warning("Field '$field->name' - $info[reason]");
continue; continue;
} }
/** @var Field $field */
/** @var Fieldtype $fieldtype */
$value = $page->getUnformatted($field->name); $value = $page->getUnformatted($field->name);
$exportValue = $field->type->exportValue($page, $field, $value, array('system' => true)); $exportValue = $field->type->exportValue($page, $field, $value, $exportValueOptions);
$this->message($exportValue); // $this->message($exportValue);
$a['data'][$field->name] = $exportValue; $a['data'][$field->name] = $exportValue;
} }
@@ -313,6 +329,9 @@ class PagesExportImport extends Wire {
if(empty($a['pages'])) return $options['count'] ? 0 : $pageArray; if(empty($a['pages'])) return $options['count'] ? 0 : $pageArray;
// @todo generate warnings from this import info
$info = $this->getImportInfo($a);
foreach($a['pages'] as $item) { foreach($a['pages'] as $item) {
$page = $this->arrayToPage($item, $options); $page = $this->arrayToPage($item, $options);
$e = $page->errors('string clear'); $e = $page->errors('string clear');
@@ -344,11 +363,14 @@ class PagesExportImport extends Wire {
* - `fieldNames` (array): Import only these field names, or omit to use all import data (default=[]). * - `fieldNames` (array): Import only these field names, or omit to use all import data (default=[]).
* - `changeStatus` (bool): Allow status to be changed aon existing pages? (default=true) * - `changeStatus` (bool): Allow status to be changed aon existing pages? (default=true)
* - `changeSort` (bool): Allow sort and sortfield to be changed on existing pages? (default=true) * - `changeSort` (bool): Allow sort and sortfield to be changed on existing pages? (default=true)
* - `replaceTemplates` (array): Array of import-data template name to replacement template name (default=[])
* - `replaceFields` (array): Array of import-data field name to replacement field name (default=[])
* *
* The following options are for future use and not currently applicable: * The following options are for future use and not currently applicable:
* - `changeTemplate` (bool): Allow template to be changed on existing pages? (default=false) * - `changeTemplate` (bool): Allow template to be changed on existing pages? (default=false)
* - `changeParent` (bool): Allow parent to be changed on existing pages? (default=false) * - `changeParent` (bool): Allow parent to be changed on existing pages? (default=false)
* - `changeName` (bool): Allow name to be changed on existing pages? (default=false) * - `changeName` (bool): Allow name to be changed on existing pages? (default=false)
* - `replaceParents` (array): Array of import-data parent path to replacement parent path (default=[])
* *
* @param array $a * @param array $a
* @param array $options Options to modify default behavior, see method description. * @param array $options Options to modify default behavior, see method description.
@@ -375,7 +397,11 @@ class PagesExportImport extends Wire {
'changeSort' => true, 'changeSort' => true,
'saveOptions' => array('adjustName' => true), // options passed to Pages::save 'saveOptions' => array('adjustName' => true), // options passed to Pages::save
'fieldNames' => array(), // import only these field names, when specified 'fieldNames' => array(), // import only these field names, when specified
'replaceFields' => array(), // array of import-data field name to replacement page field name
'replaceTemplates' => array(), // array of import-data template name to replacement page template name
'replaceParents' => array(), // array of import-data parent path to replacement parent path
'commit' => true, // commit the import? If false, changes aren't saved (dry run). 'commit' => true, // commit the import? If false, changes aren't saved (dry run).
'debug' => false,
); );
$options = array_merge($defaults, $options); $options = array_merge($defaults, $options);
@@ -383,9 +409,8 @@ class PagesExportImport extends Wire {
$warnings = array(); // non-fatal warnings $warnings = array(); // non-fatal warnings
$messages = array(); // informational $messages = array(); // informational
$pages = $this->wire('pages'); $pages = $this->wire('pages');
$path = $a['path'];
$languages = $this->wire('languages'); $languages = $this->wire('languages');
$fileFields = array(); $missingFields = array();
if($options['id']) { if($options['id']) {
$options['update'] = true; $options['update'] = true;
@@ -395,7 +420,186 @@ class PagesExportImport extends Wire {
/** @var Languages $languages */ /** @var Languages $languages */
if($languages) $languages->setDefault(); if($languages) $languages->setDefault();
// determine parent and template
$page = $this->importGetPage($a, $options, $errors);
$parent = $page->id ? $page->parent : $this->importGetParent($a, $options, $errors);
$template = $page->id ? $page->template : $this->importGetTemplate($a, $options, $errors);
$isNew = $page->id == 0 && !$page instanceof NullPage;
$page->setTrackChanges(true);
$page->setQuietly('_importPath', $a['path']);
$page->setQuietly('_importType', $isNew ? 'create' : 'update');
$page->setQuietly('_importTemplate', $template);
$page->setQuietly('_importParent', $parent);
$page->setQuietly('_importOriginalID', $a['settings']['id']); // original/external ID
// if any errors occurred above, abort
if(count($errors) && !$page instanceof NullPage) $page = new NullPage();
// if we were only able to create a NullPage, abort now
if($page instanceof NullPage) {
foreach($errors as $error) $page->error($error);
if($languages) $languages->unsetDefault();
return $page;
}
$page->of(false);
$this->importPageSettings($page, $a['settings'], $options);
$changes = $page->getChanges();
// save blank page now if it is new, so that it has an ID
if($isNew && $options['commit']) {
$pages->save($page, $options['saveOptions']);
}
// populate custom fields
foreach($a['data'] as $name => $value) {
if(count($options['fieldNames']) && !in_array($name, $options['fieldNames'])) continue;
if(isset($options['replaceFields'][$name])) $name = $options['replaceFields'][$name];
$field = $this->wire('fields')->get($name);
if(!$field) {
if(is_array($value) && !count($value)) continue;
if(!is_array($value) && !strlen($value)) continue;
$missingFields[$name] = $name;
continue;
}
$fieldInfo = $this->getFieldInfo($field);
if(!$fieldInfo['exportable']) {
// field cannot be imported
$warnings[] = $fieldInfo['reason'];
} else {
// proceed with import of field
try {
$this->importFieldValue($page, $field, $value, $options);
} catch(\Exception $e) {
$warnings[] = $e->getMessage();
}
}
}
if(count($missingFields)) {
$warnings[] = "Skipped fields (not found): " . implode(', ', $missingFields);
}
$changes = array_unique(array_merge($changes, $page->getChanges()));
if($options['commit']) {
$pages->save($page, $options['saveOptions']);
}
if($languages) $languages->unsetDefault();
foreach($errors as $error) $page->error($error);
foreach($warnings as $warning) $page->warning($warning);
foreach($messages as $message) $page->message($message);
$page->setQuietly('_importChanges', $changes);
$page->setQuietly('_importMissingFields', $missingFields);
return $page;
}
/**
* Get the page to import to
*
* @param array $a Import data
* @param array $options Import settings
* @param array $errors Errors array
* @return NullPage|Page
*
*/
protected function importGetPage(array &$a, array &$options, array &$errors) {
/** @var Pages $pages */
$pages = $this->wire('pages');
$path = $a['path'];
/** @var Page|NullPage $page */
if(!empty($options['id'])) {
$page = $pages->get((int) $options['id']);
if(!$page->id) {
$errors[] = "Unable to find specified page to update by ID: $options[id]";
}
} else {
if(isset($a['_importToID'])) {
// if provided with ID added by getImportInfo() method
$id = (int) $a['_importToID'];
$page = $id ? $pages->get($id) : new NullPage();
} else {
$page = $pages->get($path);
}
if($page->id && !$options['update']) {
// create new page rather than updating existing page
$errors[] = "Skipped update to existing page because update option is disabled";
} else if($page->id) {
// update of existing page allowed
} else if(!$options['create']) {
// creation of new pages is not allowed
$errors[] = "Skipped create of new page because create option is disabled";
} else if(wireClassExists($a['class'])) {
// use specified class
$page = new $a['class']();
} else {
// requested page class does not exist (warning?)
$warnings[] = "Unable to locate Page class '$a[class]', using Page class instead";
$page = new Page();
}
}
return $page;
}
/**
* Get the Page Template to use for import
*
* @param array $a Import data
* @param array $options Import options
* @param array $errors Errors array
* @return Template|null
*
*/
protected function importGetTemplate(array &$a, array &$options, array &$errors) {
$template = empty($options['template']) ? $a['template'] : $options['template'];
$name = is_object($template) ? $template->name : $template;
if(isset($options['replaceTemplates'][$name])) $template = $options['replaceTemplates'][$name];
$_template = $template;
if(is_object($template)) {
// ok
} else {
$template = $this->wire('templates')->get($template);
}
if($template) {
$options['template'] = $template;
$a['template'] = (string) $template;
} else {
$errors[] = "Unable to locate template: $_template";
}
return $template;
}
/**
* Get the parent of the page being imported
*
* @param array $a Import data
* @param array $options Import options
* @param array $errors Errors array
* @return Page|NullPage
*
*/
protected function importGetParent(array &$a, array &$options, array &$errors) {
// determine parent // determine parent
static $previousPaths = array();
$usePrevious = true;
$pages = $this->wire('pages');
$path = $a['path'];
if($options['parent']) { if($options['parent']) {
// parent specified in options // parent specified in options
if(is_object($options['parent']) && $options['parent'] instanceof Page) { if(is_object($options['parent']) && $options['parent'] instanceof Page) {
@@ -417,8 +621,24 @@ class PagesExportImport extends Wire {
$parts = explode('/', trim($path, '/')); $parts = explode('/', trim($path, '/'));
array_pop($parts); // pop off name array_pop($parts); // pop off name
$parentPath = '/' . implode('/', $parts); $parentPath = '/' . implode('/', $parts);
if(strlen($parentPath) > 1) $parentPath .= '/';
if(isset($options['replaceParents'][$parentPath])) {
$parentPath = $options['replaceParents'][$parentPath];
}
$parent = $pages->get($parentPath); $parent = $pages->get($parentPath);
if(!$parent->id) $errors[] = "Unable to locate parent page: $parentPath"; if(!$parent->id) {
$foundParent = false;
if(!$options['commit']) {
// check if the parent will be created by the import
if(isset($previousPaths[$parentPath])) {
$foundParent = true;
}
}
if(!$foundParent) {
$errors[] = "Unable to locate parent page: $parentPath";
$usePrevious = false;
}
}
} else if($path === '/') { } else if($path === '/') {
// homepage, parent is not applicable // homepage, parent is not applicable
$parent = new NullPage(); $parent = new NullPage();
@@ -428,121 +648,146 @@ class PagesExportImport extends Wire {
$errors[] = "Unable to determine parent"; $errors[] = "Unable to determine parent";
} }
// determine template if($parent->id) {
$template = empty($options['template']) ? $a['template'] : $options['template']; $options['parent'] = $parent;
if(!is_object($template)) {
$_template = $template;
$template = $this->wire('templates')->get($template);
if(!$template) $errors[] = "Unable to locate template: $_template";
} }
// determine page (new or existing) if($usePrevious){
/** @var Page|NullPage $page */ $key = rtrim($path, '/');
if(!empty($options['id'])) { if($key) $previousPaths[$path] = true;
$page = $pages->get((int) $options['id']);
if(!$page->id) {
$errors[] = "Unable to find specified page to update by ID: $options[id]";
}
} else {
$page = $pages->get($path);
if($page->id && !$options['update']) {
// create new page rather than updating existing page
$errors[] = "Skipped update to existing page because update option is disabled";
} else if($page->id) {
// update of existing page allowed
} else if(!$options['create']) {
// creation of new pages is not allowed
$errors[] = "Skipped create of new page because create option is disabled";
} else if(wireClassExists($a['class'])) {
// use specified class
$page = new $a['class']();
} else {
// requested page class does not exist (warning?)
$warnings[] = "Unable to locate Page class “$a[class]”, using Page class instead";
$page = new Page();
}
} }
$isNew = $page->id == 0; return $parent;
$page->setTrackChanges(true);
$page->setQuietly('_importPath', $a['path']);
$page->setQuietly('_importType', $isNew ? 'create' : 'update');
// if any errors occurred above, abort
if(count($errors) && !$page instanceof NullPage) $page = new NullPage();
// if we were only able to create a NullPage, abort now
if($page instanceof NullPage) {
foreach($errors as $error) $page->error($error);
return $page;
} }
/**
* Import native page settings
*
* @param Page $page
* @param array $settings Contents of the import data 'settings' array
* @param array $options
*
*/
protected function importPageSettings(Page $page, array $settings, array $options) {
$isNew = $page->get('_importType') == 'create';
// we don't currently allow template changes on existing pages // we don't currently allow template changes on existing pages
if(!$isNew) $options['changeTemplate'] = false; if(!$isNew) $options['changeTemplate'] = false;
$template = $options['template'];
$parent = $options['parent'];
$languages = $this->wire('languages');
$langProperties = array();
// populate page base settings // populate page base settings
$page->of(false); if($options['changeTemplate'] || $isNew) {
if($options['changeTemplate'] || $isNew) if($page->template->name != $template->name) $page->template = $template; if(!$page->template || $page->template->name != $template->name) $page->template = $template;
if($options['changeParent'] || $isNew) if($page->parent->id != $parent->id) $page->parent = $parent; }
if($options['changeName'] || $isNew) if($page->name != $a['settings']['name']) $page->name = $a['settings']['name']; if($options['changeParent'] || $isNew) {
if($options['changeStatus'] || $isNew) if($page->status != $a['settings']['status']) $page->status = $a['settings']['status']; if($parent && $page->parent->id != $parent->id) $page->parent = $parent;
}
if($options['changeStatus'] || $isNew) {
if($page->status != $settings['status']) $page->status = $settings['status'];
$langProperties[] = 'status';
}
if($options['changeName'] || $isNew) {
if($page->name != $settings['name']) $page->name = $settings['name'];
$langProperties[] = 'name';
}
if($options['changeSort'] || $isNew) { if($options['changeSort'] || $isNew) {
if($page->sort != $a['settings']['sort']) $page->sort = $a['settings']['sort']; if($page->sort != $settings['sort']) $page->sort = $settings['sort'];
if($page->sortfield != $a['settings']['sortfield']) $page->sortfield = $a['settings']['sortfield']; if($page->sortfield != $settings['sortfield']) $page->sortfield = $settings['sortfield'];
} }
$changes = $page->getChanges(); if($languages && count($langProperties)) {
foreach($langProperties as $property) {
// save blank page now if it is new, so that it has an ID foreach($languages as $language) {
if($isNew && $options['commit']) { if($language->isDefault()) continue;
$pages->save($page, $options['saveOptions']); $remoteKey = "{$property}_$language->name";
$localKey = "{$property}$language->id";
if(!isset($settings[$remoteKey])) continue;
if($settings[$remoteKey] != $page->get($localKey)) {
$page->set($localKey, $settings[$remoteKey]);
} }
// populate custom fields
foreach($page->template->fieldgroup as $field) {
if(count($options['fieldNames']) && !in_array($field->name, $options['fieldNames'])) continue;
$fieldInfo = $this->getFieldInfo($field);
if(!$fieldInfo['exportable']) {
$warnings[] = $fieldInfo['reason'];
continue;
} }
if(!isset($a['data'][$field->name])) {
$warnings[] = "Skipped field “$field->name” - template “$template” does not have it";
continue;
} else if($field->type instanceof FieldtypeFile) {
$fileFields[] = $field;
continue;
} }
try {
$value = $field->type->importValue($page, $field, $a['data'][$field->name], array('system' => true));
$page->set($field->name, $value);
} catch(\Exception $e) {
$warnings[] = $e->getMessage();
} }
} }
// handle file fields /**
if(count($fileFields)) { * Import value for a single field
foreach($fileFields as $field) { *
$this->importFileField($page, $field, $a['data'][$field->name], $options); * @param Page $page
* @param Field $field
* @param array|string|int|float $importValue
* @param array $options Looks only at 'commit' option to determine when testing
*
*/
protected function importFieldValue(Page $page, Field $field, $importValue, array $options) {
if($field->type instanceof FieldtypeFile) {
// file fields (cannot be accessed until page exists)
if($page->id) {
return $this->importFileFieldValue($page, $field, $importValue, $options);
} else if(!empty($importValue)) {
$page->trackChange($field->name);
} }
} }
$changes = array_unique(array_merge($changes, $page->getChanges())); $o = array(
'system' => true,
'caller' => $this,
'test' => !$options['commit']
);
if($options['commit']) { // fake-commit for more verbose testing of certain fieldtypes
$pages->save($page, $options['saveOptions']); $fakeCommit = false;
if(!$options['commit']) {
// we fake-commit Page refs so that validity is tested and errors known before commit
if($field->type instanceof FieldtypePage) $fakeCommit = true;
} }
if($languages) $languages->unsetDefault(); if($page->get('_importType') == 'create' && !$options['commit'] && !$fakeCommit) {
// test import on a new page, so value will always be used
$page->trackChange($field->name);
return;
}
foreach($errors as $error) $page->error($error); $pageValue = $page->getUnformatted($field->name);
foreach($warnings as $warning) $page->warning($warning); $exportValue = $pageValue === null ? null : $field->type->exportValue($page, $field, $pageValue, $o);
foreach($messages as $message) $page->message($message);
$page->setQuietly('_importChanges', $changes); if(is_array($importValue) && is_array($exportValue)) {
// use regular '==' only for array comparisons
if($exportValue == $importValue) return;
} else {
// use '===' for all other value comparisons
if($exportValue === $importValue) return;
}
return $page; if($options['commit'] || $fakeCommit) {
$pageValue = $field->type->importValue($page, $field, $importValue, $o);
if($pageValue !== null) $page->set($field->name, $pageValue);
if(is_object($pageValue) && $pageValue instanceof Wire) {
// copy notices from the pageValue to the page
foreach(array('errors', 'warnings', 'messages') as $noticeType) {
foreach($pageValue->$noticeType('clear') as $notice) {
$method = rtrim($noticeType, 's');
$page->$method($notice->text);
$this->warning($notice->text);
}
}
}
} else {
// test import on existing page, avoids actually setting value to the page
$page->trackChange($field->name); // values appear to be different
}
if($options['debug']) {
if(is_string($exportValue)) $exportValue = strlen($exportValue) . " bytes\n" . $exportValue;
if(is_string($importValue)) $importValue = strlen($importValue) . " bytes\n" . $importValue;
$this->message("$field->name OLD: <pre>" . htmlentities(print_r($exportValue, true)) . "</pre>", Notice::allowMarkup);
$this->message("$field->name NEW: <pre>" . htmlentities(print_r($importValue, true)) . "</pre>", Notice::allowMarkup);
}
} }
/** /**
@@ -550,32 +795,320 @@ class PagesExportImport extends Wire {
* *
* @param Page $page * @param Page $page
* @param Field $field * @param Field $field
* @param array $data Export/sleep value of file field * @param array $data Export value of file field
* @param array $options * @param array $options
* *
*/ */
protected function importFileField(Page $page, Field $field, array $data, array $options = array()) { protected function importFileFieldValue(Page $page, Field $field, array $data, array $options = array()) {
// Expected format of given $data argument: // Expected format of given $data argument:
// $data = [ // $data = [
// 'file1.jpg' => [ // 'file1.jpg' => [
// 'url' => 'http://domain.com/site/assets/files/123/file1.jpg', // 'url' => 'http://domain.com/site/assets/files/123/file1.jpg',
// 'description' => 'file description', // 'description' => 'file description',
// 'tags' => 'file tags' // 'tags' => 'file tags',
// 'variations' => [ 'file1.260x0.jpg' => 'http://domain.com/site/assets/files/123/file1.260x0.jpg' ]
// ], // ],
// 'file2.png' => [ ... see above ... ], // 'file2.png' => [ ... see above ... ],
// 'file3.gif' => [ ... see above ... ], // 'file3.gif' => [ ... see above ... ],
// ]; // ];
// @todo method needs implementation /** @var Pagefiles $pagefiles */
$pagefiles = $page->get($field->name);
$page->warning("File field '$field->name' not yet supported"); if(!$pagefiles || !$pagefiles instanceof Pagefiles) {
$page->warning("Unable to import files to field '$field->name' because it is not a files field");
if($options['commit']) { return;
} else {
// do not commit
} }
$filesAdded = array();
$filesUpdated = array();
$filesRemoved = array();
$filesDownloaded = array();
$maxFiles = (int) $field->get('maxFiles');
$languages = $this->wire('languages');
$filesPath = $pagefiles->path();
/** @var WireHttp $http */
$http = $this->wire(new WireHttp());
foreach($data as $fileName => $fileInfo) {
/** @var Pagefile $pagefile */
$pagefile = $pagefiles->get($fileName);
$isNew = false;
if(!$pagefile) {
// new file, needs to be added
$isNew = true;
try {
if($options['commit']) {
$pagefiles->add($fileInfo['url']);
$pagefile = $pagefiles->last();
if(!$pagefile) throw new WireException("Unable to add file $fileInfo[url]");
if($maxFiles === 1 && $pagefiles->count() > 1) {
$pagefiles->remove($pagefiles->first()); // file replacement
}
} else {
$pagefile = null;
}
$filesAdded[] = $fileName;
} catch(\Exception $e) {
$page->warning($e->getMessage());
$pagefile = null;
}
if(!$pagefile) continue;
}
$pagefile->setTrackChanges(true);
$variations = array();
// description, tags, etc.
foreach($fileInfo as $key => $value) {
if($key == 'url') continue;
if($key == 'variations') {
$variations = $value;
continue;
}
if($key == 'description') {
$oldValue = $languages ? $pagefile->description(true, true) : $pagefile->get('description');
} else {
$oldValue = $pagefile->get($key);
}
if($value == $oldValue) {
continue; // no differences
}
if(empty($value) && empty($oldValue)) {
continue; // no differences
}
if($key == 'description') {
$pagefile->description($value);
if(!$pagefile->isChanged($key)) continue;
} else if($options['commit']) {
$pagefile->set($key, $value);
if(!$pagefile->isChanged($key)) continue;
}
if(!isset($filesUpdated[$key])) $filesUpdated[$key] = array();
if(!$isNew) {
$filesUpdated[$key][] = $fileName;
if($options['debug']) {
$this->message("$field->name: $pagefile->name ($key) OLD: <pre>" .
print_r($oldValue, true) . "</pre>", Notice::allowMarkup);
$this->message("$field->name: $pagefile->name ($key) NEW: <pre>" .
print_r($value, true) . "</pre>", Notice::allowMarkup);
}
}
}
if(count($variations)) {
foreach($variations as $name => $url) {
$targetFile = $filesPath . $name;
if(!file_exists($targetFile)) {
try {
if($options['commit']) $http->download($url, $targetFile);
$filesDownloaded[] = $name;
} catch(\Exception $e) {
$page->warning("Error downloading file $url - " . $e->getMessage());
}
}
}
}
}
// determine removed files
foreach($pagefiles as $pagefile) {
if(isset($data[$pagefile->name])) continue;
$filesRemoved[] = $pagefile->name;
if($options['commit']) $pagefiles->remove($pagefile);
}
$numAdded = count($filesAdded);
$numUpdated = count($filesUpdated);
$numRemoved = count($filesRemoved);
$numDownloaded = count($filesDownloaded);
$numTotal = $numAdded + $numUpdated + $numRemoved; // intentionally excludes numDownloaded
if($numTotal > 0) {
$pagefiles->trackChange('value');
if($options['commit']) $page->set($field->name, $pagefiles);
$page->trackChange($field->name);
if($numAdded) $page->message("$field->name: " .
sprintf($this->_n('Added %d file', 'Added %d files', $numAdded), $numAdded) . ": " .
implode(', ', $filesAdded)
);
if($numUpdated) {
foreach($filesUpdated as $property => $files) {
$numFiles = count($files);
$page->message("$field->name: " .
sprintf($this->_n('Updated %s for %d file', 'Updated %s for %d files', $numFiles), $property, $numFiles) . ': ' .
implode(', ', $files)
);
}
}
if($numRemoved) $page->message("$field->name: " .
sprintf($this->_n('Removed %d file', 'Removed %d files', $numRemoved), $numRemoved) . ": " .
implode(', ', $filesRemoved)
);
}
if($numDownloaded) {
$page->trackChange($field->name);
$page->message("$field->name (variation): " .
sprintf($this->_n('Downloaded %d file', 'Downloaded %d files', $numDownloaded), $numDownloaded) . ": " .
implode(', ', $filesDownloaded)
);
}
}
/**
* Return array of info about the import data
*
* This also populates the given import data ($a) with an '_info' property, which is an array containing
* all of the import info returned by this method. For each item in the 'pages' index it also populates
* an '_importToID' property containing the ID of the existing local page to update, or 0 if it should be
* a newly created page.
*
* Return value:
* ~~~~~
* array(
* 'numNew' => 0,
* 'numExisting' => 0,
* 'missingParents' => [ '/path/to/parent/' ],
* 'missingTemplates' => [ 'basic-page-hello' ],
* 'missingFields' => [ 'some_field', 'another_field' ],
* 'missingFieldsTypes' => [ 'some_field' => 'FieldtypeText', 'another_field' => 'FieldtypeTextarea' ]
* 'mismatchedFields' => [ 'some_field' => 'FieldtypeText' ] // field name => expected type
* 'missingTemplateFields' => [ 'template_name' => [ 'field1', 'field2', etc ] ]
* );
* ~~~~~
*
* @param array $a Import data array
* @return array
*
*/
public function getImportInfo(array &$a) {
$missingTemplateFields = array();
$missingFieldsTypes = array();
$missingTemplates = array();
$mismatchedFields = array();
$missingParents = array();
$missingFields = array();
$templateNames = array();
$parentPaths = array();
$pagePaths = array();
$numExisting = 0;
$numNew = 0;
/** @var Pages $pages */
$pages = $this->wire('pages');
/** @var Fields $fields */
$fields = $this->wire('fields');
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
/** @var PageFinder $pageFinder */
$pageFinder = $this->wire(new PageFinder());
// Identify missing fields
foreach($a['fields'] as $fieldName => $fieldInfo) {
// Note: $fieldInfo [ 'type' => 'FieldtypeText', 'version' => '1.0.0', 'blankValue' => '' ]
$field = $fields->get($fieldName);
if(!$field) {
$missingFields[] = $fieldName;
$missingFieldsTypes[$fieldName] = $fieldInfo['type'];
} else if($fieldInfo['type'] != $field->type->className()) {
$mismatchedFields[$fieldName] = $fieldInfo['type'];
}
}
// Determine which pages are new and which are existing
foreach($a['pages'] as $key => $item) {
$path = $sanitizer->pagePathNameUTF8($item['path']);
if($item['path'] !== $path) continue;
$pagePaths[$path] = $item['settings']['id'];
if($path != '/') {
$parts = explode('/', trim($path, '/'));
array_pop($parts);
$parentPath = '/' . implode('/', $parts);
if(count($parts)) $parentPath .= '/';
$parentPaths[$parentPath] = $parentPath;
}
$templateName = $item['template'];
if(!isset($templateNames[$templateName])) {
$templateNames[$templateName] = array_keys($item['data']);
}
$pageIDs = $pageFinder->findIDs(new Selectors("path=$path, include=all"));
if(!count($pageIDs)) {
// no match
$pageID = 0;
} else if(count($pageIDs) > 1) {
// more than one match, use another method
$pageID = $pages->get($path)->id;
} else {
// found
$pageID = reset($pageIDs);
}
$a['pages'][$key]['_importToID'] = $pageID; // populate local ID
$pageID ? $numExisting++ : $numNew++;
}
// determine which templates are missing, and which fields are missing from templates
foreach($templateNames as $templateName => $fieldNames) {
$template = $this->wire('templates')->get($templateName);
if($template) {
// template exists
$missingTemplateFields[$templateName] = array();
foreach($fieldNames as $fieldName) {
if(isset($missingFields[$fieldName]) || !$template->hasField($fieldName)) {
$missingTemplateFields[$templateName][] = $fieldName;
}
}
} else {
// template does not exist
$missingTemplates[] = $templateName;
}
}
// determine which parents are missing
foreach($parentPaths as $key => $path) {
if(isset($pagePaths[$path])) {
// this parent already exists or will be created during import
} else {
$parentID = $pages->getByPath($path, array('getID' => true));
if(!$parentID) $missingParents[] = $path;
}
}
/*
foreach($missingParents as $key => $path) {
// remove parents that are children of another missing parent
foreach($missingParents as $k => $p) {
if($key === $k) continue;
if(strlen($path) > strlen($p)) {
if(strpos($path, $p) === 0) unset($missingParents[$key]);
} else {
if(strpos($p, $path) === 0) unset($missingParents[$k]);
}
}
}
*/
$info = array(
'numNew' => $numNew,
'numExisting' => $numExisting,
'missingParents' => $missingParents,
'missingFields' => $missingFields,
'missingFieldsTypes' => $missingFieldsTypes,
'mismatchedFields' => array(),
'missingTemplates' => $missingTemplates,
'missingTemplateFields' => $missingTemplateFields
);
$a['_info'] = $info;
return $info;
} }
/** /**
@@ -595,36 +1128,43 @@ class PagesExportImport extends Wire {
if(isset($cache[$field->id])) return $cache[$field->id]; if(isset($cache[$field->id])) return $cache[$field->id];
$info = array( $fieldtype = $field->type;
'exportable' => true, $exportable = true;
'reason' => '', $reason = '';
);
if($field->type instanceof FieldtypeFile) { if($fieldtype instanceof FieldtypeFile) {
// we will handle these // files are allowed
$info['exportable'] = false;
$info['reason'] = 'Not yet supported';
$cache[$field->id] = $info;
return $info;
}
} else if($fieldtype instanceof FieldtypeFieldsetOpen || $fieldtype instanceof FieldtypeFieldsetClose) {
// fieldsets not exportable
$reason = 'Nothing to export/import for fieldsets';
$exportable = false;
} else {
// test to see if exportable
try { try {
$schema = $field->type->getDatabaseSchema($field); $schema = $fieldtype->getDatabaseSchema($field);
} catch(\Exception $e) { } catch(\Exception $e) {
$info['exportable'] = false; $exportable = false;
$info['reason'] = $e->getMessage(); $reason = $e->getMessage();
$cache[$field->id] = $info;
return $info;
} }
if(!isset($schema['xtra']['all']) || $schema['xtra']['all'] !== true) { if($exportable && (!isset($schema['xtra']['all']) || $schema['xtra']['all'] !== true)) {
// this fieldtype is storing data outside of the DB or in other unknown tables // this fieldtype is storing data outside of the DB or in other unknown tables
// there's a good chance we won't be able to export/import this into an array // there's a good chance we won't be able to export/import this into an array
// @todo check if fieldtype implements its own exportValue/importValue, and if // @todo check if fieldtype implements its own exportValue/importValue, and if
// it does then allow the value to be exported // it does then allow the value to be exported
$info['exportable'] = false; $exportable = false;
$info['reason'] = "Field '$field' cannot be used because $field->type uses data outside table '$field->table'"; $reason = "Field '$field' cannot be used because $field->type uses data outside table '$field->table'";
} }
}
if(!$exportable && empty($reason)) $reason = 'Export/import not supported';
$info = array(
'exportable' => $exportable,
'reason' => $reason,
);
$cache[$field->id] = $info; $cache[$field->id] = $info;

View File

@@ -134,7 +134,7 @@ abstract class WireAction extends WireData implements Module {
* *
*/ */
public function ___getConfigInputfields() { public function ___getConfigInputfields() {
$info = $this->wire('modules')->getModuleInfoVerbose($this->className()); $info = $this->wire('modules')->getModuleInfoVerbose($this->className(), array('noCache' => true));
$fieldset = $this->wire('modules')->get('InputfieldFieldset'); $fieldset = $this->wire('modules')->get('InputfieldFieldset');
$fieldset->label = $info['title']; $fieldset->label = $info['title'];
$fieldset->description = $info['summary']; $fieldset->description = $info['summary'];