Files
resourcespace/pages/tools/merge_rs_systems.php
2025-07-18 16:20:14 +07:00

1791 lines
80 KiB
PHP

<?php
/**
* @package ResourceSpace\Tools
*
* A tool to help administrators merge two ResourceSpace systems.
*/
if ('cli' != PHP_SAPI) {
http_response_code(401);
exit('Access denied - Command line only!');
}
$help_text = "NAME
merge_rs_systems - a tool for merging two ResourceSpace systems.
SYNOPSIS
On the system (also known as SRC system) that is going to merge with the other (DEST) system:
php path/tools/merge_rs_systems.php [OPTION...] DEST
On the system (also known as DEST system), merging in data from the other (SRC) system:
php path/tools/merge_rs_systems.php [OPTION...] SRC
DESCRIPTION
A tool to help administrators merge two ResourceSpace systems.
A specification file is required for the migration to be possible. The spec file will contain:-
- A mapping between the SRC system and the DEST systems' records. Use the --generate-spec-file option to get
an example.
OPTIONS SUMMARY
-h, --help display this help and exit
-u, --user run script as a ResourceSpace user. Use the ID of the user
-l, --language Set the language when translating i18n strings (see https://www.resourcespace.com/knowledge-base/systemadmin/translations)
Useful to prevent duplicate fixed list field options because one side has translations and
the other doesn't.
Please note if using the option multiple times, only the last occurence will take precedence.
--dry-run perform a trial run with no changes made. IMPORTANT: unavailable for import!
--clear-progress clear the progress file which is automatically generated at import
--generate-spec-file generate an example specification file
--spec-file=FILE read specification from FILE
--export export information from ResourceSpace
--import import information to ResourceSpace based on the specification file (Requires spec-file and
user options)
DEPENDENCIES
The tool requires rse_workflow plugin to be enabled.
EXAMPLES
Export
======
php /path/to/pages/tools/merge_rs_systems.php --dry-run --export /path/to/export_folder/
Import
======
php /path/to/pages/tools/merge_rs_systems.php --clear-progress --spec-file=\"/path/to/spec.php\" --import /path/to/export_folder_from_src/
" . PHP_EOL;
$cli_short_options = "hu:l:";
$cli_long_options = array(
"help",
"dry-run",
"clear-progress",
"user:",
"language:",
"spec-file:",
"export",
"import",
"generate-spec-file",
);
$options = getopt($cli_short_options, $cli_long_options);
$help = false;
$dry_run = false;
$export = false;
$import = false;
$clear_progress = false;
foreach ($options as $option_name => $option_value) {
if (in_array($option_name, array("h", "help"))) {
echo $help_text;
exit(0);
}
if (
in_array(
$option_name,
array(
"dry-run",
"clear-progress",
"export",
"import",
"generate-spec-file")
)
) {
fwrite(STDOUT, "Script running with '{$option_name}' option enabled!" . PHP_EOL);
$option_name = str_replace("-", "_", $option_name);
$$option_name = true;
}
if ($option_name == "spec-file" && !is_array($option_value)) {
if (!file_exists($option_value)) {
fwrite(STDERR, "ERROR: Unable to open input file '{$option_value}'!" . PHP_EOL);
exit(1);
}
$spec_file_path = $option_value;
}
if (in_array($option_name, array("u", "user")) && !is_array($option_value)) {
if (!is_numeric($option_value) || (int) $option_value <= 0) {
fwrite(STDERR, "ERROR: Invalid 'user' value provided: '{$option_value}' of type " . gettype($option_value) . PHP_EOL);
fwrite(STDOUT, PHP_EOL . $help_text);
exit(1);
}
$user = $option_value;
}
if (in_array($option_name, ["l", "language"])) {
if (is_array($option_value)) {
fwrite(STDERR, "ERROR: Language should only be set once. Use either -l or --language." . PHP_EOL);
fwrite(STDOUT, PHP_EOL . $help_text);
exit(1);
}
$language = trim($option_value);
}
}
$webroot = dirname(dirname(__DIR__));
include_once "{$webroot}/include/boot.php";
set_time_limit(0);
// Increase this DB session idle timeout to 24 hours
ps_query('SET session wait_timeout=86400;');
$sql_session_wait_timeout = array_column(ps_query('SHOW SESSION VARIABLES LIKE "wait_timeout"'), 'Value', 'Variable_name');
logScript("Database session 'wait_timeout' variable is set to {$sql_session_wait_timeout['wait_timeout']} seconds");
$get_file_handler = function ($file_path, $mode) {
$file_handler = fopen($file_path, $mode);
if ($file_handler === false) {
logScript("ERROR: Unable to open output file '{$file_path}'!");
exit(1);
}
return $file_handler;
};
$json_decode_file_data = function ($fh) {
$input_lines = array();
while (($line = fgets($fh)) !== false) {
if (trim($line) != "" && mb_check_encoding($line, "UTF-8")) {
$input_lines[] = trim($line);
}
}
fclose($fh);
if (empty($input_lines)) {
logScript("WARNING: No data to import! To be safe, double check on the source side whether this is true.");
return array();
}
$json_decoded_data = array();
foreach ($input_lines as $input_line) {
$value = json_decode($input_line, true);
if (json_last_error() !== JSON_ERROR_NONE) {
logScript("ERROR: Unable to decode JSON because of the following error: " . json_last_error_msg());
exit(100);
}
$json_decoded_data[] = $value;
}
return $json_decoded_data;
};
if (isset($generate_spec_file) && $generate_spec_file) {
$spec_fpath = (getcwd() ?: dirname(__DIR__, 2)) . '/spec_file_example.php';
$spec_fh = $get_file_handler($spec_fpath, "w+b");
fwrite($spec_fh, '<?php
// All of the following configuration options have the left side (keys) represent the SRC and DEST on the right (values)
// User groups can be configured in three ways:
// - map to an existing record in DEST system
// - create new
// - do not create
$usergroups_spec = array(
3 => 3, # Super Admin
9 => array(
"create" => true,
),
13 => array(
"create" => false,
),
);
// Archive states can either be mapped to an existing record or be created as a new state on the DEST system
$archive_states_spec = array(
0 => 0, # Active
4 => null, # Marketing - new on DEST system
);
// Resource types can either be mapped to an existing record or be created as a new state on the DEST system
$resource_types_spec = array(
0 => 0, # Global (magic type)
999 => 999, # Archive Only (magic type)
1 => 1, # Photo
5 => null, # Case Study
);
/*
Resource type fields can be configured in three ways:
- map to an existing record on DEST system
- do not create
- create new record
IMPORTANT: make sure you map to a compatible field on the DEST system. This is especially true for a category tree which
can end up a flat structure if mapped to a fixed list field of different type.
When it comes to field mappings, these are the rules the script goes by:-
- one SRC field can only be mapped to one DEST field;
- different SRC fixed list fields could be mapped to the same DEST field;
- when mapping fields, all the options from the SRC fixed list field move over to the DEST fixed list field (this also
applies to fields that are created as new on the destination system - ie. there\'s no mapping)
For fixed list fields, if required, there can be extra rules for the options. See nodes_spec rules. Please bear in mind
that the metadata field mapping becomes the fallback rule for nodes.
*/
$resource_type_fields_spec = array(
// Note: when mapping to a field on DEST system, the "create" property should still be true
3 => array(
"create" => true,
"ref" => 3,
), # Country | dynamic keywords
8 => array(
"create" => true,
"ref" => 8,
), # Title | text box
9 => array("create" => false), # Document extract
10 => array("create" => false), # Credit
87 => array(
"create" => true,
"ref" => null,
), # Display condition parent | drop down
88 => array(
"create" => true,
"ref" => null,
), # Display condition child | text box
);
/*
Allow nodes (ie fixed list field options) mapping between SRC and DEST. This is optional and should only be done as
needed (ie. where a SRC field needs to become something else on DEST and its import is no longer required).
The rules governing nodes mapping is as follows:-
- one SRC node can be mapped to multiple DEST nodes (the DEST node can be under a different DEST field - compared
to the mapping of the SRC nodes\' field);
- different SRC nodes can be mapped to the same DEST node;
IMPORTANT: this is the true source of truth for mapping a SRC node to DEST node(s).
*/
$nodes_spec = [
// from field 89
280 => [260],
281 => [181, 261,],
282 => [262],
// from field 91
288 => [287], # Example of a map to a category tree ==> "Folder 1/1.2/1.2.1"
289 => [181],
];
/*
Considering the configuration for both resource_type_fields_spec and nodes_spec, you can expect the following behaviour:-
SRC RTF | Create on DEST? | Map to DEST RTF? | Options mapped | Outcome
===============|=================|==================|================|========
fixed list rtf | no | no | no | discard all options
fixed list rtf | no | no | yes | use only mapped DEST nodes, discard everything else
fixed list rtf | yes | no | yes | move only mapped nodes to their DEST nodes, everything else is added as an option on the new field
fixed list rtf | yes | yes | yes | move only mapped nodes to their DEST node, everything else is moved to the mapped DEST field
*/
// Metadata field to store the SRC resource ID. MUST be of type "Text box (single line)". Set to zero to disable.
$rtf_src_resource_ref = 0;
// Fixed list field options that will be applied to all imported SRC resources. List of node IDs.
$nodes_applied_to_all_merged_resources = [];
// A resource access moves across as is. Exception are resources with custom access which need to be converted to a
// different access level (e.g open, restricted or confidential)
// Acceptable values are: RESOURCE_ACCESS_FULL -or- RESOURCE_ACCESS_RESTRICTED -or- RESOURCE_ACCESS_CONFIDENTIAL
$custom_access_new_value_spec = RESOURCE_ACCESS_RESTRICTED;
// SRC API details. Currently used to generate the SRC file (or alternatives) for the upload_processing job which, when
// run, will pull it in the DEST system.
$src_api = [
\'base_url\' => \'\',
\'username\' => \'\',
\'key\' => \'\',
\'http_options\' => [
// \'timeout\' => 5, # in seconds, default is 60
],
];
' . PHP_EOL);
fclose($spec_fh);
logScript("Successfully generated an example of the spec file. Location: '{$spec_fpath}'");
exit(0);
}
// Advanced workflow plugin is required to be enabled
$active_plugins = array_column(get_active_plugins(), 'name');
if (!in_array('rse_workflow', $active_plugins)) {
logScript("ERROR: Missing requirement - 'Advanced workflow' plugin is not enabled.");
exit(1);
}
if ($import && !isset($user)) {
logScript("ERROR: You need to specify a user when importing. It is best if it is a Super Admin.");
echo $help_text;
exit(1);
}
if (isset($user)) {
$user_select_sql = new PreparedStatementQuery();
$user_select_sql->sql = "u.ref = ?";
$user_select_sql->parameters = ["i",$user];
$user_data = validate_user($user_select_sql, true);
if (!is_array($user_data) || count($user_data) == 0) {
logScript("ERROR: Unable to validate user ID #{$user}!");
exit(1);
}
setup_user($user_data[0]);
logScript("Running script as user '{$username}' (ID #{$userref})");
// Reset any "maintenance mode" config options the system might be configured with
$global_permissions_mask = "";
$system_read_only = false;
}
logScript("Running script with language set to '{$language}'");
/*
For the following usage:
- php path/tools/merge_rs_systems.php [OPTION...] --export DEST
- php path/tools/merge_rs_systems.php [OPTION...] --import SRC
Ensure DEST/SRC folder has been provided when exporting/importing data
*/
if ($export || $import) {
$folder_path = end($argv);
if (!file_exists($folder_path) || !is_dir($folder_path)) {
$folder_type = $export ? "DEST" : ($import ? "SRC" : "");
logScript("ERROR: {$folder_type} MUST be an existing folder. Value provided: '{$folder_path}'");
exit(1);
}
}
if ($export && isset($folder_path)) {
$tables = array(
array(
"name" => "usergroup",
"formatted_name" => "user groups",
"filename" => "usergroups",
"record_feedback" => array(
"text" => "User group #%ref '%name'",
"placeholders" => array("ref", "name")
),
"additional_process" => function ($record) {
$usergroup_preferences = array();
if (get_config_options(array('usergroup' => $record["ref"]), $usergroup_preferences)) {
logScript("Found user group preferences");
$record["usergroup_preferences"] = $usergroup_preferences;
}
return $record;
}
),
array(
"name" => "user",
"formatted_name" => "users",
"filename" => "users",
"record_feedback" => array(
"text" => "User #%ref '%fullname' (Username: %username | E-mail: %email)",
"placeholders" => array("ref", "fullname", "username", "email")
),
"sql" => array(
"select" => "*",
"from" => "user",
"where" => "
username IS NOT NULL AND trim(username) <> ''
AND usergroup IS NOT NULL AND trim(usergroup) <> ''",
),
"additional_process" => function ($record) {
$user_preferences = array();
if (get_config_options(array('user' => $record["ref"]), $user_preferences)) {
logScript("Found user preferences");
$record["user_preferences"] = $user_preferences;
}
return $record;
},
),
array(
"name" => "resource_type",
"formatted_name" => "resource types",
"filename" => "resource_types",
"record_feedback" => array(
"text" => "Resource type #%ref '%name'",
"placeholders" => array("ref", "name")
)
),
array(
"name" => "resource_type_field",
"formatted_name" => "resource type fields",
"filename" => "resource_type_fields",
"record_feedback" => array(
"text" => "Resource type field #%ref '%title' (shortname: '%name')",
"placeholders" => array("ref", "title", "name")
)
),
array(
"name" => "resource_type_field_resource_type",
"formatted_name" => "resource type field resource types",
"filename" => "resource_type_field_resource_type",
"record_feedback" => array(
"text" => "Resource type field (#%resource_type_field) -> Resource type (#%resource_type) mappings",
"placeholders" => array("resource_type_field", "resource_type"),
),
"sql" => array(
"where" => "resource_type_field IN (SELECT ref FROM resource_type_field)",
"order_by" => 'resource_type_field ASC, resource_type ASC',
),
"pagination" => [
"cursor_sql" => new PreparedStatementQuery(
'(
resource_type_field > ?
OR (resource_type_field = ? AND resource_type > ?)
)',
['i', 0, 'i', 0, 'i', 0]
),
"last_page_cursor_sql" => static function (PreparedStatementQuery $cursor, array $record): PreparedStatementQuery {
return new PreparedStatementQuery(
$cursor->sql,
[
'i',$record['resource_type_field'],
'i',$record['resource_type_field'],
'i',$record['resource_type'],
]
);
},
],
),
array(
"name" => "node",
"formatted_name" => "nodes",
"filename" => "nodes",
"record_feedback" => array(),
"sql" => array(
"where" => "resource_type_field IN (SELECT ref FROM resource_type_field)",
),
),
array(
"name" => "resource",
"formatted_name" => "resources",
"filename" => "resources",
"record_feedback" => array(),
"sql" => array(
"where" => "ref > 0
AND resource_type IN (SELECT ref FROM resource_type)
AND archive IN (SELECT `code` FROM archive_states)
AND (file_extension IS NOT NULL AND trim(file_extension) <> '')
AND (preview_extension IS NOT NULL AND trim(preview_extension) <> '')",
),
"additional_process" => static function ($record) {
if (!file_exists(get_resource_path($record["ref"], true, "", false, $record["file_extension"]))) {
logScript("WARNING: unable to get original file for resource #{$record["ref"]}");
return false;
}
return $record;
},
),
array(
"name" => "resource_alt_files",
"formatted_name" => "resource alternative files",
"filename" => "resource_alt_files",
"record_feedback" => array(),
"sql" => array(
"select" => "raf.*",
"from" => "resource_alt_files AS raf
INNER JOIN resource AS r ON raf.resource = r.ref",
"where" => "raf.resource > 0
AND r.resource_type IN (SELECT ref FROM resource_type)
AND r.archive IN (SELECT `code` FROM archive_states)
AND (r.file_extension IS NOT NULL AND trim(r.file_extension) <> '')
AND (r.preview_extension IS NOT NULL AND trim(r.preview_extension) <> '')"
. (
in_array('rse_version', $active_plugins)
? "\r\n AND raf.ref NOT IN (
SELECT previous_file_alt_ref
FROM resource_log
WHERE previous_file_alt_ref IS NOT null
AND type = 'u'
AND resource = r.ref
)\r\n"
: ''
),
"order_by" => 'raf.ref ASC',
),
"pagination" => [
"cursor_sql" => new PreparedStatementQuery('raf.ref > ?', ['i', 0]),
],
"additional_process" => static function ($record) {
if (
!file_exists(
get_resource_path(
$record["resource"],
true,
"",
false,
$record["file_extension"],
true,
1,
false,
"",
$record["ref"]
)
)
) {
logScript("WARNING: unable to get original file for resource - alternative pair: #{$record["resource"]} - #{$record["ref"]}");
return false;
}
return $record;
},
),
array(
"name" => "resource_node",
"formatted_name" => "resource nodes",
"filename" => "resource_nodes",
"record_feedback" => array(),
"sql" => array(
"select" => "rn.resource, rn.node, rn.hit_count, rn.new_hit_count",
"from" => "resource_node AS rn
RIGHT JOIN resource AS r ON rn.resource = r.ref
RIGHT JOIN node AS n ON rn.node = n.ref",
"where" => "resource > 0
AND r.resource_type IN (SELECT ref FROM resource_type)
AND r.archive IN (SELECT `code` FROM archive_states)
AND (r.file_extension IS NOT NULL AND trim(r.file_extension) <> '')
AND (r.preview_extension IS NOT NULL AND trim(r.preview_extension) <> '')
AND n.resource_type_field IN (SELECT ref FROM resource_type_field)",
"order_by" => 'resource ASC, node ASC',
),
"pagination" => [
"cursor_sql" => new PreparedStatementQuery(
'(
resource > ?
OR (resource = ? AND node > ?)
)',
['i', 0, 'i', 0, 'i', 0]
),
"last_page_cursor_sql" => static function (PreparedStatementQuery $cursor, array $record): PreparedStatementQuery {
return new PreparedStatementQuery(
$cursor->sql,
[
'i',$record['resource'],
'i',$record['resource'],
'i',$record['node'],
]
);
},
],
),
array(
"name" => "resource_dimensions",
"formatted_name" => "resource dimensions",
"filename" => "resource_dimensions",
"record_feedback" => array(),
"sql" => array(
"where" => "resource > 0 AND EXISTS(SELECT ref FROM resource WHERE ref = resource_dimensions.resource)",
"order_by" => 'resource ASC',
),
"pagination" => [
"cursor_sql" => new PreparedStatementQuery('resource > ?', ['i', 0]),
"last_page_cursor_sql" => static function (PreparedStatementQuery $cursor, array $record): PreparedStatementQuery {
return new PreparedStatementQuery($cursor->sql, ['i', $record['resource']]);
},
],
),
array(
"name" => "resource_related",
"formatted_name" => "resource related",
"filename" => "resource_related",
"record_feedback" => array(),
"sql" => array(
"where" => "resource > 0
AND EXISTS(SELECT ref FROM resource WHERE ref = resource_related.resource)
AND EXISTS(SELECT ref FROM resource WHERE ref = resource_related.related)",
"order_by" => 'resource ASC, related ASC',
),
"pagination" => [
"cursor_sql" => new PreparedStatementQuery(
'(
resource > ?
OR (resource = ? AND related > ?)
)',
['i', 0, 'i', 0, 'i', 0]
),
"last_page_cursor_sql" => static function (PreparedStatementQuery $cursor, array $record): PreparedStatementQuery {
return new PreparedStatementQuery(
$cursor->sql,
[
'i',$record['resource'],
'i',$record['resource'],
'i',$record['related'],
]
);
},
],
),
);
foreach ($tables as $table) {
logScript("");
logScript("Exporting {$table["formatted_name"]}...");
$export_fh = $get_file_handler($folder_path . DIRECTORY_SEPARATOR . "{$table["filename"]}_export.json", "w+b");
$select = isset($table["sql"]["select"]) && trim($table["sql"]["select"]) != "" ? $table["sql"]["select"] : "*";
$from = isset($table["sql"]["from"]) && trim($table["sql"]["from"]) != "" ? $table["sql"]["from"] : $table["name"];
$where = isset($table["sql"]["where"]) && trim($table["sql"]["where"]) != "" ? "WHERE {$table["sql"]["where"]}" : "";
$additional_process = isset($table["additional_process"]) && is_callable($table["additional_process"]) ? $table["additional_process"] : null;
// Keyset (cursor-based) pagination. Please note it requires a data set with deterministic order (the order_by
// is tightly coupled with the cursor logic).
$order_by = isset($table['sql']['order_by']) && trim($table['sql']['order_by']) != '' ? "{$table["sql"]["order_by"]}" : 'ref ASC';
$cursor_sql = isset($table['pagination'], $table['pagination']['cursor_sql'])
? $table['pagination']['cursor_sql']
: new PreparedStatementQuery('ref > ?', ['i', 0]);
$cursor_sql->sql = $where === '' ? "WHERE {$cursor_sql->sql}" : "AND {$cursor_sql->sql}";
$last_page_cursor = $cursor_sql;
$last_page_cursor_key = isset($table['pagination'], $table['pagination']['last_page_cursor_sql'])
? $table['pagination']['last_page_cursor_sql']
: static function (PreparedStatementQuery $cursor, array $record): PreparedStatementQuery {
return new PreparedStatementQuery($cursor->sql, ['i', $record['ref']]);
};
$chunk_size = 5000;
// end of pagination
do {
$records = ps_query(
"SELECT {$select} FROM {$from} {$where} {$last_page_cursor->sql} ORDER BY {$order_by} LIMIT ?",
array_merge($last_page_cursor->parameters, ['i', $chunk_size])
);
// Check if no records were found for the first page. Note: cursor key(s) will be zero.
$cursor_key_values = array_filter(
$last_page_cursor->parameters,
static fn($_, $key) => $key % 2 === 1, ARRAY_FILTER_USE_BOTH
);
if (empty($records) && array_filter($cursor_key_values, is_positive_int_loose(...)) === []) {
logScript("WARNING: no data found!");
break;
}
// Process the current chunk
foreach ($records as $record) {
$last_page_cursor = $last_page_cursor_key($last_page_cursor, $record);
// Sometimes you want to provide feedback to the user to let him/ her know a particular record is being processed
if (isset($table["record_feedback"]) && !empty($table["record_feedback"])) {
$log_msg = $table["record_feedback"]["text"];
foreach ($table["record_feedback"]["placeholders"] as $placeholder) {
$log_msg = str_replace("%{$placeholder}", $record["{$placeholder}"], $log_msg);
}
logScript($log_msg);
}
if (!is_null($additional_process)) {
$record = $additional_process($record);
// additional processing might determine we don't want to process this record at all
if ($record === false) {
continue;
}
}
if ($dry_run) {
continue;
}
fwrite($export_fh, json_encode($record, JSON_NUMERIC_CHECK) . PHP_EOL);
}
} while (count($records) === $chunk_size);
fclose($export_fh);
}
# ARCHIVE STATES
################
logScript("");
logScript("Exporting archive states...");
$archive_states_export_fh = $get_file_handler($folder_path . DIRECTORY_SEPARATOR . "archive_states_export.json", "w+b");
foreach (get_workflow_states() as $archive_state) {
if (!isset($lang["status{$archive_state}"])) {
logScript("Warning: language not set for archive state #{$archive_state}");
continue;
}
$archive_state_text = $lang["status{$archive_state}"];
logScript("Archive state '{$archive_state_text}' (ID #{$archive_state})");
if ($dry_run) {
continue;
}
$exported_archive_state = array(
"ref" => $archive_state,
"lang" => $archive_state_text);
fwrite($archive_states_export_fh, json_encode($exported_archive_state, JSON_NUMERIC_CHECK) . PHP_EOL);
}
fclose($archive_states_export_fh);
}
if ($import && isset($folder_path)) {
if (!isset($spec_file_path) || trim($spec_file_path) == "") {
logScript("ERROR: Specification file not provided or empty!");
exit(1);
}
include_once $spec_file_path;
// Quick spec file validation
if (!in_array($custom_access_new_value_spec, array_diff(RESOURCE_ACCESS_TYPES, [RESOURCE_ACCESS_CUSTOM_GROUP]))) {
logScript('ERROR: Specification file - invalid $custom_access_new_value_spec');
exit(1);
}
/*
progress.php is used to override the specification file that was provided as original input by keeping track of new
mappings created and saving them in the progress file.
*/
$progress_fp = $folder_path . DIRECTORY_SEPARATOR . "progress.php";
$progress_fh = $get_file_handler($progress_fp, "a+b");
$php_tag_found = false;
while (($line = fgets($progress_fh)) !== false) {
if (trim($line) != "" && mb_check_encoding($line, "UTF-8") && mb_strpos($line, "<?php") !== false) {
$php_tag_found = true;
}
}
if (!$php_tag_found || $clear_progress) {
ftruncate($progress_fh, 0);
fwrite($progress_fh, "<?php" . PHP_EOL);
}
include_once $progress_fp;
define("TX_SAVEPOINT", "merge_rs_systems_import");
if (!db_begin_transaction(TX_SAVEPOINT)) {
logScript("ERROR: MySQL - unable to begin transaction!");
exit(1);
}
db_end_transaction(TX_SAVEPOINT);
$call_src_api = static function (string $function, array $data) use ($src_api) {
$query = http_build_query(
array_merge(
[
'user' => $src_api['username'],
'language' => $GLOBALS['language'] ?? $GLOBALS['defaultlanguage'],
'function' => $function,
],
$data
)
);
logScript("[SRC API] Query: {$query}");
$sign = hash('sha256', $src_api['key'] . $query);
$response = file_get_contents(
"{$src_api['base_url']}/api/?$query&sign=$sign",
false,
stream_context_create([
'http' => array_merge(
$src_api['http_options'],
[
'ignore_errors' => true,
'user_agent' => sprintf('ResourceSpace-Merge-Script/1.0 (%s)', $GLOBALS['baseurl']),
]
),
])
);
$status_code = preg_match('/\d{3}/', $http_response_header[0], $match) ? (int) $match[0] : 0;
$results = json_decode($response, true);
if ($status_code === 401) {
throw new RuntimeException('Unauthorised');
} elseif ($status_code !== 200 && JSON_ERROR_NONE !== json_last_error()) {
// Handle generic fails (simple string responses)
throw new RuntimeException($response);
} elseif ($status_code !== 200 && isset($results['error']['detail'])) {
// Handle generic (structured) errors (usually done using ajax_functions.php)
throw new RuntimeException($results['error']['detail']);
} elseif ($status_code === 200 && JSON_ERROR_NONE !== json_last_error()) {
throw new RuntimeException('(JSON) ' . json_last_error_msg());
} else {
return $results;
}
};
// Check API credentials. Any errors require further investigation before moving forward.
logScript('Checking the SRC API details');
try {
$call_src_api('get_system_status', []);
} catch (RuntimeException $e) {
logScript("ERROR: Bad SRC API details! Response: {$e->getMessage()}");
exit(1);
}
# USER GROUPS
#############
logScript("");
logScript("Importing user groups...");
if (!isset($usergroups_spec) || empty($usergroups_spec)) {
logScript("ERROR: Spec missing 'usergroups_spec'");
exit(1);
}
$processed_usergroups = (isset($processed_usergroups) ? $processed_usergroups : array());
$usergroups_not_created = (isset($usergroups_not_created) ? $usergroups_not_created : array());
$src_usergroups = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "usergroups_export.json", "r+b"));
$dest_usergroups = get_usergroups(false, "", true);
$process_usergroup_preferences = static function ($usergroup_ref, $usergroup_data) {
db_begin_transaction(TX_SAVEPOINT);
if (isset($usergroup_data["usergroup_preferences"]) && is_array($usergroup_data["usergroup_preferences"]) && !empty($usergroup_data["usergroup_preferences"])) {
logScript("Processing user group preferences (if no warning is showing, this is ok)");
foreach ($usergroup_data["usergroup_preferences"] as $usergroup_p) {
if (!set_usergroup_config_option($usergroup_ref, $usergroup_p["parameter"], $usergroup_p["value"])) {
logScript("ERROR: unable to save user group preference: {$usergroup_p["parameter"]} = '{$usergroup_p["value"]}'");
exit(1);
}
}
}
db_end_transaction(TX_SAVEPOINT);
};
foreach ($src_usergroups as $src_ug) {
if (in_array($src_ug["ref"], $processed_usergroups) || in_array($src_ug["ref"], $usergroups_not_created)) {
continue;
}
logScript("Processing {$src_ug["name"]} (ID #{$src_ug["ref"]})...");
if (!array_key_exists($src_ug["ref"], $usergroups_spec)) {
logScript("WARNING: Specification for usergroups does not contain a mapping for this group! Skipping");
$usergroups_not_created[] = $src_ug["ref"];
fwrite($progress_fh, "\$usergroups_not_created[] = {$src_ug["ref"]};" . PHP_EOL);
continue;
}
$spec_cfg_value = $usergroups_spec[$src_ug["ref"]];
if (is_numeric($spec_cfg_value) && $spec_cfg_value > 0 && array_key_exists($spec_cfg_value, $dest_usergroups)) {
logScript("Found direct 1:1 mapping to '{$dest_usergroups[$spec_cfg_value]}' (ID #{$spec_cfg_value})... Skipping");
$processed_usergroups[] = $src_ug["ref"];
fwrite($progress_fh, "\$processed_usergroups[] = {$src_ug["ref"]};" . PHP_EOL);
} elseif (is_array($spec_cfg_value)) {
if (!isset($spec_cfg_value["create"])) {
logScript("ERROR: usergroup specification config value is invalid. Required keys: create - true/false");
exit(1);
}
if ((bool) !$spec_cfg_value["create"]) {
logScript("Skipping usergroup as per the specification record");
$usergroups_not_created[] = $src_ug["ref"];
fwrite($progress_fh, "\$usergroups_not_created[] = {$src_ug["ref"]};" . PHP_EOL);
continue;
}
db_begin_transaction(TX_SAVEPOINT);
$new_ug_ref = save_usergroup(0, array('name' => $src_ug["name"], 'request_mode' => 1));
log_activity(null, LOG_CODE_CREATED, null, 'usergroup', null, $new_ug_ref);
log_activity(null, LOG_CODE_CREATED, $src_ug["name"], 'usergroup', 'name', $new_ug_ref, null, '');
log_activity(null, LOG_CODE_CREATED, '1', 'usergroup', 'request_mode', $new_ug_ref, null, '');
$process_usergroup_preferences($new_ug_ref, $src_ug);
logScript("Created new user group '{$src_ug["name"]}' (ID #{$new_ug_ref})");
$usergroups_spec[$src_ug["ref"]] = $new_ug_ref;
$processed_usergroups[] = $src_ug["ref"];
fwrite(
$progress_fh,
"\$usergroups_spec[{$src_ug["ref"]}] = {$new_ug_ref};"
. PHP_EOL
. "\$processed_usergroups[] = {$src_ug["ref"]};"
. PHP_EOL
);
} else {
logScript("ERROR: Invalid usergroup specification record for key #{$src_ug["ref"]}");
exit(1);
}
}
unset($src_usergroups);
unset($dest_usergroups);
# USERS & USER PREFERENCES
##########################
logScript("");
logScript("Importing users and their preferences...");
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$usernames_mapping = (isset($usernames_mapping) ? $usernames_mapping : array());
$users_not_created = (isset($users_not_created) ? $users_not_created : array());
$src_users = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "users_export.json", "r+b"));
$process_user_preferences = function ($user_ref, $user_data) use ($progress_fh, &$usernames_mapping) {
db_begin_transaction(TX_SAVEPOINT);
if (isset($user_data["user_preferences"]) && is_array($user_data["user_preferences"]) && !empty($user_data["user_preferences"])) {
logScript("Processing user preferences (if no warning is showing, this is ok)");
foreach ($user_data["user_preferences"] as $user_p) {
if (!set_config_option($user_ref, $user_p["parameter"], $user_p["value"])) {
logScript("ERROR: unable to save user preference: {$user_p["parameter"]} = '{$user_p["value"]}'");
exit(1);
}
}
}
$usernames_mapping[$user_data["ref"]] = $user_ref;
fwrite($progress_fh, "\$usernames_mapping[{$user_data["ref"]}] = {$user_ref};" . PHP_EOL);
db_end_transaction(TX_SAVEPOINT);
};
foreach ($src_users as $user) {
if (array_key_exists($user["ref"], $usernames_mapping) || in_array($user["ref"], $users_not_created)) {
continue;
}
if (
(($found_udata = get_user((int) get_user_by_username($user['username']))) && $found_udata !== false)
|| (
$user['email'] != ''
&& ($found_udata = get_user((int) get_user_by_username($user['email'])))
&& $found_udata !== false
)
) {
logScript("Username '{$user["username"]}' found in current system as '{$found_udata["username"]}', full name '{$found_udata["fullname"]}'");
$process_user_preferences($found_udata['ref'], $user);
continue;
}
if (in_array($user["usergroup"], $usergroups_not_created)) {
logScript("WARNING: User '{$user["username"]}' belongs to a user group that was not created as per the specification file. Skipping");
$users_not_created[] = $user["ref"];
fwrite($progress_fh, "\$users_not_created[] = {$user["ref"]};" . PHP_EOL);
continue;
}
db_begin_transaction(TX_SAVEPOINT);
$new_uref = new_user($user["username"], $usergroups_spec[$user["usergroup"]]);
if ($new_uref <= 0) {
logScript("ERROR: User could not be created - check user limit or user name conflicts.");
exit(1);
}
logScript("Created new user '{$user["username"]}' (ID #{$new_uref} | User group ID: {$usergroups_spec[$user["usergroup"]]})");
$valid_user_tbl_cols = array_diff(columns_in('user', null, null, true), ['ref']);
$save_user_stm = new PreparedStatementQuery();
$log_act_user_edit = [];
foreach ($user as $col => $value) {
if (!in_array($col, $valid_user_tbl_cols)) {
continue;
}
$save_user_stm->sql .= ($save_user_stm->sql !== '' ? ', ' : '') . "`{$col}` = ?";
$save_user_stm->parameters[] = is_int_loose($value) ? 'i' : 's';
$save_user_stm->parameters[] = $value;
$log_act_user_edit[$col] = $value;
}
if ($save_user_stm->sql !== '') {
ps_query(
"UPDATE `user` SET {$save_user_stm->sql} WHERE ref = ?",
[...$save_user_stm->parameters, 'i', $new_uref]
);
foreach ($log_act_user_edit as $col => $value) {
log_activity(null, LOG_CODE_EDITED, $value, 'user', $col, $new_uref);
}
logScript("Saved user details");
} else {
logScript("ERROR: failed to save user '{$user["username"]}'");
exit(1);
}
$process_user_preferences($new_uref, $user);
}
unset($src_users);
# ARCHIVE STATES
################
logScript("");
logScript("Importing archive states...");
if (!isset($archive_states_spec) || empty($archive_states_spec)) {
logScript("ERROR: Spec missing 'archive_states_spec'");
exit(1);
}
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$processed_archive_states = (isset($processed_archive_states) ? $processed_archive_states : array());
$src_archive_states = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "archive_states_export.json", "r+b"));
$dest_archive_states = get_workflow_states();
foreach ($src_archive_states as $archive_state) {
if (in_array($archive_state["ref"], $processed_archive_states)) {
continue;
}
logScript("Processing '{$archive_state["lang"]}' (ID #{$archive_state["ref"]})");
if (
array_key_exists($archive_state["ref"], $archive_states_spec)
&& in_array($archive_states_spec[$archive_state["ref"]], $dest_archive_states)
&& !is_null($archive_states_spec[$archive_state["ref"]])
) {
$lang_text = $lang["status{$archive_states_spec[$archive_state["ref"]]}"];
logScript("Found direct 1:1 mapping to #{$archive_states_spec[$archive_state["ref"]]} - {$lang_text}");
$processed_archive_states[] = $archive_state["ref"];
fwrite($progress_fh, "\$processed_archive_states[] = {$archive_state["ref"]};" . PHP_EOL);
continue;
} elseif (
array_key_exists($archive_state["ref"], $archive_states_spec)
&& !in_array($archive_states_spec[$archive_state["ref"]], $dest_archive_states)
) {
logScript("ERROR: Incorrect mapping? Attempted to map to workflow state #{$archive_states_spec[$archive_state["ref"]]}!");
exit(1);
}
if (array_key_exists($archive_state["ref"], $archive_states_spec) && is_null($archive_states_spec[$archive_state["ref"]])) {
$new_archive_state = rse_workflow_create_state(['name' => $archive_state['lang']]);
if ($new_archive_state === false) {
logScript("ERROR: Unable to create new workflow state!");
exit(1);
}
$dest_archive_states[] = $new_archive_state['code'];
$processed_archive_states[] = $archive_state['ref'];
fwrite($progress_fh, "\$processed_archive_states[] = {$archive_state['ref']};" . PHP_EOL);
logScript("Created new workflow state with code #{$new_archive_state['code']}");
}
}
unset($src_archive_states);
clear_query_cache('workflow');
# RESOURCE TYPES
################
logScript("");
logScript("Importing resource types...");
if (!isset($resource_types_spec) || empty($resource_types_spec)) {
logScript("ERROR: Spec missing 'resource_types_spec'");
exit(1);
}
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$processed_resource_types = (isset($processed_resource_types) ? $processed_resource_types : array());
$src_resource_types = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "resource_types_export.json", "r+b"));
$dest_resource_types = get_resource_types("", false);
foreach ($src_resource_types as $resource_type) {
if (in_array($resource_type["ref"], $processed_resource_types)) {
continue;
}
logScript("Processing #{$resource_type["ref"]} '{$resource_type["name"]}'");
if (!array_key_exists($resource_type["ref"], $resource_types_spec)) {
logScript("ERROR: resource_types_spec does not have a record for this resource type");
exit(1);
}
if (!is_null($resource_types_spec[$resource_type["ref"]])) {
if (!is_numeric($resource_types_spec[$resource_type["ref"]])) {
logScript("ERROR: Invalid mapped value!");
exit(1);
}
$found_rt_index = array_search($resource_types_spec[$resource_type["ref"]], array_column($dest_resource_types, "ref"));
if ($found_rt_index === false) {
logScript("ERROR: Unable to find destination resource type!");
exit(1);
}
$found_rt = $dest_resource_types[$found_rt_index];
logScript("Found direct 1:1 mapping to #{$found_rt["ref"]} '{$found_rt["name"]}'");
$processed_resource_types[] = $resource_type["ref"];
fwrite($progress_fh, "\$processed_resource_types[] = {$resource_type["ref"]};" . PHP_EOL);
continue;
}
// New record
ps_query(
"INSERT INTO resource_type(`name`, config_options, allowed_extensions, tab_name, push_metadata) VALUES (?, ?, ?, ?, ?, ?);",
[
's', trim($resource_type["name"]) != "" ? $resource_type["name"] : null,
's', trim($resource_type["config_options"]) != "" ? $resource_type["config_options"] : null,
's', trim($resource_type["allowed_extensions"]) != "" ? $resource_type["allowed_extensions"] : null,
's', trim($resource_type["tab_name"]) != "" ? $resource_type["tab_name"] : null,
'i', trim($resource_type["push_metadata"]) != "" ? $resource_type["push_metadata"] : null,
]
);
$new_rt_ref = sql_insert_id();
log_activity(null, LOG_CODE_EDITED, $resource_type["name"], 'resource_type', 'name', $new_rt_ref);
log_activity(null, LOG_CODE_EDITED, $resource_type["config_options"], 'resource_type', 'config_options', $new_rt_ref);
log_activity(null, LOG_CODE_EDITED, $resource_type["allowed_extensions"], 'resource_type', 'allowed_extensions', $new_rt_ref);
log_activity(null, LOG_CODE_EDITED, $resource_type["tab_name"], 'resource_type', 'tab_name', $new_rt_ref);
log_activity(null, LOG_CODE_EDITED, $resource_type["push_metadata"], 'resource_type', 'push_metadata', $new_rt_ref);
logScript("Created new record #{$new_rt_ref} '{$resource_type["name"]}'");
$resource_types_spec[$resource_type["ref"]] = $new_rt_ref;
$processed_resource_types[] = $resource_type["ref"];
fwrite(
$progress_fh,
"\$resource_types_spec[{$resource_type["ref"]}] = {$new_rt_ref};"
. PHP_EOL
. "\$processed_resource_types[] = {$resource_type["ref"]};"
. PHP_EOL
);
}
unset($src_resource_types);
unset($dest_resource_types);
# RESOURCE TYPE FIELDS
######################
logScript("");
logScript("Importing resource type fields...");
if (!isset($resource_type_fields_spec) || empty($resource_type_fields_spec)) {
logScript("ERROR: Spec missing 'resource_type_fields_spec'");
exit(1);
}
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$processed_resource_type_fields = (isset($processed_resource_type_fields) ? $processed_resource_type_fields : array());
$resource_type_fields_not_created = (isset($resource_type_fields_not_created) ? $resource_type_fields_not_created : array());
$src_resource_type_fields = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "resource_type_fields_export.json", "r+b"));
$dest_resource_type_fields = get_resource_type_fields("", "ref", "ASC", "", array());
$compatible_rtf_types = array(
FIELD_TYPE_TEXT_BOX_SINGLE_LINE => $TEXT_FIELD_TYPES,
FIELD_TYPE_TEXT_BOX_MULTI_LINE => $TEXT_FIELD_TYPES,
FIELD_TYPE_CHECK_BOX_LIST => $FIXED_LIST_FIELD_TYPES,
FIELD_TYPE_DROP_DOWN_LIST => $FIXED_LIST_FIELD_TYPES,
FIELD_TYPE_DATE_AND_OPTIONAL_TIME => $DATE_FIELD_TYPES,
FIELD_TYPE_TEXT_BOX_LARGE_MULTI_LINE => $TEXT_FIELD_TYPES,
FIELD_TYPE_EXPIRY_DATE => array(FIELD_TYPE_EXPIRY_DATE),
FIELD_TYPE_CATEGORY_TREE => $FIXED_LIST_FIELD_TYPES,
FIELD_TYPE_TEXT_BOX_FORMATTED_AND_TINYMCE => array(FIELD_TYPE_TEXT_BOX_FORMATTED_AND_TINYMCE),
FIELD_TYPE_DYNAMIC_KEYWORDS_LIST => $FIXED_LIST_FIELD_TYPES,
FIELD_TYPE_DATE => $DATE_FIELD_TYPES,
FIELD_TYPE_RADIO_BUTTONS => $FIXED_LIST_FIELD_TYPES,
FIELD_TYPE_WARNING_MESSAGE => array(FIELD_TYPE_WARNING_MESSAGE),
FIELD_TYPE_DATE_RANGE => array(FIELD_TYPE_DATE_RANGE)
);
foreach ($src_resource_type_fields as $src_rtf) {
if (in_array($src_rtf["ref"], $processed_resource_type_fields) || in_array($src_rtf["ref"], $resource_type_fields_not_created)) {
continue;
}
logScript("Processing #{$src_rtf["ref"]} '{$src_rtf["title"]}'");
if (!array_key_exists($src_rtf["ref"], $resource_type_fields_spec)) {
logScript("WARNING: Specification missing mapping for this resource type field! Skipping");
$resource_type_fields_not_created[] = $src_rtf["ref"];
fwrite($progress_fh, "\$resource_type_fields_not_created[] = {$src_rtf["ref"]};" . PHP_EOL);
continue;
}
// Check if we need to create this field
if (!(isset($resource_type_fields_spec[$src_rtf["ref"]]["create"]) && is_bool($resource_type_fields_spec[$src_rtf["ref"]]["create"]))) {
logScript("ERROR: invalid mapping configuration for mapped value. Expecting array type with index 'create' of type boolean.");
exit(1);
}
if (!$resource_type_fields_spec[$src_rtf["ref"]]["create"]) {
logScript("Mapping set to not be created. Skipping");
$resource_type_fields_not_created[] = $src_rtf["ref"];
fwrite($progress_fh, "\$resource_type_fields_not_created[] = {$src_rtf["ref"]};" . PHP_EOL);
continue;
}
/*
Check if we have a field mapped. Expected values:
- integer when we have a direct mapping
- null when a new field should be created
*/
if (
!(
(
isset($resource_type_fields_spec[$src_rtf["ref"]]["ref"])
&& (
is_int($resource_type_fields_spec[$src_rtf["ref"]]["ref"])
&& $resource_type_fields_spec[$src_rtf["ref"]]["ref"] > 0
)
)
|| is_null($resource_type_fields_spec[$src_rtf["ref"]]["ref"])
)
) {
logScript("ERROR: invalid mapping configuration for mapped value. Expecting array type with index 'ref' of type integer OR use 'null' to create new field.");
exit(1);
}
$mapped_rtf_ref = $resource_type_fields_spec[$src_rtf["ref"]]["ref"];
// This is merged as a new field
if (is_null($mapped_rtf_ref)) {
$db_rtf_known_columns = array_column(ps_query('DESCRIBE resource_type_field', [], -1, false), 'Field');
db_begin_transaction(TX_SAVEPOINT);
$new_rtf_ref = create_resource_type_field(
$src_rtf["title"],
$resource_types_spec[$src_rtf["resource_type"]],
$src_rtf["type"],
$src_rtf["name"],
$src_rtf["keywords_index"]
);
if ($new_rtf_ref === false) {
logScript("ERROR: unable to create new resource type field!");
exit(1);
}
// IMPORTANT: we explicitly don't escape SQL values in this case as this should be the exact value stored in the SRC DB
$sql = "";
foreach ($src_rtf as $column => $value) {
if (
// SRC may be very old and contain columns no longer in use which are unavailable on DEST
!in_array($column, $db_rtf_known_columns)
// Ignore columns that have been used for creating this field
|| in_array($column, array("ref", "name", "title", "type", "keywords_index", "resource_type"))
) {
continue;
}
if (trim($sql) != "") {
$sql .= ", ";
}
$col_val = trim($value) == "" ? null : $value;
$sql .= "`{$column}` = ?";
$sql_params[] = 'i';
$sql_params[] = $col_val;
log_activity(null, LOG_CODE_EDITED, $col_val, 'resource_type_field', $column, $new_rtf_ref);
}
$sql_params[] = 'i';
$sql_params[] = $new_rtf_ref;
ps_query("UPDATE resource_type_field SET {$sql} WHERE ref = ?", $sql_params);
logScript("Created new record #{$new_rtf_ref} '{$src_rtf["title"]}'");
$resource_type_fields_spec[$src_rtf["ref"]] = array("create" => true, "ref" => $new_rtf_ref);
$processed_resource_type_fields[] = $src_rtf["ref"];
fwrite(
$progress_fh,
"\$resource_type_fields_spec[{$src_rtf["ref"]}] = array(\"create\" => true, \"ref\" => {$new_rtf_ref});"
. PHP_EOL
. "\$processed_resource_type_fields[] = {$src_rtf["ref"]};"
. PHP_EOL
);
$new_rtf_data = $src_rtf;
$new_rtf_data["ref"] = $new_rtf_ref;
$new_rtf_data["resource_type"] = $resource_types_spec[$src_rtf["resource_type"]];
$dest_resource_type_fields[] = $new_rtf_data;
unset($new_rtf_ref);
unset($new_rtf_data);
db_end_transaction(TX_SAVEPOINT);
continue;
}
$found_rtf_index = array_search($mapped_rtf_ref, array_column($dest_resource_type_fields, "ref"));
if ($found_rtf_index === false) {
logScript("ERROR: Unable to find destination resource type field!");
exit(1);
}
$found_rtf = $dest_resource_type_fields[$found_rtf_index];
logScript("Found direct 1:1 mapping to #{$found_rtf["ref"]} '{$found_rtf["title"]}'");
if (!in_array($found_rtf["type"], $compatible_rtf_types[$src_rtf["type"]])) {
$compat_rtf_type_names_msg = '';
foreach ($compatible_rtf_types[$found_rtf["type"]] as $compatible_rtf_type) {
$compat_rtf_type_names_msg .= PHP_EOL . " - {$lang[$field_types[$compatible_rtf_type]]}";
}
logScript("ERROR: incompatible types! Consider mapping to a field with one of these types: {$compat_rtf_type_names_msg}");
exit(1);
}
$processed_resource_type_fields[] = $src_rtf["ref"];
fwrite($progress_fh, "\$processed_resource_type_fields[] = {$src_rtf["ref"]};" . PHP_EOL);
if ($src_rtf["type"] == FIELD_TYPE_CATEGORY_TREE && $found_rtf["type"] != FIELD_TYPE_CATEGORY_TREE) {
logScript("WARNING: SRC field is a category type and DEST field is a different fixed list type. THIS WILL FLATTEN THE CATEGORY TREE!");
}
}
unset($src_resource_type_fields);
# NODES
#######
logScript("");
logScript("Importing nodes...");
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$nodes_spec = $nodes_spec ?? [];
$new_nodes_mapping = $new_nodes_mapping ?? [];
$nodes_not_created = $nodes_not_created ?? [];
$src_nodes = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "nodes_export.json", "r+b"));
$dest_node_refs = ps_array('SELECT ref AS `value` FROM node', array());
foreach ($src_nodes as $src_node) {
if (array_key_exists($src_node['ref'], $new_nodes_mapping) || in_array($src_node['ref'], $nodes_not_created)) {
continue;
}
logScript("Processing #{$src_node["ref"]} '{$src_node["name"]}'");
// Check if the specification has a mapping defined for this node to other DEST node(s).
if (isset($nodes_spec[$src_node['ref']])) {
if (!is_array($nodes_spec[$src_node['ref']])) {
logScript('ERROR: Invalid nodes specification! Reason: expected a list of nodes, received type is ' . gettype($nodes_spec[$src_node['ref']]));
exit(1);
}
$nodes_spec[$src_node['ref']] = array_filter($nodes_spec[$src_node['ref']], 'is_int_loose');
if (empty($nodes_spec[$src_node['ref']])) {
logScript('ERROR: Invalid nodes specification! Reason: no mapping defined.');
exit(1);
}
// Safe check: error if any of the mappings for this node is invalid (ie ref doesn't exist).
$found_invalid_nodes_map = array_diff($nodes_spec[$src_node['ref']], $dest_node_refs);
if (!empty($found_invalid_nodes_map)) {
logScript('ERROR: Invalid DEST node(s) mapping found: ' . implode(', ', $found_invalid_nodes_map));
exit(1);
}
logScript('Found direct mapping to DEST node(s): ' . implode(', ', $nodes_spec[$src_node['ref']]));
continue;
}
// If this nodes' resource type field was specified to not be created on the DEST system (via $resource_type_fields_spec), skip it
if (in_array($src_node["resource_type_field"], $resource_type_fields_not_created)) {
logScript("Skipping as resource type field was not created on the destination system!");
$nodes_not_created[] = $src_node["ref"];
fwrite($progress_fh, "\$nodes_not_created[] = {$src_node["ref"]};" . PHP_EOL);
continue;
}
$mapped_rtf_ref = $resource_type_fields_spec[$src_node["resource_type_field"]]["ref"];
$found_rtf_index = array_search($mapped_rtf_ref, array_column($dest_resource_type_fields, "ref"));
if ($found_rtf_index === false) {
logScript("ERROR: Unable to find destination resource type field!");
exit(1);
}
$found_rtf = $dest_resource_type_fields[$found_rtf_index];
// Determine parent node
if (
$found_rtf["type"] == FIELD_TYPE_CATEGORY_TREE
&& (!is_null($src_node["parent"]) || trim($src_node["parent"]) != "")
&& in_array($src_node["parent"], $nodes_not_created)
) {
logScript("ERROR: Unable to create new node because its parent was not created!");
exit(1);
} elseif (
$found_rtf["type"] == FIELD_TYPE_CATEGORY_TREE
&& (!is_null($src_node["parent"]) || trim($src_node["parent"]) != "")
&& !in_array($src_node["parent"], $nodes_not_created)
&& isset($new_nodes_mapping[$src_node["parent"]])
) {
$node_parent = $new_nodes_mapping[$src_node["parent"]];
$node_parent_sql = ' AND parent = ?';
$node_parent_sql_params = ['i', $node_parent];
} else {
$node_parent = null;
$node_parent_sql = '';
$node_parent_sql_params = [];
}
// Check if we can find an existing node for this metadata field (taking into account language translations as well)
$all_nodes_for_rtf = ps_query(
"SELECT ref, `name` FROM node WHERE resource_type_field = ?{$node_parent_sql}",
array_merge(['i', $mapped_rtf_ref], $node_parent_sql_params)
);
$found_matching_node_i18n = get_node_by_name($all_nodes_for_rtf, $src_node['name'], true);
if (!empty($found_matching_node_i18n)) {
logScript("Found matching node after translation: '{$found_matching_node_i18n['name']}'");
$new_nodes_mapping[$src_node['ref']] = $found_matching_node_i18n['ref'];
fwrite($progress_fh, "\$new_nodes_mapping[{$src_node['ref']}] = {$found_matching_node_i18n['ref']};" . PHP_EOL);
} else {
db_begin_transaction(TX_SAVEPOINT);
$new_node_ref = set_node(null, $mapped_rtf_ref, $src_node["name"], $node_parent, "");
if ($new_node_ref === false) {
logScript("ERROR: unable to create new node!");
exit(1);
}
logScript("Created new node record #{$new_node_ref} '{$src_node["name"]}'. Node set for resource type field {$mapped_rtf_ref}");
$new_nodes_mapping[$src_node["ref"]] = $new_node_ref;
fwrite($progress_fh, "\$new_nodes_mapping[{$src_node["ref"]}] = {$new_node_ref};" . PHP_EOL);
db_end_transaction(TX_SAVEPOINT);
}
}
unset($src_nodes, $dest_node_refs, $all_nodes_for_rtf);
# RESOURCES
###########
logScript("");
logScript("Importing resources...");
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$resources_mapping = (isset($resources_mapping) ? $resources_mapping : array());
$src_resources = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "resources_export.json", "r+b"));
foreach ($src_resources as $src_resource) {
if (array_key_exists($src_resource["ref"], $resources_mapping)) {
continue;
}
logScript("Processing #{$src_resource["ref"]} | resource_type: {$src_resource["resource_type"]} | archive: {$src_resource["archive"]} | created_by: {$src_resource["created_by"]}");
if (
!array_key_exists($src_resource["archive"], $archive_states_spec)
|| !in_array($archive_states_spec[$src_resource["archive"]], $dest_archive_states)
) {
logScript("ERROR: Invalid resource archive state! Please check archive_states_spec or dest_archive_states.");
exit(1);
}
$created_by = $userref ?? -1;
if (!in_array($src_resource["created_by"], $users_not_created) && isset($usernames_mapping[$src_resource["created_by"]])) {
$created_by = $usernames_mapping[$src_resource["created_by"]];
}
db_begin_transaction(TX_SAVEPOINT);
$new_resource_ref = create_resource(
$resource_types_spec[$src_resource["resource_type"]],
$archive_states_spec[$src_resource["archive"]],
$created_by,
$lang["createdfrommergerssystems"]
);
if ($new_resource_ref === false) {
logScript("ERROR: unable to create new resource!");
exit(1);
}
// Move across the resource access. Custom access gets remapped.
if (in_array($src_resource['access'], RESOURCE_ACCESS_TYPES)) {
$new_resource_acccess = $src_resource['access'] == RESOURCE_ACCESS_CUSTOM_GROUP ? $custom_access_new_value_spec : $src_resource['access'];
} else {
logScript("ERROR: unknown resource access type - '{$src_resource['access']}'");
exit(1);
}
$new_resource_data = [
'creation_date' => $src_resource['creation_date'] ?: null,
'rating' => $src_resource['rating'] !== '' ? (int) $src_resource['rating'] : null,
'user_rating' => $src_resource['user_rating'] !== '' ? (int) $src_resource['user_rating'] : null,
'access' => (int) $new_resource_acccess,
'mapzoom' => $src_resource['mapzoom'] !== '' ? (int) $src_resource['mapzoom'] : null,
'modified' => $src_resource['modified'] ?: null,
'geo_lat' => $src_resource['geo_lat'] !== '' ? $src_resource['geo_lat'] : null,
'geo_long' => $src_resource['geo_long'] !== '' ? $src_resource['geo_long'] : null,
];
$new_resource_data = array_diff($new_resource_data, array_filter($new_resource_data, 'is_null'));
if (!empty($new_resource_data) && !put_resource_data($new_resource_ref, $new_resource_data)) {
logScript("ERROR: unable to update the new resource properties!");
exit(1);
}
try {
$src_file_url = $call_src_api(
'get_resource_path',
[
'ref' => $src_resource['ref'],
'size' => '',
'generate' => 0,
'extension' => $src_resource['file_extension'],
]
);
} catch (RuntimeException $e) {
logScript("Response: {$e->getMessage()}");
$src_file_url = '';
}
if ($src_file_url === '') {
logScript('ERROR: unable to fetch the original SRC file!');
exit(1);
}
// we don't want to extract, revert or autorotate. This is a basic file pull into the DEST system from a remote SRC
$job_data = array(
"resource" => $new_resource_ref,
"extract" => false,
"revert" => false,
"autorotate" => false,
"upload_file_by_url" => $src_file_url,
);
$job_code = "merge_rs_systems_{$src_resource["ref"]}_{$new_resource_ref}_" . md5("{$src_resource["ref"]}_{$new_resource_ref}");
$job_failure_lang = "Merge RS systems - upload processing fail "
. str_replace(
array('%ref', '%title'),
array($new_resource_ref, ""),
$lang["ref-title"]
);
$job_queue_added = job_queue_add("upload_processing", $job_data, $userref, "", "", $job_failure_lang, $job_code);
if ($job_queue_added === false) {
logScript("ERROR: unable to create job queue for uploading (copying) resource original file from SRC system");
exit(1);
} elseif (is_string($job_queue_added) && trim($job_queue_added) != "") {
logScript("ERROR: unable to create job queue. Reason: '{$job_queue_added}'");
exit(1);
}
resource_log(
$new_resource_ref,
LOG_CODE_SYSTEM,
"",
"merge_rs_systems: SRC resource ref was #{$src_resource["ref"]}",
$src_resource["ref"],
$new_resource_ref
);
// If configured, also store the SRC resource ID in a text field
if ($rtf_src_resource_ref > 0) {
$rtf_src_resource_ref_data = get_resource_type_field($rtf_src_resource_ref);
$ursrr_errors = [];
if (
$rtf_src_resource_ref_data !== false
&& $rtf_src_resource_ref_data['type'] == FIELD_TYPE_TEXT_BOX_SINGLE_LINE
&& !update_field($new_resource_ref, $rtf_src_resource_ref, $src_resource['ref'], $ursrr_errors)
) {
logScript("WARNING: unable to update the new resource and store the source resource ID! Reason: " . implode('; ', $ursrr_errors));
}
}
logScript("Created new record #{$new_resource_ref}");
$resources_mapping[$src_resource["ref"]] = $new_resource_ref;
fwrite($progress_fh, "\$resources_mapping[{$src_resource["ref"]}] = {$new_resource_ref};" . PHP_EOL);
db_end_transaction(TX_SAVEPOINT);
}
// If specification is configured to add certain nodes to all imported resources, do it now
if (!empty($nodes_applied_to_all_merged_resources)) {
if (
!isset($processed_nodes_applied_to_all_merged_resources)
&& add_resource_nodes_multi(array_values($resources_mapping), $nodes_applied_to_all_merged_resources, false, true)
) {
logScript('Updated all resources with the following SRC node IDs: ' . implode(', ', $nodes_applied_to_all_merged_resources));
fwrite($progress_fh, '$processed_nodes_applied_to_all_merged_resources = true;' . PHP_EOL);
} elseif (isset($processed_nodes_applied_to_all_merged_resources)) {
logScript('Nodes that should be applied to all merged resources have already been added. Skipping');
} else {
logScript('ERROR: Failed to update all resources with the following SRC node IDs: ' . implode(', ', $nodes_applied_to_all_merged_resources));
exit(1);
}
}
unset($src_resources);
# RESOURCE NODES
################
logScript("");
logScript("Importing resource nodes...");
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$processed_resource_nodes = (isset($processed_resource_nodes) ? $processed_resource_nodes : array());
$src_resource_nodes = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "resource_nodes_export.json", "r+b"));
foreach ($src_resource_nodes as $src_rn) {
if (in_array("{$src_rn["resource"]}_{$src_rn["node"]}", $processed_resource_nodes)) {
continue;
}
logScript("Processing SRC resource #{$src_rn["resource"]} and node #{$src_rn["node"]}");
if (!array_key_exists($src_rn["resource"], $resources_mapping)) {
logScript("WARNING: Unable to find a resource mapping. Skipping");
$processed_resource_nodes[] = "{$src_rn["resource"]}_{$src_rn["node"]}";
fwrite($progress_fh, "\$processed_resource_nodes[] = \"{$src_rn["resource"]}_{$src_rn["node"]}\";" . PHP_EOL);
continue;
}
// If specification defined a mapping for a SRC node, apply it
if (isset($nodes_spec[$src_rn['node']])) {
if (add_resource_nodes($resources_mapping[$src_rn['resource']], $nodes_spec[$src_rn['node']], false, true)) {
logScript("Found direct mapping ==> added to DEST resource #{$resources_mapping[$src_rn['resource']]} the mapped DEST nodes: " . implode(', ', $nodes_spec[$src_rn['node']]));
$processed_resource_nodes[] = "{$src_rn["resource"]}_{$src_rn["node"]}";
fwrite($progress_fh, "\$processed_resource_nodes[] = \"{$src_rn["resource"]}_{$src_rn["node"]}\";" . PHP_EOL);
} else {
logScript("WARNING: Failed to add found mapped DEST nodes to the resource (ie. resource_node)");
}
continue;
}
if (in_array($src_rn["node"], $nodes_not_created)) {
logScript("Skipping as the node was not created on the destination system!");
$processed_resource_nodes[] = "{$src_rn["resource"]}_{$src_rn["node"]}";
fwrite($progress_fh, "\$processed_resource_nodes[] = \"{$src_rn["resource"]}_{$src_rn["node"]}\";" . PHP_EOL);
continue;
}
if (!isset($new_nodes_mapping[$src_rn["node"]])) {
logScript("WARNING: unable to find a node mapping!");
$processed_resource_nodes[] = "{$src_rn["resource"]}_{$src_rn["node"]}";
fwrite($progress_fh, "\$processed_resource_nodes[] = \"{$src_rn["resource"]}_{$src_rn["node"]}\";" . PHP_EOL);
continue;
}
ps_query(
"INSERT INTO resource_node (resource, node, hit_count, new_hit_count) VALUES (?, ?, ?, ?)",
[
'i', $resources_mapping[$src_rn["resource"]],
'i', $new_nodes_mapping[$src_rn["node"]],
'i', $src_rn["hit_count"],
'i', $src_rn["new_hit_count"],
]
);
$processed_resource_nodes[] = "{$src_rn["resource"]}_{$src_rn["node"]}";
fwrite($progress_fh, "\$processed_resource_nodes[] = \"{$src_rn["resource"]}_{$src_rn["node"]}\";" . PHP_EOL);
}
unset($src_resource_nodes);
# RESOURCE DIMENSIONS
#####################
logScript("");
logScript("Importing resource dimensions...");
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$processed_resource_dimensions = (isset($processed_resource_dimensions) ? $processed_resource_dimensions : array());
$src_resource_dimensions = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "resource_dimensions_export.json", "r+b"));
foreach ($src_resource_dimensions as $src_rdms) {
$process_rdms_value = "{$src_rdms["resource"]}_{$src_rdms["width"]}_{$src_rdms["height"]}_"
. md5("{$src_rdms["resource"]}|{$src_rdms["width"]}|{$src_rdms["height"]}|{$src_rdms["file_size"]}|{$src_rdms["resolution"]}|{$src_rdms["unit"]}|{$src_rdms["page_count"]}");
if (in_array($process_rdms_value, $processed_resource_dimensions)) {
continue;
}
logScript("Processing dimensions for resource #{$src_rdms["resource"]} | width: {$src_rdms["width"]} | height: {$src_rdms["height"]} | page_count: #{$src_rdms["page_count"]}");
if (!array_key_exists($src_rdms["resource"], $resources_mapping)) {
logScript("WARNING: Unable to find a resource mapping. Skipping");
$processed_resource_dimensions[] = $process_rdms_value;
fwrite($progress_fh, "\$processed_resource_dimensions[] = \"{$process_rdms_value}\";" . PHP_EOL);
continue;
}
ps_query(
"INSERT INTO resource_dimensions (resource, width, height, file_size, resolution, unit, page_count) VALUES (?, ?, ?, ?, ?, ?, ?)",
[
'i', $resources_mapping[$src_rdms["resource"]],
'i', $src_rdms["width"],
'i', $src_rdms["height"],
'i', $src_rdms["file_size"],
'i', $src_rdms["resolution"],
's', $src_rdms["unit"],
'i', is_int_loose($src_rdms["page_count"]) ? $src_rdms["page_count"] : null,
]
);
$processed_resource_dimensions[] = $process_rdms_value;
fwrite($progress_fh, "\$processed_resource_dimensions[] = \"{$process_rdms_value}\";" . PHP_EOL);
}
unset($src_resource_dimensions);
# RESOURCE RELATED
##################
logScript("");
logScript("Importing resource related...");
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$processed_resource_related = (isset($processed_resource_related) ? $processed_resource_related : array());
$processed_resource_related = array_flip($processed_resource_related);
$src_resource_related = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "resource_related_export.json", "r+b"));
$src_resource_related_chunks = array_chunk($src_resource_related, 2000);
foreach ($src_resource_related_chunks as $src_resource_related_chunk) {
$insertvals = $insertvals_bparams = [];
$temp_processed_related = [];
$temp_processed_related_log = "";
foreach ($src_resource_related_chunk as $src_rr) {
if (isset($processed_resource_related["{$src_rr["resource"]}_{$src_rr["related"]}"])) {
continue;
}
logScript("Processing resource related - resource: #{$src_rr["resource"]} | related: #{$src_rr["related"]}");
if (
!array_key_exists($src_rr["resource"], $resources_mapping)
|| !array_key_exists($src_rr["related"], $resources_mapping)
) {
logScript("WARNING: Unable to find a resource mapping for either resource or related. Skipping");
$processed_resource_related["{$src_rr["resource"]}_{$src_rr["related"]}"] = true;
fwrite($progress_fh, "\$processed_resource_related[] = \"{$src_rr["resource"]}_{$src_rr["related"]}\";" . PHP_EOL);
continue;
}
$insertvals[] = "(?, ?)";
$insertvals_bparams = array_merge(
$insertvals_bparams,
ps_param_fill([$resources_mapping[$src_rr["resource"]], $resources_mapping[$src_rr["related"]]], 'i')
);
$temp_processed_related["{$src_rr["resource"]}_{$src_rr["related"]}"] = true;
$temp_processed_related_log .= "\$processed_resource_related[] = \"{$src_rr["resource"]}_{$src_rr["related"]}\";" . PHP_EOL;
}
if (count($insertvals) > 0) {
ps_query("INSERT INTO resource_related (resource, related) VALUES " . implode(",", $insertvals), $insertvals_bparams);
$processed_resource_related = array_merge($processed_resource_related, $temp_processed_related);
fwrite($progress_fh, $temp_processed_related_log);
}
}
unset($src_resource_related);
# RESOURCE ALTERNATIVE FILES
############################
logScript("");
logScript("Importing resource alternative files...");
fwrite($progress_fh, PHP_EOL . PHP_EOL);
$processed_resource_alt_files = (isset($processed_resource_alt_files) ? $processed_resource_alt_files : array());
$src_resource_alt_files = $json_decode_file_data($get_file_handler($folder_path . DIRECTORY_SEPARATOR . "resource_alt_files_export.json", "r+b"));
foreach ($src_resource_alt_files as $src_raf) {
if (in_array("{$src_raf["resource"]}_{$src_raf["ref"]}", $processed_resource_alt_files)) {
continue;
}
logScript("Processing resource alternative file - resource: #{$src_raf["resource"]} | alternative: #{$src_raf["ref"]}");
if (!array_key_exists($src_raf["resource"], $resources_mapping)) {
logScript("WARNING: Unable to find a resource mapping. Skipping");
$processed_resource_alt_files[] = "{$src_raf["resource"]}_{$src_raf["ref"]}";
fwrite($progress_fh, "\$processed_resource_alt_files[] = \"{$src_raf["resource"]}_{$src_raf["ref"]}\";" . PHP_EOL);
continue;
}
db_begin_transaction(TX_SAVEPOINT);
$new_alternative_ref = add_alternative_file(
$resources_mapping[$src_raf["resource"]],
$src_raf["name"],
$src_raf["description"],
$src_raf["file_name"],
$src_raf["file_extension"],
$src_raf["file_size"],
$src_raf["alt_type"]
);
try {
$src_alt_file_url = $call_src_api(
'get_resource_path',
[
'ref' => $src_raf['resource'],
'size' => '',
'generate' => 0,
'extension' => $src_raf['file_extension'],
'alternative' => $src_raf['ref'],
]
);
} catch (RuntimeException $e) {
logScript("Response: {$e->getMessage()}");
$src_alt_file_url = '';
}
if ($src_alt_file_url === '') {
logScript('ERROR: unable to fetch the SRC alternative (original) file!');
exit(1);
}
// we don't want to extract, revert or autorotate. This is a basic file pull into the DEST system from a remote SRC
$job_data = array(
"resource" => $resources_mapping[$src_raf["resource"]],
"extract" => false,
"revert" => false,
"autorotate" => false,
"alternative" => $new_alternative_ref,
"upload_file_by_url" => $src_alt_file_url,
"extension" => $src_raf["file_extension"],
);
$job_code = "merge_rs_systems_{$src_raf["ref"]}_{$resources_mapping[$src_raf["resource"]]}_"
. md5("{$src_raf["ref"]}_{$resources_mapping[$src_raf["resource"]]}");
$job_failure_lang = "Merge RS systems - alternative upload processing fail "
. str_replace(
array('%ref', '%title'),
array($src_raf["ref"], ""),
$lang["ref-title"]
);
$job_queue_added = job_queue_add("upload_processing", $job_data, $userref, "", "", $job_failure_lang, $job_code);
if ($job_queue_added === false) {
logScript("ERROR: unable to create job queue for uploading (copying) resource alternative file from SRC system");
exit(1);
} elseif (is_string($job_queue_added) && trim($job_queue_added) != "") {
logScript("ERROR: unable to create job queue. Reason: '{$job_queue_added}'");
exit(1);
}
$processed_resource_alt_files[] = "{$src_raf["resource"]}_{$src_raf["ref"]}";
fwrite($progress_fh, "\$processed_resource_alt_files[] = \"{$src_raf["resource"]}_{$src_raf["ref"]}\";" . PHP_EOL);
db_end_transaction(TX_SAVEPOINT);
}
unset($src_resource_alt_files);
logScript("");
logScript("Script ran successfully!");
fclose($progress_fh);
}