diff --git a/wire/modules/Pages/PagesVersions/PageVersionInfo.php b/wire/modules/Pages/PagesVersions/PageVersionInfo.php
index 2abfb0e0..24092e77 100644
--- a/wire/modules/Pages/PagesVersions/PageVersionInfo.php
+++ b/wire/modules/Pages/PagesVersions/PageVersionInfo.php
@@ -3,25 +3,30 @@
/**
* Page Version Info
*
- * For pages that are a version, this class represents
- * the `_version` property of the page.
+ * For pages that are a version, this class represents the `_version`
+ * property of the page. It is also used as the return value for some
+ * methods in the PagesVersions class to return version information.
+
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
- * @property int $version
- * @property string $description
- * @property int $created
- * @property int $modified
- * @property int $pages_id
- * @property Page $page
- * @property int $created_users_id
- * @property int $modified_users_id
- * @property-read User|NullPage $createdUser
- * @property-read User|NullPage $modifiedUser
- * @property-read string $createdStr
- * @property-read string $modifiedStr
- * @property string $action
+ * @property int $version Version number
+ * @property string $description Version description (not entity encoded)
+ * @property-read string $descriptionHtml Version description entity encoded for output in HTML
+ * @property int $created Date/time created (unix timestamp)
+ * @property-read string $createdStr Date/time created (YYYY-MM-DD HH:MM:SS)
+ * @property int $modified Date/time last modified (unix timestamp)
+ * @property-read string $modifiedStr Date/time last modified (YYYY-MM-DD HH:MM:SS)
+ * @property int $pages_id ID of page this version is for
+ * @property Page $page Page this version is for
+ * @property int $created_users_id ID of user that created this version
+ * @property-read User|NullPage $createdUser User that created this version
+ * @property int $modified_users_id ID of user that last modified this version
+ * @property-read User|NullPage $modifiedUser User that last modified this version
+ * @property array $properties Native page properties array in format [ property => value ]
+ * @property-read array $fieldNames Names of fields in this version
+ * @property string $action Populated with action name if info is being used for an action
*
*/
class PageVersionInfo extends WireData {
@@ -52,6 +57,7 @@ class PageVersionInfo extends WireData {
'created_users_id' => 0,
'modified_users_id' => 0,
'pages_id' => 0,
+ 'properties' => [],
'action' => '',
];
parent::setArray(array_merge($defaults, $data));
@@ -98,6 +104,8 @@ class PageVersionInfo extends WireData {
case 'modifiedUser': return $this->getModifiedUser();
case 'createdStr': return $this->created > 0 ? date('Y-m-d H:i:s', $this->created) : '';
case 'modifiedStr': return $this->modified > 0 ? date('Y-m-d H:i:s', $this->modified) : '';
+ case 'fieldNames': return $this->getFieldNames();
+ case 'descriptionHtml': return $this->wire()->sanitizer->entities(parent::get('description'));
}
return parent::get($key);
}
@@ -150,6 +158,47 @@ class PageVersionInfo extends WireData {
return $id ? $this->wire()->users->get($id) : $this->getCreatedUser();
}
+ /**
+ * Get native property names in this version
+ *
+ * #pw-internal
+ *
+ * @return string[]
+ *
+ */
+ public function getPropertyNames() {
+ return array_keys($this->properties);
+ }
+
+ /**
+ * Get field names in this version
+ *
+ * #pw-internal
+ *
+ * @return string[]
+ *
+ */
+ public function getFieldNames() {
+ $a = parent::get('fieldNames');
+ if(!empty($a)) return $a;
+ $a = $this->wire()->pagesVersions->getPageVersionFields($this->pages_id, $this->version);
+ $a = array_keys($a);
+ parent::set('fieldNames', $a);
+ return $a;
+ }
+
+ /**
+ * Get field and property names in this version
+ *
+ * #pw-internal
+ *
+ * @return string[]
+ *
+ */
+ public function getNames() {
+ return array_merge($this->getPropertyNames(), $this->getFieldNames());
+ }
+
/**
* Set action for PagesVersions
*
diff --git a/wire/modules/Pages/PagesVersions/PagesVersions.module.php b/wire/modules/Pages/PagesVersions/PagesVersions.module.php
index ef3fc6c2..c4b3a913 100644
--- a/wire/modules/Pages/PagesVersions/PagesVersions.module.php
+++ b/wire/modules/Pages/PagesVersions/PagesVersions.module.php
@@ -57,7 +57,7 @@ class PagesVersions extends Wire implements Module {
return [
'title' => 'Pages Versions',
'summary' => 'Provides a version control API for pages in ProcessWire.',
- 'version' => 1,
+ 'version' => 2,
'icon' => self::iconName,
'autoload' => true,
'author' => 'Ryan Cramer',
@@ -86,6 +86,8 @@ class PagesVersions extends Wire implements Module {
*
* @param Page $page Page that version is for
* @param int $version Version number to get
+ * @param array $options
+ * - `names` (array): Optionally load only these field/property names from version.
* @return Page|NullPage
* - Returned page is a clone/copy of the given page updated for version data.
* - Returns a `NullPage` if requested version is not found or not allowed.
@@ -120,13 +122,13 @@ class PagesVersions extends Wire implements Module {
public function loadPageVersion(Page $page, $version, array $options = []) {
$defaults = [
- 'names' => [], // Optionally load only these field/property names from version.
+ 'names' => [],
];
$database = $this->wire()->database;
$table = self::versionsTable;
$options = array_merge($defaults, $options);
- $filter = count($options['names']) > 0;
+ $partial = count($options['names']) > 0;
$version = $this->pageVersionNumber($page, $version);
$of = $page->of();
@@ -166,23 +168,26 @@ class PagesVersions extends Wire implements Module {
if(is_array($data)) {
foreach($data as $name => $value) {
- if($filter && !in_array($name, $options['names'])) continue;
+ if($partial && !in_array($name, $options['names'])) continue;
$page->set($name, $value);
}
}
foreach($page->template->fieldgroup as $field) {
/** @var Field $field */
- if($filter && !in_array($field->name, $options['names'])) continue;
+ if($partial && !in_array($field->name, $options['names'])) continue;
+
$allow = $this->allowFieldVersions($field);
if(!$allow) continue;
+
if($allow instanceof FieldtypeDoesVersions) {
$value = $allow->getPageFieldVersion($page, $field, $version);
} else {
$value = $this->getPageFieldVersion($page, $field, $version);
}
+
if($value === null) {
- // @todo set to blankValue or leave as-is?
+ // value is not present in version
} else {
$page->set($field->name, $value);
}
@@ -209,8 +214,9 @@ class PagesVersions extends Wire implements Module {
*
* @param Page $page
* @param array $options
- * - `getInfo`: Specify true to instead get PageVersionInfo objects (default=false)
- * - `sort`: Sort by property, one of: 'created', '-created', 'version', '-version' (default='-created')
+ * - `getInfo` (bool): Specify true to instead get PageVersionInfo objects (default=false)
+ * - `sort` (string): Sort by property, one of: 'created', '-created', 'version', '-version' (default='-created')
+ * - `version` (array): Limit to this version number, for internal use (default=0)
* @return PageVersionInfo[]|Page[]
* - Returns Array of `Page` objects or array of `PageVersionInfo` objects if `getInfo` requested.
* - When returning pages, version info is in `$page->_version` value of each page,
@@ -223,6 +229,7 @@ class PagesVersions extends Wire implements Module {
$defaults = [
'getInfo' => false,
'sort' => '-created',
+ 'version' => 0,
];
$sorts = [
@@ -245,18 +252,22 @@ class PagesVersions extends Wire implements Module {
$sql =
"SELECT version, description, created, modified, " .
- "created_users_id, modified_users_id " .
+ "created_users_id, modified_users_id, data " .
"FROM $table " .
- "WHERE pages_id=:pages_id " .
- "ORDER BY " . $sorts[$options['sort']];
+ "WHERE pages_id=:pages_id " . ($options['version'] ? "AND version=:version " : "") .
+ ($options['version'] ? "LIMIT 1" : "ORDER BY " . $sorts[$options['sort']]);
$query = $database->prepare($sql);
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
+ if($options['version']) $query->bindValue(':version', (int) $options['version'], \PDO::PARAM_INT);
$query->execute();
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
+ $properties = json_decode($row['data'], true);
+ unset($row['data']);
$info = new PageVersionInfo($row);
$info->set('pages_id', $page->id);
+ $info->set('properties', $properties);
$rows[] = $this->wire($info);
}
@@ -283,16 +294,49 @@ class PagesVersions extends Wire implements Module {
return $pageVersions;
}
+ /**
+ * Get info for given page and version
+ *
+ * ~~~~~
+ * // get info for version 2
+ * $info = $pagesVersions->getPageVersionInfo($page, 2);
+ * if($info) {
+ * echo "Version: $info->version
";
+ * echo "Created: $info->createdStr by {$info->createdUser->name}
";
+ * echo "Description: $info->descriptionHtml";
+ * } else {
+ * echo "Version does not exist";
+ * }
+ * ~~~~~
+ *
+ * #pw-group-info
+ *
+ * @param Page $page
+ * @param int $version
+ * @return PageVersionInfo|null
+ *
+ */
+ public function getPageVersionInfo(Page $page, $version) {
+ $options = [
+ 'getInfo' => true,
+ 'version' => $version,
+ ];
+ $a = $this->getPageVersions($page, $options);
+ return count($a) ? reset($a) : null;
+ }
+
/**
* Get just PageVersionInfo objects for all versions of given page
*
- * This is the same as using the getPageVersions() method with the `getInfo` option.
+ * This is the same as using the getPageVersions() method with the `getInfo` option.
+ *
+ * #pw-group-info
*
* ~~~~~
* $page = $pages->get(1234);
* $infos = $pagesVersions->getPageVersionInfos($page);
* foreach($infos as $info) {
- * echo $info->version; // i.e. 2, 3, 4, etc.
+ * echo "
$info->version: $descriptionHtml"; // i.e. "2: Hello world"
* }
* ~~~~~
*
@@ -308,14 +352,13 @@ class PagesVersions extends Wire implements Module {
$options['getInfo'] = true;
return $this->getPageVersions($page, $options);
}
-
+
/**
* Get all pages that have 1 or more versions available
*
* #pw-group-getting
*
* @return PageArray
- * @throws WireException
*
*/
public function getAllPagesWithVersions() {
@@ -334,7 +377,7 @@ class PagesVersions extends Wire implements Module {
/**
* Does page have the given version?
*
- * #pw-group-getting
+ * #pw-group-info
*
* @param Page $page
* @param int|string|PageVersionInfo $version Version number or omit to return quantity of versions
@@ -374,7 +417,7 @@ class PagesVersions extends Wire implements Module {
* This is the same as calling the `hasPageVersion()` method
* with $version argument omitted.
*
- * #pw-group-getting
+ * #pw-group-info
*
* @param Page $page
* @return int
@@ -405,7 +448,8 @@ class PagesVersions extends Wire implements Module {
*
* @param Page $page
* @param array $options
- * - `description` (string) Optional text description for version
+ * - `description` (string): Optional text description for version.
+ * - `names` (array): Names of fields/properties to include in the version or omit for all.
* @return int Version number or 0 if no version created
* @throws WireException|\PDOException
*
@@ -414,6 +458,7 @@ class PagesVersions extends Wire implements Module {
$defaults = [
'description' => '',
+ 'names' => [],
'retry' => 10, // max times to retry
];
@@ -457,14 +502,8 @@ class PagesVersions extends Wire implements Module {
* the `addPageVersion()` method and returns the added version number.
* @param array $options
* - `description` (string): Optional text description for version (default='')
- * - `returnVersion` (bool): Return the version number on success? (default=false)
- * - `returnNames` (bool): Return names of properties/fields that were saved (default=false)
* - `update` (bool): Update version if it already exists (default=true)
- * @return bool|int|array Boolean true, version number, or array of property/field names
- * - Returns boolean true when version is saved or false when no version is saved.
- * - Returns integer version number when no version specified in arguments and new version added,
- * - Returns integer version number when the `returnVersion` option is true.
- * - Returns array of field names in the version when the `returnNames` option is true.
+ * @return int|array Returns version number saved or added or 0 on fail
* @throws WireException|\PDOException
*
*/
@@ -472,30 +511,30 @@ class PagesVersions extends Wire implements Module {
$defaults = [
'description' => null,
- 'names' => [], // save only these field/property names, internal use only
+ 'names' => [],
'copyFiles' => true, // make a copy of the page’s files? internal use only
- 'returnVersion' => false,
- 'returnNames' => false,
+ 'returnNames' => false, // undocumented option, internal use only
'update' => true,
];
$options = array_merge($defaults, $options);
$database = $this->wire()->database;
- $filter = !empty($options['names']);
+ $copyFilesByField = false;
+ $partial = !empty($options['names']);
$table = self::versionsTable;
$date = date('Y-m-d H:i:s');
$user = $this->wire()->user;
$of = $page->of();
- if(!$this->allowPageVersions($page)) return false;
+ if(!$this->allowPageVersions($page)) return 0;
if($of) $page->of(false);
if(!is_int($version)) $version = $this->pageVersionNumber($page, $version);
if($version < 1) $version = $this->pageVersionNumber($page);
if(!$version) {
- $result = $this->addPageVersion($page);
+ $version = $this->addPageVersion($page, $options);
if($of) $page->of(true);
- return $result;
+ return $version;
}
$sql =
@@ -526,32 +565,57 @@ class PagesVersions extends Wire implements Module {
$query->bindValue(':description', (string) $options['description']);
$query->bindValue(':data', json_encode($data));
$query->execute();
-
- if($options['copyFiles']) {
+
+ if(!$options['copyFiles']) {
+ // files will be excluded from the data
+
+ } else if($partial) {
+ // if only saving some fields in the version then copy by field
+ if($this->pagesVersionsFiles->useFilesByField($page, $options['names'])) {
+ $copyFilesByField = true;
+ } else {
+ // page does not support partial version
+ $this->pageError($page, $this->_('Partial version not supported (file fields), saved full version'));
+ $partial = false;
+ }
+
+ } else if($this->pagesVersionsFiles->useFilesByField($page)) {
+ // page and all its fields support copying files by field
+ $copyFilesByField = true;
+ }
+
+ if(!$copyFilesByField) {
+ // copy all files in directory (fallback)
$this->pagesVersionsFiles->copyPageVersionFiles($page, $version);
}
foreach($page->fieldgroup as $field) {
+
/** @var Field $field */
- if($filter && !in_array($field->name, $options['names'])) continue;
+ if($partial && !in_array($field->name, $options['names'])) continue;
+
$allow = $this->allowFieldVersions($field);
+
if($allow instanceof FieldtypeDoesVersions) {
$added = $allow->savePageFieldVersion($page, $field, $version);
+
} else if($allow === true) {
$added = $this->savePageFieldVersion($page, $field, $version);
+ if($added && $copyFilesByField && $this->pagesVersionsFiles->fieldSupportsFiles($field)) {
+ $this->pagesVersionsFiles->copyPageFieldVersionFiles($page, $field, $version);
+ }
+
} else {
// field excluded from version
$added = false;
}
+
if($added) $names[] = $field->name;
}
if($of) $page->of(true);
- if($options['returnVersion']) return $version;
- if($options['returnNames']) return $names;
-
- return true;
+ return ($options['returnNames'] ? $names : $version);
}
/**
@@ -669,6 +733,7 @@ class PagesVersions extends Wire implements Module {
* @param Page $page Page to restore version to or a page that was loaded as a version.
* @param int $version Version number to restore. Can be omitted if given $page is already a version.
* @param array $options
+ * - `names` (array): Names of fields/properties to restore or omit for all (default=[])
* - `useTempVersion` (bool): Create a temporary version and restore from that? (default=auto-detect).
* This is necessary for some Fieldtypes like nested repeaters. Use of it is auto-detected so
* it is not necessary to specify this when using the public API.
@@ -679,6 +744,7 @@ class PagesVersions extends Wire implements Module {
public function restorePageVersion(Page $page, $version = 0, array $options = []) {
$defaults = [
+ 'names' => [],
'useTempVersion' => null,
];
@@ -686,18 +752,29 @@ class PagesVersions extends Wire implements Module {
$version = (int) "$version";
$pageVersion = $this->pageVersionNumber($page);
$useTempVersion = $options['useTempVersion'];
+ $partialRestore = count($options['names']) > 0;
$of = $page->of();
-
+
if($version < 1) $version = $pageVersion;
if($version < 1) {
- return $this->pageError($page, $this->_('Cannot restore unknown version'));
+ return $this->pageError($page,
+ sprintf($this->_('Cannot restore unknown version %s'), "$version")
+ );
}
if(!$this->allowPageVersions($page)) {
- return $this->pageError($page, $this->_('Restore failed, page does not allow versions'));
+ return $this->pageError($page,
+ $this->_('Restore failed, page does not allow versions')
+ );
}
+ if($partialRestore && !$this->pageSupportsPartialVersion($page, $options['names'])) {
+ return $this->pageError($page,
+ $this->_('One or more fields requested does not support partial restore.')
+ );
+ }
+
if($pageVersion) {
// given page is the one to restore
$versionPage = $page;
@@ -722,14 +799,32 @@ class PagesVersions extends Wire implements Module {
$version = $useTempVersion;
}
}
-
+
+ // this action is looked for in the Pages::saveReady hook
$this->pageVersionInfo($versionPage, 'action', PageVersionInfo::actionRestore);
- $this->pagesVersionsFiles->restorePageVersionFiles($page, $version);
+
+ if(!$partialRestore) {
+ // restore all files
+ $this->pagesVersionsFiles->restorePageVersionFiles($page, $version);
+ }
foreach($page->fieldgroup as $field) {
+ /** @var Field $field */
$allow = $this->allowFieldVersions($field);
- if($allow instanceof FieldtypeDoesVersions) {
+
+ if(!$allow) {
+ // field not allowed in versions
+
+ } else if($partialRestore && !in_array($field->name, $options['names'])) {
+ // partial restore does not include this field
+
+ } else if($allow instanceof FieldtypeDoesVersions) {
+ // fieldtype handles its own version restore
$allow->restorePageFieldVersion($page, $field, $version);
+
+ } else if($partialRestore && $this->pagesVersionsFiles->fieldSupportsFiles($field)) {
+ // restore just files for a particular file field
+ $this->pagesVersionsFiles->restorePageFieldVersionFiles($versionPage, $field, $version);
}
}
@@ -765,11 +860,18 @@ class PagesVersions extends Wire implements Module {
* @param Page $page
* @param Field $field
* @param int $version
+ * @param array $options
+ * - `getRaw` (bool): Get raw data rather than page-ready value? (default=false)
* @return mixed|null Returns null if version data for field not available, field value otherwise
*
*/
- public function getPageFieldVersion(Page $page, Field $field, $version) {
+ public function getPageFieldVersion(Page $page, Field $field, $version, array $options = []) {
+
+ $defaults = [
+ 'getRaw' => false,
+ ];
+ $options = array_merge($defaults, $options);
$database = $this->wire()->database;
$table = self::valuesTable;
$version = (int) "$version";
@@ -792,7 +894,9 @@ class PagesVersions extends Wire implements Module {
$value = $value['data'];
}
}
- $value = $field->type->wakeupValue($page, $field, $value);
+ if(!$options['getRaw']) {
+ $value = $field->type->wakeupValue($page, $field, $value);
+ }
} else {
$value = null;
}
@@ -817,7 +921,7 @@ class PagesVersions extends Wire implements Module {
$database = $this->wire()->database;
$names = isset($options['names']) ? $options['names'] : [];
- $filter = !empty($names);
+ $partial = !empty($names);
$sql = "SELECT * FROM pages WHERE id=:id";
$query = $database->prepare($sql);
@@ -827,10 +931,9 @@ class PagesVersions extends Wire implements Module {
$query->closeCursor();
unset($data['id']);
- if($filter) {
+ if($partial) {
foreach($data as $name => $value) {
- if(in_array($name, $names)) continue;
- unset($data[$name]);
+ if(!in_array($name, $names)) unset($data[$name]);
}
}
@@ -984,6 +1087,9 @@ class PagesVersions extends Wire implements Module {
/**
* Delete a page field version
*
+ * This should not be called independently of deletePageVersion() as this
+ * method does not delete any files connected to the version.
+ *
* @param Page $page
* @param Field $field
* @param int $version
@@ -1195,6 +1301,37 @@ class PagesVersions extends Wire implements Module {
if(wireInstanceOf($page, $disallows)) return false;
return true;
}
+
+ /**
+ * Get the fields included with given page version
+ *
+ * #pw-internal
+ *
+ * @param Page|int $page
+ * @param int $version
+ * @return Field[] Array of field objects indexed by field name
+ *
+ */
+ public function getPageVersionFields($page, $version) {
+ $pageId = (int) "$page";
+ $fields = $this->wire()->fields;
+ $versionFields = [];
+ $table = self::valuesTable;
+ $sql = "SELECT field_id FROM $table WHERE pages_id=:pages_id AND version=:version";
+ $query = $this->wire()->database->prepare($sql);
+ $query->bindValue(':pages_id', $pageId, \PDO::PARAM_INT);
+ $query->bindValue(':version', (int) $version, \PDO::PARAM_INT);
+ $query->execute();
+ while($row = $query->fetch(\PDO::FETCH_NUM)) {
+ $fieldId = (int) $row[0];
+ $field = $fields->get($fieldId);
+ if(!$field) continue;
+ $versionFields[$field->name] = $field;
+ }
+ $query->closeCursor();
+ return $versionFields;
+ }
+
/**
* Get next available version number for given page
@@ -1236,6 +1373,26 @@ class PagesVersions extends Wire implements Module {
return $fieldtype->versions()->hasNestedRepeaterFields($page);
}
+ /**
+ * Does given page support partial version save and restore?
+ *
+ * #pw-internal
+ *
+ * @param Page $page
+ * @param array $names Optionally limit check to these field names
+ * @return bool
+ *
+ */
+ public function pageSupportsPartialVersion(Page $page, array $names = []) {
+ $fileFields = $this->pagesVersionsFiles->getFileFields($page, [ 'names' => $names ]);
+ if(!count($fileFields)) {
+ return true;
+ } else if($this->pagesVersionsFiles->useFilesByField($page, $names)) {
+ return true;
+ }
+ return false;
+ }
+
/********************************************************************************
* HOOKS
*
@@ -1271,7 +1428,7 @@ class PagesVersions extends Wire implements Module {
}
$event->replace = true;
- $event->return = $this->savePageVersion($page, $info->version, $options);
+ $event->return = (bool) $this->savePageVersion($page, $info->version, $options);
$this->pagesVersionsFiles->hookBeforePagesSave($page);
}
diff --git a/wire/modules/Pages/PagesVersions/PagesVersionsFiles.php b/wire/modules/Pages/PagesVersions/PagesVersionsFiles.php
index 8d6dad77..26d44fbf 100644
--- a/wire/modules/Pages/PagesVersions/PagesVersionsFiles.php
+++ b/wire/modules/Pages/PagesVersions/PagesVersionsFiles.php
@@ -46,7 +46,7 @@ class PagesVersionsFiles extends Wire {
}
/********************************************************************************
- * API SUPPORT METHODS
+ * API SUPPORT METHODS FOR FILES BY ENTIRE DIRECTORY
*
*/
@@ -54,25 +54,34 @@ class PagesVersionsFiles extends Wire {
* Copy files for given $page into version directory
*
* @param Page $page
- * @param $version
+ * @param int $version
* @return bool|int
*
*/
public function copyPageVersionFiles(Page $page, $version) {
-
- if(!$page->hasFilesPath()) return 0;
-
- $files = $this->wire()->files;
- $filesManager = $page->filesManager();
-
- $sourcePath = $filesManager->path();
- $targetPath = $this->versionFilesPath($filesManager->___path(), $version);
-
- if($sourcePath === $targetPath) {
- // skipping copy
- $qty = 0;
+
+ $qty = 0;
+
+ if(!$page->hasFilesPath()) {
+ // files not applicable
+
+ } else if($this->useFilesByField($page)) {
+ foreach($this->getFileFields($page) as $field) {
+ $qty += $this->copyPageFieldVersionFiles($page, $field, $version);
+ }
+
} else {
- $qty = $files->copy($sourcePath, $targetPath, [ 'recursive' => false ]);
+ $files = $this->wire()->files;
+ $filesManager = $page->filesManager();
+
+ $sourcePath = $filesManager->path();
+ $targetPath = $this->versionFilesPath($filesManager->___path(), $version);
+
+ if($sourcePath === $targetPath) {
+ // skipping copy
+ } else {
+ $qty = $files->copy($sourcePath, $targetPath, ['recursive' => false]);
+ }
}
return $qty;
@@ -102,23 +111,288 @@ class PagesVersionsFiles extends Wire {
*
*/
public function restorePageVersionFiles(Page $page, $version) {
- if(!$page->hasFilesPath()) return 0;
- $filesManager = $page->filesManager();
- $livePath = $filesManager->___path();
- $versionPath = $this->versionFilesPath($livePath, $version);
- if(!is_dir($versionPath)) return 0;
- $this->disableFileHooks = true;
- $filesManager->emptyPath(false, false);
- $qty = $filesManager->importFiles($versionPath);
- $this->disableFileHooks = false;
+
+ $qty = 0;
+
+ if(!$page->hasFilesPath()) {
+ // files not applicable
+ } else if($this->useFilesByField($page)) {
+ foreach($this->getFileFields($page) as $field) {
+ $qty += $this->restorePageFieldVersionFiles($page, $field, $version);
+ }
+ } else {
+ $filesManager = $page->filesManager();
+ $livePath = $filesManager->___path();
+ $versionPath = $this->versionFilesPath($livePath, $version);
+ if(!is_dir($versionPath)) return 0;
+ $this->disableFileHooks = true;
+ $filesManager->emptyPath(false, false);
+ $qty = $filesManager->importFiles($versionPath);
+ $this->disableFileHooks = false;
+ }
+
return $qty;
}
+ /********************************************************************************
+ * API SUPPORT METHODS FOR FILES BY FIELD
+ *
+ */
+
+ /**
+ * Copy files for given $page and field into version directory
+ *
+ * @param Page $page
+ * @param Field $field
+ * @param int $version
+ * @return int
+ *
+ */
+ public function copyPageFieldVersionFiles(Page $page, Field $field, $version) {
+
+ $fieldtype = $field->type;
+
+ if(!$fieldtype instanceof FieldtypeHasFiles) return 0;
+ if(!$page->hasFilesPath() || !$version) return 0;
+
+ $files = $this->wire()->files;
+ $filesManager = $page->filesManager();
+ $pageVersion = $this->pagesVersions->pageVersionNumber($page);
+ $livePath = $filesManager->___path();
+ $sourcePath = $pageVersion ? $this->versionFilesPath($livePath, $pageVersion) : $livePath;
+ $targetPath = $this->versionFilesPath($livePath, $version);
+ $qty = 0;
+
+ foreach($fieldtype->getFiles($page, $field) as $sourceFile) {
+ $sourceFile = $sourcePath . basename($sourceFile);
+ $targetFile = $targetPath . basename($sourceFile);
+ if($sourceFile === $targetFile) continue;
+ if($files->copy($sourceFile, $targetFile)) $qty++;
+ }
+
+ return $qty;
+ }
+
+ /**
+ * Delete files for given page and field version
+ *
+ * @todo is this method even needed?
+ *
+ * @param Page $page
+ * @param Field $field
+ * @param int $version
+ * @return int
+ *
+ */
+ protected function deletePageFieldVersionFiles(Page $page, Field $field, $version) {
+ $fieldtype = $field->type;
+ if(!$page->hasFilesPath() || !$version) return 0;
+ if(!$fieldtype instanceof FieldtypeHasFiles) return 0;
+ $page = $this->pageVersion($page, $version);
+ $path = $this->versionFilesPath($page->filesManager()->___path(), $version);
+ $files = $this->wire()->files;
+ if(!is_dir($path)) return 0;
+ $qty = 0;
+ foreach($fieldtype->getFiles($page, $field) as $filename) {
+ $filename = $path . basename($filename);
+ if(!is_file($filename)) continue;
+ if($files->unlink($filename)) $qty++;
+ }
+ return $qty;
+ }
+
+ /**
+ * Restore files for given field from version into live $page
+ *
+ * @param Page $page
+ * @param Field $field
+ * @param int $version
+ * @return int
+ *
+ */
+ public function restorePageFieldVersionFiles(Page $page, Field $field, $version) {
+
+ $fieldtype = $field->type;
+
+ if(!$fieldtype instanceof FieldtypeHasFiles) return 0;
+ if(!$page->hasFilesPath() || !$version) return 0;
+
+ $v = $this->pagesVersions->pageVersionNumber($page);
+
+ if($v == $version) {
+ // page already has the version number we want to restore
+ $versionPage = $page;
+ $livePage = $this->pageVersion($page, 0);
+ } else if($v) {
+ // page is for some other version we do not want
+ $livePage = $this->pageVersion($page, 0);
+ $versionPage = $this->pageVersion($livePage, $version);
+ } else {
+ // page is live page and we also need version page
+ $versionPage = $this->pageVersion($page, $version);
+ $livePage = $page;
+ }
+
+ $files = $this->wire()->files;
+ $filesManager = $livePage->filesManager();
+ $livePath = $filesManager->___path();
+ $versionPath = $this->versionFilesPath($livePath, $version);
+ $qty = 0;
+
+ if(!is_dir($versionPath)) return 0;
+
+ // clear out live files for this field
+ foreach($fieldtype->getFiles($livePage, $field) as $filename) {
+ $files->unlink($filename, $livePath);
+ }
+
+ // copy version files to live path for this field
+ foreach($fieldtype->getFiles($versionPage, $field) as $filename) {
+ $basename = basename($filename);
+ $sourceFile = $versionPath . $basename;
+ $targetFile = $livePath . $basename;
+ if($files->copy($sourceFile, $targetFile)) $qty++;
+ }
+
+ return $qty;
+ }
+
+
/********************************************************************************
* UTILITIES
*
*/
+ protected $fileFieldsCache = [];
+
+ /**
+ * Get all fields that can support files
+ *
+ * @param Page $page
+ * @param array $options
+ * - `populated` (bool): Only return populated file fields with 1+ files in them? (default=false)
+ * - `names` (array): Limit check to these field names or omit for all. (default=[])
+ * @return Field[] Returned fields array is indexed by field name
+ *
+ */
+ public function getFileFields(Page $page, array $options = []) {
+
+ $defaults = [
+ 'populated' => false,
+ 'names' => [],
+ ];
+
+ $options = array_merge($defaults, $options);
+ $fileFields = [];
+ $cacheKey = ($options['populated'] ? "p$page" : "t$page->templates_id");
+
+ if(isset($this->fileFieldsCache[$cacheKey]) && empty($options['names'])) {
+ return $this->fileFieldsCache[$cacheKey];
+ }
+
+ foreach($page->template->fieldgroup as $field) {
+ if(!$this->fieldSupportsFiles($field)) continue;
+ $fieldtype = $field->type;
+ if($options['populated'] && $fieldtype instanceof FieldtypeHasFiles) {
+ if($fieldtype->hasFiles($page, $field)) $fileFields[$field->name] = $field;
+ } else {
+ $fileFields[$field->name] = $field;
+ }
+ }
+
+ if(empty($options['names'])) $this->fileFieldsCache[$cacheKey] = $fileFields;
+
+ return $fileFields;
+ }
+
+ /**
+ * Does given field support files?
+ *
+ * @param Field|string|int $field
+ * @return bool
+ *
+ */
+ public function fieldSupportsFiles($field) {
+
+ if(!$field instanceof Field) {
+ $field = $this->wire()->fields->get($field);
+ if(!$field) return false;
+ }
+
+ $fieldtype = $field->type;
+ if($fieldtype instanceof FieldtypeHasFiles) return true;
+
+ $typeName = $fieldtype->className();
+
+ if($typeName === 'FieldtypeTable' || $typeName === 'FieldtypeCombo') {
+ // Table or Combo version prior to one that implemented FieldtypeHasFiles
+ $version = $this->wire()->modules->getModuleInfoProperty($typeName, 'versionStr');
+ if($typeName === 'FieldtypeCombo') return version_compare($version, '0.0.9', '>=');
+ return version_compare($version, '0.2.3', '>=');
+ }
+
+ return false;
+ }
+
+ /**
+ * Copy/restore files individually by field for given page?
+ *
+ * - Return true if files should be copied/restored individually by field.
+ * - Returns false if entire page directory should be copied/restored at once.
+ *
+ * @param Page $page
+ * @param Field[]|string[] $names Optionally limit check to these fields
+ * @return bool
+ *
+ */
+ public function useFilesByField(Page $page, array $names = []) {
+
+ $fileFields = $this->getFileFields($page);
+ if(!count($fileFields)) return false;
+
+ $useFilesByField = true;
+
+ if(count($names)) {
+ $a = [];
+ foreach($names as $name) $a["$name"] = $name;
+ $names = $a;
+ } else {
+ $names = null;
+ }
+
+ foreach($fileFields as $field) {
+ if($names && !isset($names[$field->name])) continue;
+ $fieldtype = $field->type;
+ if($fieldtype instanceof FieldtypeHasFiles) {
+ // supports individual files
+ } else {
+ // version of table or combo that doesn't implement FieldtypeHasFiles
+ $useFilesByField = false;
+ break;
+ }
+ }
+
+ return $useFilesByField;
+ }
+
+ /**
+ * Ensure that given page is given version, and return version page if it isn't already
+ *
+ * @param Page $page
+ * @param int $version Page version or 0 to get live page
+ * @return NullPage|Page
+ *
+ */
+ protected function pageVersion(Page $page, $version) {
+ $v = $this->pagesVersions->pageVersionNumber($page);
+ if($v == $version) return $page;
+ if($version === 0) return $this->wire()->pages->getFresh($page->id);
+ $pageVersion = $this->pagesVersions->getPageVersion($page, $version);
+ if(!$pageVersion->id) throw new WireException("Cannot find page $page version $version");
+ return $pageVersion;
+ }
+
+
/**
* Update given files path for version
*
@@ -135,7 +409,6 @@ class PagesVersionsFiles extends Wire {
return $path . self::dirPrefix . $version . '/';
}
-
/**
* Get the total size of all files in given version
*