1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 08:17:12 +02:00

Various upgrades to the PagesVersions module with the biggest being the addition of partial save/restore. This provides the ability to save or restore some fields and not others. Previously you could only save/restore the entire page version at once. This version also adds support for ProFields Table fields so long as they are non-paginated, and adds partial support for PageTable fields. Note that if a FieldtypeTable or FieldtypeCombo field is using any file/image fields, those don't yet support partial versions, but I have new versions of both that do, which will be released in ProFields soon.

This commit is contained in:
Ryan Cramer
2023-12-22 12:09:36 -05:00
parent c205d475bf
commit a02020cef0
3 changed files with 571 additions and 92 deletions

View File

@@ -3,25 +3,30 @@
/** /**
* Page Version Info * Page Version Info
* *
* For pages that are a version, this class represents * For pages that are a version, this class represents the `_version`
* the `_version` property of the page. * 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 * ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
* @property int $version * @property int $version Version number
* @property string $description * @property string $description Version description (not entity encoded)
* @property int $created * @property-read string $descriptionHtml Version description entity encoded for output in HTML
* @property int $modified * @property int $created Date/time created (unix timestamp)
* @property int $pages_id * @property-read string $createdStr Date/time created (YYYY-MM-DD HH:MM:SS)
* @property Page $page * @property int $modified Date/time last modified (unix timestamp)
* @property int $created_users_id * @property-read string $modifiedStr Date/time last modified (YYYY-MM-DD HH:MM:SS)
* @property int $modified_users_id * @property int $pages_id ID of page this version is for
* @property-read User|NullPage $createdUser * @property Page $page Page this version is for
* @property-read User|NullPage $modifiedUser * @property int $created_users_id ID of user that created this version
* @property-read string $createdStr * @property-read User|NullPage $createdUser User that created this version
* @property-read string $modifiedStr * @property int $modified_users_id ID of user that last modified this version
* @property string $action * @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 { class PageVersionInfo extends WireData {
@@ -52,6 +57,7 @@ class PageVersionInfo extends WireData {
'created_users_id' => 0, 'created_users_id' => 0,
'modified_users_id' => 0, 'modified_users_id' => 0,
'pages_id' => 0, 'pages_id' => 0,
'properties' => [],
'action' => '', 'action' => '',
]; ];
parent::setArray(array_merge($defaults, $data)); parent::setArray(array_merge($defaults, $data));
@@ -98,6 +104,8 @@ class PageVersionInfo extends WireData {
case 'modifiedUser': return $this->getModifiedUser(); case 'modifiedUser': return $this->getModifiedUser();
case 'createdStr': return $this->created > 0 ? date('Y-m-d H:i:s', $this->created) : ''; 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 '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); return parent::get($key);
} }
@@ -150,6 +158,47 @@ class PageVersionInfo extends WireData {
return $id ? $this->wire()->users->get($id) : $this->getCreatedUser(); 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 * Set action for PagesVersions
* *

View File

@@ -57,7 +57,7 @@ class PagesVersions extends Wire implements Module {
return [ return [
'title' => 'Pages Versions', 'title' => 'Pages Versions',
'summary' => 'Provides a version control API for pages in ProcessWire.', 'summary' => 'Provides a version control API for pages in ProcessWire.',
'version' => 1, 'version' => 2,
'icon' => self::iconName, 'icon' => self::iconName,
'autoload' => true, 'autoload' => true,
'author' => 'Ryan Cramer', 'author' => 'Ryan Cramer',
@@ -86,6 +86,8 @@ class PagesVersions extends Wire implements Module {
* *
* @param Page $page Page that version is for * @param Page $page Page that version is for
* @param int $version Version number to get * @param int $version Version number to get
* @param array $options
* - `names` (array): Optionally load only these field/property names from version.
* @return Page|NullPage * @return Page|NullPage
* - Returned page is a clone/copy of the given page updated for version data. * - 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. * - 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 = []) { public function loadPageVersion(Page $page, $version, array $options = []) {
$defaults = [ $defaults = [
'names' => [], // Optionally load only these field/property names from version. 'names' => [],
]; ];
$database = $this->wire()->database; $database = $this->wire()->database;
$table = self::versionsTable; $table = self::versionsTable;
$options = array_merge($defaults, $options); $options = array_merge($defaults, $options);
$filter = count($options['names']) > 0; $partial = count($options['names']) > 0;
$version = $this->pageVersionNumber($page, $version); $version = $this->pageVersionNumber($page, $version);
$of = $page->of(); $of = $page->of();
@@ -166,23 +168,26 @@ class PagesVersions extends Wire implements Module {
if(is_array($data)) { if(is_array($data)) {
foreach($data as $name => $value) { 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); $page->set($name, $value);
} }
} }
foreach($page->template->fieldgroup as $field) { foreach($page->template->fieldgroup as $field) {
/** @var Field $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); $allow = $this->allowFieldVersions($field);
if(!$allow) continue; if(!$allow) continue;
if($allow instanceof FieldtypeDoesVersions) { if($allow instanceof FieldtypeDoesVersions) {
$value = $allow->getPageFieldVersion($page, $field, $version); $value = $allow->getPageFieldVersion($page, $field, $version);
} else { } else {
$value = $this->getPageFieldVersion($page, $field, $version); $value = $this->getPageFieldVersion($page, $field, $version);
} }
if($value === null) { if($value === null) {
// @todo set to blankValue or leave as-is? // value is not present in version
} else { } else {
$page->set($field->name, $value); $page->set($field->name, $value);
} }
@@ -209,8 +214,9 @@ class PagesVersions extends Wire implements Module {
* *
* @param Page $page * @param Page $page
* @param array $options * @param array $options
* - `getInfo`: Specify true to instead get PageVersionInfo objects (default=false) * - `getInfo` (bool): Specify true to instead get PageVersionInfo objects (default=false)
* - `sort`: Sort by property, one of: 'created', '-created', 'version', '-version' (default='-created') * - `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[] * @return PageVersionInfo[]|Page[]
* - Returns Array of `Page` objects or array of `PageVersionInfo` objects if `getInfo` requested. * - 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, * - When returning pages, version info is in `$page->_version` value of each page,
@@ -223,6 +229,7 @@ class PagesVersions extends Wire implements Module {
$defaults = [ $defaults = [
'getInfo' => false, 'getInfo' => false,
'sort' => '-created', 'sort' => '-created',
'version' => 0,
]; ];
$sorts = [ $sorts = [
@@ -245,18 +252,22 @@ class PagesVersions extends Wire implements Module {
$sql = $sql =
"SELECT version, description, created, modified, " . "SELECT version, description, created, modified, " .
"created_users_id, modified_users_id " . "created_users_id, modified_users_id, data " .
"FROM $table " . "FROM $table " .
"WHERE pages_id=:pages_id " . "WHERE pages_id=:pages_id " . ($options['version'] ? "AND version=:version " : "") .
"ORDER BY " . $sorts[$options['sort']]; ($options['version'] ? "LIMIT 1" : "ORDER BY " . $sorts[$options['sort']]);
$query = $database->prepare($sql); $query = $database->prepare($sql);
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT); $query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
if($options['version']) $query->bindValue(':version', (int) $options['version'], \PDO::PARAM_INT);
$query->execute(); $query->execute();
while($row = $query->fetch(\PDO::FETCH_ASSOC)) { while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
$properties = json_decode($row['data'], true);
unset($row['data']);
$info = new PageVersionInfo($row); $info = new PageVersionInfo($row);
$info->set('pages_id', $page->id); $info->set('pages_id', $page->id);
$info->set('properties', $properties);
$rows[] = $this->wire($info); $rows[] = $this->wire($info);
} }
@@ -283,16 +294,49 @@ class PagesVersions extends Wire implements Module {
return $pageVersions; 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 <br />";
* echo "Created: $info->createdStr by {$info->createdUser->name} <br />";
* 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 * 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); * $page = $pages->get(1234);
* $infos = $pagesVersions->getPageVersionInfos($page); * $infos = $pagesVersions->getPageVersionInfos($page);
* foreach($infos as $info) { * foreach($infos as $info) {
* echo $info->version; // i.e. 2, 3, 4, etc. * echo "<li>$info->version: $descriptionHtml</li>"; // i.e. "2: Hello world"
* } * }
* ~~~~~ * ~~~~~
* *
@@ -315,7 +359,6 @@ class PagesVersions extends Wire implements Module {
* #pw-group-getting * #pw-group-getting
* *
* @return PageArray * @return PageArray
* @throws WireException
* *
*/ */
public function getAllPagesWithVersions() { public function getAllPagesWithVersions() {
@@ -334,7 +377,7 @@ class PagesVersions extends Wire implements Module {
/** /**
* Does page have the given version? * Does page have the given version?
* *
* #pw-group-getting * #pw-group-info
* *
* @param Page $page * @param Page $page
* @param int|string|PageVersionInfo $version Version number or omit to return quantity of versions * @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 * This is the same as calling the `hasPageVersion()` method
* with $version argument omitted. * with $version argument omitted.
* *
* #pw-group-getting * #pw-group-info
* *
* @param Page $page * @param Page $page
* @return int * @return int
@@ -405,7 +448,8 @@ class PagesVersions extends Wire implements Module {
* *
* @param Page $page * @param Page $page
* @param array $options * @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 * @return int Version number or 0 if no version created
* @throws WireException|\PDOException * @throws WireException|\PDOException
* *
@@ -414,6 +458,7 @@ class PagesVersions extends Wire implements Module {
$defaults = [ $defaults = [
'description' => '', 'description' => '',
'names' => [],
'retry' => 10, // max times to retry '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. * the `addPageVersion()` method and returns the added version number.
* @param array $options * @param array $options
* - `description` (string): Optional text description for version (default='') * - `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) * - `update` (bool): Update version if it already exists (default=true)
* @return bool|int|array Boolean true, version number, or array of property/field names * @return int|array Returns version number saved or added or 0 on fail
* - 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.
* @throws WireException|\PDOException * @throws WireException|\PDOException
* *
*/ */
@@ -472,30 +511,30 @@ class PagesVersions extends Wire implements Module {
$defaults = [ $defaults = [
'description' => null, 'description' => null,
'names' => [], // save only these field/property names, internal use only 'names' => [],
'copyFiles' => true, // make a copy of the pages files? internal use only 'copyFiles' => true, // make a copy of the pages files? internal use only
'returnVersion' => false, 'returnNames' => false, // undocumented option, internal use only
'returnNames' => false,
'update' => true, 'update' => true,
]; ];
$options = array_merge($defaults, $options); $options = array_merge($defaults, $options);
$database = $this->wire()->database; $database = $this->wire()->database;
$filter = !empty($options['names']); $copyFilesByField = false;
$partial = !empty($options['names']);
$table = self::versionsTable; $table = self::versionsTable;
$date = date('Y-m-d H:i:s'); $date = date('Y-m-d H:i:s');
$user = $this->wire()->user; $user = $this->wire()->user;
$of = $page->of(); $of = $page->of();
if(!$this->allowPageVersions($page)) return false; if(!$this->allowPageVersions($page)) return 0;
if($of) $page->of(false); if($of) $page->of(false);
if(!is_int($version)) $version = $this->pageVersionNumber($page, $version); if(!is_int($version)) $version = $this->pageVersionNumber($page, $version);
if($version < 1) $version = $this->pageVersionNumber($page); if($version < 1) $version = $this->pageVersionNumber($page);
if(!$version) { if(!$version) {
$result = $this->addPageVersion($page); $version = $this->addPageVersion($page, $options);
if($of) $page->of(true); if($of) $page->of(true);
return $result; return $version;
} }
$sql = $sql =
@@ -527,31 +566,56 @@ class PagesVersions extends Wire implements Module {
$query->bindValue(':data', json_encode($data)); $query->bindValue(':data', json_encode($data));
$query->execute(); $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); $this->pagesVersionsFiles->copyPageVersionFiles($page, $version);
} }
foreach($page->fieldgroup as $field) { foreach($page->fieldgroup as $field) {
/** @var Field $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); $allow = $this->allowFieldVersions($field);
if($allow instanceof FieldtypeDoesVersions) { if($allow instanceof FieldtypeDoesVersions) {
$added = $allow->savePageFieldVersion($page, $field, $version); $added = $allow->savePageFieldVersion($page, $field, $version);
} else if($allow === true) { } else if($allow === true) {
$added = $this->savePageFieldVersion($page, $field, $version); $added = $this->savePageFieldVersion($page, $field, $version);
if($added && $copyFilesByField && $this->pagesVersionsFiles->fieldSupportsFiles($field)) {
$this->pagesVersionsFiles->copyPageFieldVersionFiles($page, $field, $version);
}
} else { } else {
// field excluded from version // field excluded from version
$added = false; $added = false;
} }
if($added) $names[] = $field->name; if($added) $names[] = $field->name;
} }
if($of) $page->of(true); if($of) $page->of(true);
if($options['returnVersion']) return $version; return ($options['returnNames'] ? $names : $version);
if($options['returnNames']) return $names;
return true;
} }
/** /**
@@ -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 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 int $version Version number to restore. Can be omitted if given $page is already a version.
* @param array $options * @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). * - `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 * 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. * 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 = []) { public function restorePageVersion(Page $page, $version = 0, array $options = []) {
$defaults = [ $defaults = [
'names' => [],
'useTempVersion' => null, 'useTempVersion' => null,
]; ];
@@ -686,16 +752,27 @@ class PagesVersions extends Wire implements Module {
$version = (int) "$version"; $version = (int) "$version";
$pageVersion = $this->pageVersionNumber($page); $pageVersion = $this->pageVersionNumber($page);
$useTempVersion = $options['useTempVersion']; $useTempVersion = $options['useTempVersion'];
$partialRestore = count($options['names']) > 0;
$of = $page->of(); $of = $page->of();
if($version < 1) $version = $pageVersion; if($version < 1) $version = $pageVersion;
if($version < 1) { 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)) { 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) { if($pageVersion) {
@@ -723,13 +800,31 @@ class PagesVersions extends Wire implements Module {
} }
} }
// this action is looked for in the Pages::saveReady hook
$this->pageVersionInfo($versionPage, 'action', PageVersionInfo::actionRestore); $this->pageVersionInfo($versionPage, 'action', PageVersionInfo::actionRestore);
if(!$partialRestore) {
// restore all files
$this->pagesVersionsFiles->restorePageVersionFiles($page, $version); $this->pagesVersionsFiles->restorePageVersionFiles($page, $version);
}
foreach($page->fieldgroup as $field) { foreach($page->fieldgroup as $field) {
/** @var Field $field */
$allow = $this->allowFieldVersions($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); $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 Page $page
* @param Field $field * @param Field $field
* @param int $version * @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 * @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; $database = $this->wire()->database;
$table = self::valuesTable; $table = self::valuesTable;
$version = (int) "$version"; $version = (int) "$version";
@@ -792,7 +894,9 @@ class PagesVersions extends Wire implements Module {
$value = $value['data']; $value = $value['data'];
} }
} }
if(!$options['getRaw']) {
$value = $field->type->wakeupValue($page, $field, $value); $value = $field->type->wakeupValue($page, $field, $value);
}
} else { } else {
$value = null; $value = null;
} }
@@ -817,7 +921,7 @@ class PagesVersions extends Wire implements Module {
$database = $this->wire()->database; $database = $this->wire()->database;
$names = isset($options['names']) ? $options['names'] : []; $names = isset($options['names']) ? $options['names'] : [];
$filter = !empty($names); $partial = !empty($names);
$sql = "SELECT * FROM pages WHERE id=:id"; $sql = "SELECT * FROM pages WHERE id=:id";
$query = $database->prepare($sql); $query = $database->prepare($sql);
@@ -827,10 +931,9 @@ class PagesVersions extends Wire implements Module {
$query->closeCursor(); $query->closeCursor();
unset($data['id']); unset($data['id']);
if($filter) { if($partial) {
foreach($data as $name => $value) { foreach($data as $name => $value) {
if(in_array($name, $names)) continue; if(!in_array($name, $names)) unset($data[$name]);
unset($data[$name]);
} }
} }
@@ -984,6 +1087,9 @@ class PagesVersions extends Wire implements Module {
/** /**
* Delete a page field version * 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 Page $page
* @param Field $field * @param Field $field
* @param int $version * @param int $version
@@ -1196,6 +1302,37 @@ class PagesVersions extends Wire implements Module {
return true; 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 * Get next available version number for given page
* *
@@ -1236,6 +1373,26 @@ class PagesVersions extends Wire implements Module {
return $fieldtype->versions()->hasNestedRepeaterFields($page); 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 * HOOKS
* *
@@ -1271,7 +1428,7 @@ class PagesVersions extends Wire implements Module {
} }
$event->replace = true; $event->replace = true;
$event->return = $this->savePageVersion($page, $info->version, $options); $event->return = (bool) $this->savePageVersion($page, $info->version, $options);
$this->pagesVersionsFiles->hookBeforePagesSave($page); $this->pagesVersionsFiles->hookBeforePagesSave($page);
} }

View File

@@ -46,7 +46,7 @@ class PagesVersionsFiles extends Wire {
} }
/******************************************************************************** /********************************************************************************
* API SUPPORT METHODS * API SUPPORT METHODS FOR FILES BY ENTIRE DIRECTORY
* *
*/ */
@@ -54,14 +54,23 @@ class PagesVersionsFiles extends Wire {
* Copy files for given $page into version directory * Copy files for given $page into version directory
* *
* @param Page $page * @param Page $page
* @param $version * @param int $version
* @return bool|int * @return bool|int
* *
*/ */
public function copyPageVersionFiles(Page $page, $version) { public function copyPageVersionFiles(Page $page, $version) {
if(!$page->hasFilesPath()) return 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 {
$files = $this->wire()->files; $files = $this->wire()->files;
$filesManager = $page->filesManager(); $filesManager = $page->filesManager();
@@ -70,9 +79,9 @@ class PagesVersionsFiles extends Wire {
if($sourcePath === $targetPath) { if($sourcePath === $targetPath) {
// skipping copy // skipping copy
$qty = 0;
} else { } else {
$qty = $files->copy($sourcePath, $targetPath, [ 'recursive' => false ]); $qty = $files->copy($sourcePath, $targetPath, ['recursive' => false]);
}
} }
return $qty; return $qty;
@@ -102,7 +111,16 @@ class PagesVersionsFiles extends Wire {
* *
*/ */
public function restorePageVersionFiles(Page $page, $version) { public function restorePageVersionFiles(Page $page, $version) {
if(!$page->hasFilesPath()) return 0;
$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(); $filesManager = $page->filesManager();
$livePath = $filesManager->___path(); $livePath = $filesManager->___path();
$versionPath = $this->versionFilesPath($livePath, $version); $versionPath = $this->versionFilesPath($livePath, $version);
@@ -111,14 +129,270 @@ class PagesVersionsFiles extends Wire {
$filesManager->emptyPath(false, false); $filesManager->emptyPath(false, false);
$qty = $filesManager->importFiles($versionPath); $qty = $filesManager->importFiles($versionPath);
$this->disableFileHooks = false; $this->disableFileHooks = false;
}
return $qty; 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 * 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 * Update given files path for version
* *
@@ -135,7 +409,6 @@ class PagesVersionsFiles extends Wire {
return $path . self::dirPrefix . $version . '/'; return $path . self::dirPrefix . $version . '/';
} }
/** /**
* Get the total size of all files in given version * Get the total size of all files in given version
* *