mirror of
https://github.com/processwire/processwire.git
synced 2025-08-09 16:26:59 +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:
@@ -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
|
||||
*
|
||||
|
@@ -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 <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
|
||||
*
|
||||
* 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 "<li>$info->version: $descriptionHtml</li>"; // i.e. "2: Hello world"
|
||||
* }
|
||||
* ~~~~~
|
||||
*
|
||||
@@ -315,7 +359,6 @@ class PagesVersions extends Wire implements Module {
|
||||
* #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 =
|
||||
@@ -527,31 +566,56 @@ class PagesVersions extends Wire implements Module {
|
||||
$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,16 +752,27 @@ 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) {
|
||||
@@ -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);
|
||||
|
||||
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'];
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -1196,6 +1302,37 @@ class PagesVersions extends Wire implements Module {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
*
|
||||
* @param Page $page
|
||||
* @param $version
|
||||
* @param int $version
|
||||
* @return bool|int
|
||||
*
|
||||
*/
|
||||
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;
|
||||
$filesManager = $page->filesManager();
|
||||
|
||||
@@ -70,10 +79,10 @@ class PagesVersionsFiles extends Wire {
|
||||
|
||||
if($sourcePath === $targetPath) {
|
||||
// skipping copy
|
||||
$qty = 0;
|
||||
} else {
|
||||
$qty = $files->copy($sourcePath, $targetPath, ['recursive' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
return $qty;
|
||||
}
|
||||
@@ -102,7 +111,16 @@ class PagesVersionsFiles extends Wire {
|
||||
*
|
||||
*/
|
||||
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();
|
||||
$livePath = $filesManager->___path();
|
||||
$versionPath = $this->versionFilesPath($livePath, $version);
|
||||
@@ -111,14 +129,270 @@ class PagesVersionsFiles extends Wire {
|
||||
$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
|
||||
*
|
||||
|
Reference in New Issue
Block a user