forked from sean-e-dietrich/content_sync
-
Notifications
You must be signed in to change notification settings - Fork 2
/
content_sync.module
executable file
·295 lines (261 loc) · 12.4 KB
/
content_sync.module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
<?php
/**
* @file
* Allows site administrators to modify content.
*/
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Url;
use Drupal\content_sync\Form\ContentExportTrait;
use Drupal\content_sync\Form\ContentImportTrait;
use Drupal\Core\Queue\DatabaseQueue;
/**
* Implements hook_help().
*/
function content_sync_help($route_name, RouteMatchInterface $route_match) {
// Get path from route match.
$path = preg_replace('/^' . preg_quote(base_path(), '/') . '/', '/', Url::fromRouteMatch($route_match)->setAbsolute(FALSE)->toString());
if (!in_array($route_name, ['system.modules_list']) && strpos($route_name, 'help.page.content_sync') === FALSE && strpos($path, '/content') === FALSE) {
return NULL;
}
/** @var \Drupal\content_sync\ContentSyncHelpManagerInterface $help_manager */
$help_manager = \Drupal::service('content_sync.help_manager');
if ($route_name == 'help.page.content_sync') {
$build = $help_manager->buildIndex();
}
else {
$build = $help_manager->buildHelp($route_name, $route_match);
}
if ($build) {
$renderer = \Drupal::service('renderer');
$config = \Drupal::config('content_sync.settings');
$renderer->addCacheableDependency($build, $config);
return $build;
}
else {
return NULL;
}
switch ($route_name) {
case 'help.page.content_sync':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Content Synchronization module provides a user interface for importing and exporting content changes between installations of your website in different environments. Content is stored in YAML format. For more information, see the <a href=":url">online documentation for the Content Synchronization module</a>.', [':url' => 'https://www.drupal.org/project/content_sync']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Exporting the full content') . '</dt>';
$output .= '<dd>' . t('You can create and download an archive consisting of all your site\'s content exported as <em>*.yml</em> files on the <a href=":url">Export</a> page.', [':url' => \Drupal::url('content.export_full')]) . '</dd>';
$output .= '<dt>' . t('Importing a full content') . '</dt>';
$output .= '<dd>' . t('You can upload a full site content from an archive file on the <a href=":url">Import</a> page. When importing data from a different environment, the site and import files must have matching configuration values for UUID in the <em>system.site</em> configuration item. That means that your other environments should initially be set up as clones of the target site.', [':url' => \Drupal::url('content.import_full')]) . '</dd>';
$output .= '<dt>' . t('Exporting a single content item') . '</dt>';
$output .= '<dd>' . t('You can export a single content item by selecting a <em>Content type</em> and <em>Content name</em> on the <a href=":single-export">Single export</a> page. The content and its corresponding <em>*.yml file name</em> are then displayed on the page for you to copy.', [':single-export' => \Drupal::url('content.export_single')]) . '</dd>';
$output .= '<dt>' . t('Importing a single content item') . '</dt>';
$output .= '<dd>' . t('You can import a single content item by pasting it in YAML format into the form on the <a href=":single-import">Single import</a> page.', [':single-import' => \Drupal::url('content.import_single')]) . '</dd>';
$output .= '<dt>' . t('Synchronizing content') . '</dt>';
$output .= '<dd>' . t('You can review differences between the active content and an imported content archive on the <a href=":synchronize">Synchronize</a> page to ensure that the changes are as expected, before finalizing the import. The <a href=":synchronize">Synchronize</a>Synchronize</a> page also shows content items that would be added or removed.', [':synchronize' => \Drupal::url('content.sync')]) . '</dd>';
$output .= '<dt>' . t('Content logs') . '</dt>';
$output .= '<dd>' . t('You can view a chronological list of recorded events containing errors, warnings and operational information of the content import, export and synchronization on the <a href=":content-logs">Logs</a> page.', [':content-logs' => \Drupal::url('content.overview')]) . '</dd>';
$output .= '<dt>' . t('Content synchronization settings') . '</dt>';
$output .= '<dd>' . t('You can set specific settings for the content synchronization behaviour as ignore the UUID Site validation and more on the <a href=":settings">Settings</a> page.', [':settings' => \Drupal::url('content.settings')]) . '</dd>';
$output .= '</dl>';
//return $output;
case 'content.export_full':
$output = '';
$output .= '<p>' . t('Export and download the full content of this site as a gzipped tar file.') . '</p>';
return $output;
case 'content.import_full':
$output = '';
$output .= '<p>' . t('Upload a full site content archive to the content sync directory to be imported.') . '</p>';
return $output;
case 'content.export_single':
$output = '';
$output .= '<p>' . t('Choose a content item to display its YAML structure.') . '</p>';
return $output;
case 'content.import_single':
$output = '';
$output .= '<p>' . t('Import a single content item by pasting its YAML structure into the text field.') . '</p>';
return $output;
case 'content.sync':
$output = '';
$output .= '<p>' . t('Compare the content uploaded to your content sync directory with the active content before completing the import.') . '</p>';
return $output;
case 'content.settings':
$output = '';
$output .= '<p>' . t('Set specific settings for the content synchronization behaviour.') . '</p>';
return $output;
case 'content.overview':
$output = '';
$output .= '<p>' . t('chronological list of recorded events containing errors, warnings and operational information of the content import, export and synchronization.') . '</p>';
return $output;
}
}
/**
* Implements hook_theme().
*/
function content_sync_theme() {
$info = [
'content_sync_help' => [
'variables' => ['info' => []],
],
'content_sync_message' => [
'render element' => 'element',
],
];
// Since any rendering of a content_sync is going to require 'content_sync.theme.inc'
// we are going to just add it to every template.
foreach ($info as &$template) {
$template['file'] = 'includes/content_sync.theme.inc';
}
return $info;
}
/**
* Implements hook_file_download().
*/
function content_file_download($uri) {
$scheme = \Drupal::service('stream_wrapper_manager')->getScheme($uri);
$target = \Drupal::service('stream_wrapper_manager')->getTarget($uri);
if ($scheme == 'temporary' && $target == 'content.tar.gz') {
if (\Drupal::currentUser()->hasPermission('export content')) {
$request = \Drupal::request();
$date = DateTime::createFromFormat('U', $request->server->get('REQUEST_TIME'));
$date_string = $date->format('Y-m-d-H-i');
$hostname = str_replace('.', '-', $request->getHttpHost());
$filename = 'content' . '-' . $hostname . '-' . $date_string . '.tar.gz';
$disposition = 'attachment; filename="' . $filename . '"';
return [
'Content-disposition' => $disposition,
];
}
return -1;
}
}
/* From /docroot/core/includes/bootstrap.inc
find out how to declare a global and include it ???
Drupal core provides the
CONFIG_SYNC_DIRECTORY constant to access the sync directory.
$content_directories;
Where to declare constant variables
//const CONFIG_ACTIVE_DIRECTORY = 'active';
//const CONFIG_SYNC_DIRECTORY = 'sync';
//const CONFIG_STAGING_DIRECTORY = 'staging';
*/
/**
* Returns the path of a content directory.
*
* Content directories are configured using $content_directories in
* settings.php
*
* @param string $type
* The type of content directory to return.
*
* @return string
* The content directory path.
*
* @throws \Exception
*/
function content_sync_get_content_directory($type) {
global $content_directories;
// @todo Remove fallback in Drupal 9. https://www.drupal.org/node/2574943
/*if ($type == CONTENT_SYNC_DIRECTORY &&
!isset($content_directories[CONTENT_SYNC_DIRECTORY])
&& isset($content_directories[CONTENT_STAGING_DIRECTORY])) {
$type = CONTENT_STAGING_DIRECTORY;
}*/
if ($type == 'sync' &&
!isset($content_directories['sync'])
&& isset($content_directories['staging'])) {
$type = 'staging';
}
if (!empty($content_directories[$type])) {
return $content_directories[$type];
}
// throw new \Exception("The content directory type '$type' does not exist");
\Drupal::messenger()->addError("The content directory type '$type' does not exist");
}
/**
* hook_entity_update
* Keep the content snapshot table synced
*/
function content_sync_entity_update(Drupal\Core\Entity\EntityInterface $entity){
$renderer = \Drupal::service('renderer');
$context = new RenderContext();
// XXX: Suppress leaking cached metadata here as its not being used in the
// response whatsoever. The normalizers inside of this call generated
// cacheable metadata but there is no needed context as this is running on
// an entity update and has nothing to do with front-end rendering.
$renderer->executeInRenderContext($context, function() use ($renderer, $entity) {
// Get submitted values
$entity_type = $entity->getEntityTypeId();
$entity_bundle = $entity->bundle();
$entity_id = $entity->id();
//Validate that it is a Content Entity
$entityTypeManager = \Drupal::entityTypeManager();
$instances = $entityTypeManager->getDefinitions();
if (isset($instances[$entity_type]) && $instances[$entity_type] instanceof ContentEntityType) {
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)
->load($entity_id);
// Generate the YAML file.
$serializer_context = [];
$exported_entity = \Drupal::service('content_sync.exporter')
->exportEntity($entity, $serializer_context);
// Create the name
$name = $entity_type . "." . $entity_bundle . "." . $entity->uuid();
//Insert/Update Data
$activeStorage = new Drupal\content_sync\Content\ContentDatabaseStorage(\Drupal::database(), 'cs_db_snapshot');
$activeStorage->cs_write($name, \Drupal\Core\Serialization\Yaml::decode($exported_entity), $entity_type . "." . $entity_bundle);
// Invalidate the CS Cache of the entity.
$cache = \Drupal::cache('content')
->invalidate($entity_type . "." . $entity_bundle . ":" . $name);
}
});
}
/**
* hook_entity_insert
* Keep the content snapshot table synced
*/
function content_sync_entity_insert(Drupal\Core\Entity\EntityInterface $entity){
content_sync_entity_update($entity);
}
/**
* hook_entity_delete
* Keep the content snapshot table synced
*/
function content_sync_entity_delete(Drupal\Core\Entity\EntityInterface $entity){
// Get submitted values
$entity_type = $entity->getEntityTypeId();
$entity_bundle = $entity->Bundle();
$entity_uuid = $entity->uuid();
//Validate that it is a Content Entity
$entityTypeManager = \Drupal::entityTypeManager();
$instances = $entityTypeManager->getDefinitions();
if ( isset($instances[$entity_type]) && $instances[$entity_type] instanceof ContentEntityType ) {
// Update the data for diff
$name = $entity_type . "." . $entity_bundle . "." . $entity_uuid;
//Delete Data
$activeStorage = new Drupal\content_sync\Content\ContentDatabaseStorage(\Drupal::database(), 'cs_db_snapshot');
$activeStorage->cs_delete($name);
// Invalidate the CS Cache of the entity.
$cache = \Drupal::cache('content')->invalidate($entity_type.".".$entity_bundle.":".$name);
}
}
/**
* Implements hook_cron().
*/
function content_sync_cron() {
// Adapted from the drupal_batch queue garbage collection to clean up our
// multiple queues.
// @see DatabaseQueue::garbageCollection()
$prefixes = [
ContentExportTrait::getExportQueuePrefix(),
ContentImportTrait::getDeleteQueuePrefix(),
ContentImportTrait::getSyncQueuePrefix(),
];
$conn = \Drupal::database();
$query = $conn->delete(DatabaseQueue::TABLE_NAME)
->condition('created', \Drupal::time()->getRequestTime() - 864000, '<');
$or = $query->orConditionGroup();
foreach ($prefixes as $prefix) {
$or->condition('name', $conn->escapeLike($prefix) . '%', 'LIKE');
}
$query->condition($or)->execute();
}