Skip to content

Commit

Permalink
Merge branch '4.2-dev' into patch-37017
Browse files Browse the repository at this point in the history
  • Loading branch information
fancyFranci committed Dec 3, 2022
2 parents edc337d + 76367bc commit 66993a2
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 133 deletions.
275 changes: 143 additions & 132 deletions administrator/components/com_admin/script.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,146 +239,154 @@ protected function uninstallRepeatableFieldsPlugin()
// Ensure the FieldsHelper class is loaded for the Repeatable fields plugin we're about to remove
\JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php');

try {
$db->transactionStart();
// Get the FieldsModelField, we need it in a sec
$fieldModel = $app->bootComponent('com_fields')->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]);
/** @var FieldModel $fieldModel */

// Get the FieldsModelField, we need it in a sec
$fieldModel = $app->bootComponent('com_fields')->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]);
/** @var FieldModel $fieldModel */
// Now get a list of all `repeatable` custom field instances
$db->setQuery(
$db->getQuery(true)
->select('*')
->from('#__fields')
->where($db->quoteName('type') . ' = ' . $db->quote('repeatable'))
);

// Now get a list of all `repeatable` custom field instances
$db->setQuery(
$db->getQuery(true)
->select('*')
->from('#__fields')
->where($db->quoteName('type') . ' = ' . $db->quote('repeatable'))
);
// Execute the query and iterate over the `repeatable` instances
foreach ($db->loadObjectList() as $row) {
// Skip broken rows - just a security measure, should not happen
if (!isset($row->fieldparams) || !($oldFieldparams = json_decode($row->fieldparams)) || !is_object($oldFieldparams)) {
continue;
}

// Execute the query and iterate over the `repeatable` instances
foreach ($db->loadObjectList() as $row) {
// Skip broken rows - just a security measure, should not happen
if (!isset($row->fieldparams) || !($oldFieldparams = json_decode($row->fieldparams)) || !is_object($oldFieldparams)) {
continue;
}
// First get this field's values for later data migration, so if this fails it happens before saving new subfields
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__fields_values'))
->where($db->quoteName('field_id') . ' = ' . $row->id);
$db->setQuery($query);
$rowFieldValues = $db->loadObjectList();

/**
* We basically want to transform this `repeatable` type into a `subfields` type. While $oldFieldparams
* holds the `fieldparams` of the `repeatable` type, $newFieldparams shall hold the `fieldparams`
* of the `subfields` type.
*/
$newFieldparams = [
'repeat' => '1',
'options' => [],
];

/**
* We basically want to transform this `repeatable` type into a `subfields` type. While $oldFieldparams
* holds the `fieldparams` of the `repeatable` type, $newFieldparams shall hold the `fieldparams`
* of the `subfields` type.
*/
$newFieldparams = [
'repeat' => '1',
'options' => [],
];

/**
* This array is used to store the mapping between the name of form fields from Repeatable field
* with ID of the child-fields. It will then be used to migrate data later
*/
$mapping = [];

/**
* Store name of media fields which we need to convert data from old format (string) to new
* format (json) during the migration
*/
$mediaFields = [];

// If this repeatable fields actually had child-fields (normally this is always the case)
if (isset($oldFieldparams->fields) && is_object($oldFieldparams->fields)) {
// Small counter for the child-fields (aka sub fields)
$newFieldCount = 0;

// Iterate over the sub fields
foreach (get_object_vars($oldFieldparams->fields) as $oldField) {
// Used for field name collision prevention
$fieldname_prefix = '';
$fieldname_suffix = 0;

// Try to save the new sub field in a loop because of field name collisions
while (true) {
/**
* We basically want to create a completely new custom fields instance for every sub field
* of the `repeatable` instance. This is what we use $data for, we create a new custom field
* for each of the sub fields of the `repeatable` instance.
*/
$data = [
'context' => $row->context,
'group_id' => $row->group_id,
'title' => $oldField->fieldname,
'name' => (
$fieldname_prefix
. $oldField->fieldname
. ($fieldname_suffix > 0 ? ('_' . $fieldname_suffix) : '')
),
'label' => $oldField->fieldname,
'default_value' => $row->default_value,
'type' => $oldField->fieldtype,
'description' => $row->description,
'state' => '1',
'params' => $row->params,
'language' => '*',
'assigned_cat_ids' => [-1],
'only_use_in_subform' => 1,
];

// `number` is not a valid custom field type, so use `text` instead.
if ($data['type'] == 'number') {
$data['type'] = 'text';
}
/**
* This array is used to store the mapping between the name of form fields from Repeatable field
* with ID of the child-fields. It will then be used to migrate data later
*/
$mapping = [];

/**
* Store name of media fields which we need to convert data from old format (string) to new
* format (json) during the migration
*/
$mediaFields = [];

// If this repeatable fields actually had child-fields (normally this is always the case)
if (isset($oldFieldparams->fields) && is_object($oldFieldparams->fields)) {
// Small counter for the child-fields (aka sub fields)
$newFieldCount = 0;

// Iterate over the sub fields
foreach (get_object_vars($oldFieldparams->fields) as $oldField) {
// Used for field name collision prevention
$fieldname_prefix = '';
$fieldname_suffix = 0;

// Try to save the new sub field in a loop because of field name collisions
while (true) {
/**
* We basically want to create a completely new custom fields instance for every sub field
* of the `repeatable` instance. This is what we use $data for, we create a new custom field
* for each of the sub fields of the `repeatable` instance.
*/
$data = [
'context' => $row->context,
'group_id' => $row->group_id,
'title' => $oldField->fieldname,
'name' => (
$fieldname_prefix
. $oldField->fieldname
. ($fieldname_suffix > 0 ? ('_' . $fieldname_suffix) : '')
),
'label' => $oldField->fieldname,
'default_value' => $row->default_value,
'type' => $oldField->fieldtype,
'description' => $row->description,
'state' => '1',
'params' => $row->params,
'language' => '*',
'assigned_cat_ids' => [-1],
'only_use_in_subform' => 1,
];

if ($data['type'] == 'media') {
$mediaFields[] = $oldField->fieldname;
}
// `number` is not a valid custom field type, so use `text` instead.
if ($data['type'] == 'number') {
$data['type'] = 'text';
}

if ($data['type'] == 'media') {
$mediaFields[] = $oldField->fieldname;
}

// Reset the state because else \Joomla\CMS\MVC\Model\AdminModel will take an already
// existing value (e.g. from previous save) and do an UPDATE instead of INSERT.
$fieldModel->setState('field.id', 0);

// If an error occurred when trying to save this.
if (!$fieldModel->save($data)) {
// If the error is, that the name collided, increase the collision prevention
$error = $fieldModel->getError();

if ($error == 'COM_FIELDS_ERROR_UNIQUE_NAME') {
// If this is the first time this error occurs, set only the prefix
if ($fieldname_prefix == '') {
$fieldname_prefix = ($row->name . '_');
} else {
// Else increase the suffix
$fieldname_suffix++;
}

// And start again with the while loop.
continue 1;
// Reset the state because else \Joomla\CMS\MVC\Model\AdminModel will take an already
// existing value (e.g. from previous save) and do an UPDATE instead of INSERT.
$fieldModel->setState('field.id', 0);

// If an error occurred when trying to save this.
if (!$fieldModel->save($data)) {
// If the error is, that the name collided, increase the collision prevention
$error = $fieldModel->getError();

if ($error == 'COM_FIELDS_ERROR_UNIQUE_NAME') {
// If this is the first time this error occurs, set only the prefix
if ($fieldname_prefix == '') {
$fieldname_prefix = ($row->name . '_');
} else {
// Else increase the suffix
$fieldname_suffix++;
}

// Else bail out with the error. Something is totally wrong.
throw new \Exception($error);
// And start again with the while loop.
continue 1;
}

// Break out of the while loop, saving was successful.
break 1;
// Else bail out with the error. Something is totally wrong.
throw new \Exception($error);
}

// Get the newly created id
$subfield_id = $fieldModel->getState('field.id');
// Break out of the while loop, saving was successful.
break 1;
}

// Really check that it is valid
if (!is_numeric($subfield_id) || $subfield_id < 1) {
throw new \Exception('Something went wrong.');
}
// Get the newly created id
$subfield_id = $fieldModel->getState('field.id');

// And tell our new `subfields` field about his child
$newFieldparams['options'][('option' . $newFieldCount)] = [
'customfield' => $subfield_id,
'render_values' => '1',
];
// Really check that it is valid
if (!is_numeric($subfield_id) || $subfield_id < 1) {
throw new \Exception('Something went wrong.');
}

$newFieldCount++;
// And tell our new `subfields` field about his child
$newFieldparams['options'][('option' . $newFieldCount)] = [
'customfield' => $subfield_id,
'render_values' => '1',
];

$mapping[$oldField->fieldname] = 'field' . $subfield_id;
}
$newFieldCount++;

$mapping[$oldField->fieldname] = 'field' . $subfield_id;
}
}

try {
$db->transactionStart();

// Write back the changed stuff to the database
$db->setQuery(
Expand All @@ -389,14 +397,8 @@ protected function uninstallRepeatableFieldsPlugin()
->where($db->quoteName('id') . ' = ' . $db->quote($row->id))
)->execute();

// Migrate data for this field
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__fields_values'))
->where($db->quoteName('field_id') . ' = ' . $row->id);
$db->setQuery($query);

foreach ($db->loadObjectList() as $rowFieldValue) {
// Migrate field values for this field
foreach ($rowFieldValues as $rowFieldValue) {
// Do not do the version if no data is entered for the custom field this item
if (!$rowFieldValue->value) {
continue;
Expand Down Expand Up @@ -444,11 +446,20 @@ protected function uninstallRepeatableFieldsPlugin()
->update($db->quoteName('#__fields_values'))
->set($db->quoteName('value') . ' = ' . $db->quote(json_encode($newFieldValue)))
->where($db->quoteName('field_id') . ' = ' . $rowFieldValue->field_id)
->where($db->quoteName('item_id') . ' =' . $rowFieldValue->item_id);
->where($db->quoteName('item_id') . ' = ' . $db->quote($rowFieldValue->item_id));
$db->setQuery($query)
->execute();
}

$db->transactionCommit();
} catch (\Exception $e) {
$db->transactionRollback();
throw $e;
}
}

try {
$db->transactionStart();

// Now, unprotect the plugin so we can uninstall it
$db->setQuery(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,56 @@

define('_JOOMLA_UPDATE', 1);

require_once __DIR__ . '/finalisation.php';
include_once __DIR__ . '/finalisation.php';

if (!function_exists('clearFileInOPCache')) {
/**
* Invalidate a file in OPcache. We need to define the function here because finalizeUpdate
* function called by finalizeRestore function depends on this method
*
* Only applies if the file has a .php extension.
*
* @param string $file The filepath to clear from OPcache
*
* @return boolean
* @since __DEPLOY_VERSION__
*/
function clearFileInOPCache(string $file): bool
{
static $hasOpCache = null;

if (is_null($hasOpCache)) {
$hasOpCache = ini_get('opcache.enable')
&& function_exists('opcache_invalidate')
&& (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0);
}

if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) {
return opcache_invalidate($file, true);
}

return false;
}
}

if (!function_exists('finalizeRestore')) {
/**
* Run part of the Joomla! finalisation script, namely the part that cleans up unused files/folders.
* We need to define this function here because it is called by restore.php when users update from Joomla older
* than 4.0.4 to latest version
*
*
* @param string $siteRoot The root to the Joomla! site
* @param string $restorePath The base path to extract.php
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
function finalizeRestore(string $siteRoot, string $restorePath): void
{
if (function_exists('finalizeUpdate')) {
finalizeUpdate($siteRoot, $restorePath);
}
}
}

0 comments on commit 66993a2

Please sign in to comment.