diff --git a/administrator/com_joomgallery/forms/config.xml b/administrator/com_joomgallery/forms/config.xml index 31c8c250..bd2de4bc 100644 --- a/administrator/com_joomgallery/forms/config.xml +++ b/administrator/com_joomgallery/forms/config.xml @@ -95,19 +95,20 @@ - + description="COM_JOOMGALLERY_CONFIG_FTPUPLOAD_PATH_LONG" /> --> @@ -115,6 +116,17 @@ + + + + + +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/administrator/com_joomgallery/forms/subform_catparams.xml b/administrator/com_joomgallery/forms/subform_catparams.xml index bd95d95c..9b6621a4 100644 --- a/administrator/com_joomgallery/forms/subform_catparams.xml +++ b/administrator/com_joomgallery/forms/subform_catparams.xml @@ -13,8 +13,21 @@ type="jgradio" class="btn-group btn-group-yesno" useglobal="true" - label="COM_JOOMGALLERY_CONFIG_ALLOW_RATING" - description="COM_JOOMGALLERY_CONFIG_ALLOW_RATING_CAT_LONG"> + label="COM_JOOMGALLERY_CONFIG_RATING" + description="COM_JOOMGALLERY_CONFIG_RATING_CAT_LONG"> + + + + + + diff --git a/administrator/com_joomgallery/includes/defines.php b/administrator/com_joomgallery/includes/defines.php index d06eb992..f6445e6a 100644 --- a/administrator/com_joomgallery/includes/defines.php +++ b/administrator/com_joomgallery/includes/defines.php @@ -27,6 +27,7 @@ define('_JOOM_TABLE_GALLERIES_REF', '#__joomgallery_galleries_ref'); define('_JOOM_TABLE_IMG_TYPES', '#__joomgallery_img_types'); define('_JOOM_TABLE_TYPES', '#__joomgallery_img_types'); +define('_JOOM_TABLE_MIGRATION', '#__joomgallery_migration'); define('_JOOM_TABLE_TAGS', '#__joomgallery_tags'); define('_JOOM_TABLE_TAGS_REF', '#__joomgallery_tags_ref'); define('_JOOM_TABLE_USERS', '#__joomgallery_users'); diff --git a/administrator/com_joomgallery/language/en-GB/com_joomgallery.ini b/administrator/com_joomgallery/language/en-GB/com_joomgallery.ini old mode 100755 new mode 100644 index a0679323..96e08670 --- a/administrator/com_joomgallery/language/en-GB/com_joomgallery.ini +++ b/administrator/com_joomgallery/language/en-GB/com_joomgallery.ini @@ -11,12 +11,14 @@ ; ;Common JOOMGALLERY="JoomGallery" +SUCCESS="Success" COM_JOOMGALLERY="JoomGallery" COM_JOOMGALLERY_CONFIGURATION="JoomGallery: Options" COM_JOOMGALLERY_LOGO="JoomGallery Logo" COM_JOOMGALLERY_HELP="Help and Information" COM_JOOMGALLERY_MAINTENANCE="Maintenance" -COM_JOOMGALLERY_MIGRATIONS="Migration" +COM_JOOMGALLERY_MIGRATION="Migration" +COM_JOOMGALLERY_MIGRATIONS="Migrations" COM_JOOMGALLERY_IMAGE="Image" COM_JOOMGALLERY_IMAGES="Images" COM_JOOMGALLERY_IMAGETYPE="Image-Type" @@ -76,8 +78,32 @@ COM_JOOMGALLERY_IMPORT="Import" COM_JOOMGALLERY_MULTIPLE_NEW="Multiple New" COM_JOOMGALLERY_DEBUG_MODE="Debug Mode" COM_JOOMGALLERY_IMAGE_SELECTION="Image selection" -COM_JOOMGALLERY_IMAGE_SOURCE="Source" +COM_JOOMGALLERY_SOURCE="Source" +COM_JOOMGALLERY_DESTINATION="Destination" COM_JOOMGALLERY_REPLACE="Replace" +COM_JOOMGALLERY_LOGFILE="Log file" +COM_JOOMGALLERY_LOGDIRECTORY="Log directory" +COM_JOOMGALLERY_CHECK="Check" +COM_JOOMGALLERY_DIRECTORY="Directory" +COM_JOOMGALLERY_DIRECTORIES="Directories" +COM_JOOMGALLERY_GENERAL="General" +COM_JOOMGALLERY_SITE_OFFLINE="Site offline" +COM_JOOMGALLERY_ROOT_CATEGORY="Root Category" +COM_JOOMGALLERY_ROOT_ASSET="Root Asset" +COM_JOOMGALLERY_ROOT_CAT_ASSET="Root Category Asset" +COM_JOOMGALLERY_PATH="Path" +COM_JOOMGALLERY_TABLE="Table" +COM_JOOMGALLERY_RESUME="Resume" +COM_JOOMGALLERY_SUCCESSFUL="Successful" +COM_JOOMGALLERY_SUCCESS="Success" +COM_JOOMGALLERY_FAILED="Failed" +COM_JOOMGALLERY_WARNING="Warning" +COM_JOOMGALLERY_PENDING="Pending" +COM_JOOMGALLERY_CONFIRM="Confirm" +COM_JOOMGALLERY_CONFIRM_ERROR_MESSAGE="Error message" +COM_JOOMGALLERY_DIRECT_USAGE="Direct usage" +COM_JOOMGALLERY_STEP_X="Step %s" +COM_JOOMGALLERY_WEBSITE_HELP_URL="https://www.joomgalleryfriends.net/help/%s" ;Control panel COM_JOOMGALLERY_CONTROL_PANEL="Control Panel" @@ -177,7 +203,37 @@ COM_JOOMGALLERY_GENERIC_UPLOAD_DATA="Data entered in this form is applied on all COM_JOOMGALLERY_FILE_TITLE_HINT="Title of this file" COM_JOOMGALLERY_FILE_DESCRIPTION_HINT="Description of this file" COM_JOOMGALLERY_FILE_AUTHOR_HINT="Author of this file" - +COM_JOOMGALLERY_MIGRATION_INERRUPT_MIGRATION="Interrupt migration" +COM_JOOMGALLERY_MIGRATION_START_SCRIPT="Start script" +COM_JOOMGALLERY_MIGRATION_AVAILABLE_SCRIPTS="Available migration scripts" +COM_JOOMGALLERY_MIGRATION_ABORT_MIGRATION="Abort migration" +COM_JOOMGALLERY_MIGRATION_SCRIPT_NOT_EXIST="The requested migration script does not exist. Please provide an available script name in the request." +COM_JOOMGALLERY_MIGRATION_STEP1_TITLE="Migration configuration" +COM_JOOMGALLERY_MIGRATION_STEP1_BTN_TXT="Check migration capability" +COM_JOOMGALLERY_MIGRATION_STEP2_TITLE="Migration pre-check" +COM_JOOMGALLERY_MIGRATION_STEP2_BTN_TXT="Start migration manager" +COM_JOOMGALLERY_MIGRATION_STEP3_TITLE="Perform migration" +COM_JOOMGALLERY_MIGRATION_STEP3_BTN_TXT="Check & finish migration" +COM_JOOMGALLERY_MIGRATION_STEP4_TITLE="Finish migration" +COM_JOOMGALLERY_MIGRATION_STEP4_BTN_TXT="Back to step 3" +COM_JOOMGALLERY_MIGRATION_BTN_REMOVE_SOURCE="Remove source data" +COM_JOOMGALLERY_MIGRATION_BTN_END_MIGRATION="End migration and delete progress" +COM_JOOMGALLERY_MIGRATION_BTN_REMOVE_SOURCE_CONFIRM="Are you sure, you want to remove all source data? This is irreversible and prevents you from performing further migrations with these sources in the future." +COM_JOOMGALLERY_MIGRATION_REMOVE_SOURCE_DATA_DESC="If you are really sure that you have finished and completed all migrations using this migration script, you can use this functionality to remove all source data (databse & filesystem). If your source is another extension (component, module, plugin), deinstall this instead.

Attention!!
This is irreversible and prevents you from performing further migrations with these sources in the future." +COM_JOOMGALLERY_SHOWLOG="Show log output" +COM_JOOMGALLERY_MIGRATION_START="Start migration" +COM_JOOMGALLERY_MIGRATION_STOP="Stop migration" +COM_JOOMGALLERY_MIGRATION_MANUAL="Manual repair" +COM_JOOMGALLERY_MIGRATION_MANUAL_BTN="Mark as repaired" +COM_JOOMGALLERY_MIGRATION_MANUAL_DESC="This form can be used to change the state of a migration record manually.

For example: Failed migrations can be manually patched or migrated and with this form, marked as successful. This can be used to mark blocking records as successful in order to continue with the automatic migration process." +COM_JOOMGALLERY_MIGRATION_REPAIR_SRCPK_LABEL="Source pk" +COM_JOOMGALLERY_MIGRATION_REPAIR_SRCPK_DESC="Primary key of the source record to be repaired" +COM_JOOMGALLERY_MIGRATION_REPAIR_DESTPK_LABEL="Destination pk" +COM_JOOMGALLERY_MIGRATION_REPAIR_DESTPK_DESC="Primary key of the destination record to be repaired" +COM_JOOMGALLERY_MIGRATION_REPAIR_CONFIRM="I confirm that I successfully migrated and/or fixed the specified record." +COM_JOOMGALLERY_MIGRATION_REPAIR_STATE_LABEL="Change state to" +COM_JOOMGALLERY_MIGRATION_REPAIR_STATE_DESC="Select a state to which you want to change the selected source record to." +COM_JOOMGALLERY_MIGRATION_REPAIR_ERROR_DESC="Error message describing why this record failed migration." ;Messages COM_JOOMGALLERY_NOTE_DEVELOPMENT_VERSION="Attention!
----------------
This version of JoomGallery is still under development. Do not use this version on a live website. It is intended for testing purposes only..." @@ -187,7 +243,7 @@ COM_JOOMGALLERY_ERROR_CONFIG_INVALID_CONTEXT="Error deriving settings from confi COM_JOOMGALLERY_RESET_CONFIRM="Are you sure you want to restore to factory default?" COM_JOOMGALLERY_CONFIRM_DELETE_CATEGORIES="Are you sure you want to delete the selected categories?" COM_JOOMGALLERY_SETTINGS_POPUP="Settings-Popup" -COM_JOOMGALLERY_ERROR_JOOMLA_COMPATIBILITY="JoomGallery %s is only compatible to Joomla! %s." +COM_JOOMGALLERY_ERROR_JOOMLA_COMPATIBILITY="JoomGallery %s is not compatible with the current Joomla! version %s." COM_JOOMGALLERY_ERROR_PHP_COMPATIBILITY="JoomGallery %s is only compatible to PHP versions greater than 7.3. Your PHP version is %s." COM_JOOMGALLERY_ERROR_READ_XML_FILE="JoomGallery manifest XML file could not be readed." COM_JOOMGALLERY_ERROR_CREATE_DEFAULT_CATEGORY="Default category could not be created." @@ -268,6 +324,12 @@ COM_JOOMGALLERY_ERROR_REPLACE_IMAGETYPE="Image-Type (%s) successfully replaced." COM_JOOMGALLERY_SUCCESS_IMAGETYPE="Image-Type (%s) could not be replaced. Error: %s." COM_JOOMGALLERY_SUCCESS_REPLACE_IMAGETYPE="Image-Type (%s) successfully replaced." COM_JOOMGALLERY_ERROR_REPLACE_IMAGETYPE="Image-Type (%s) could not be replaced. Error: %s." +COM_JOOMGALLERY_ERROR_IMGTYPE_TABLE_NOT_EXISTING="Table for content type '%s' does not exist." +COM_JOOMGALLERY_ERROR_TABLE_NOT_EXISTING="Table does not exist." +COM_JOOMGALLERY_ERROR_PATH_NOT_EXISTING="Path does not exist." +COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED="You are not permitted to perform this task. Requested task: %s" +COM_JOOMGALLERY_ERROR_CHECKED_OUT_BY_ANOTHER_USER="This is item is currently checked out by another user (name: %s). You are not allowed to view or modify this item as long as it is checked out." +COM_JOOMGALLERY_ERROR_NETWORK_PROBLEM="Network problem occured. No connection to the server endpoint established or the server responds with an error." ;Mail Templates COM_JOOMGALLERY_MAIL_NEWIMAGE_TITLE="JoomGallery: New Image" @@ -304,6 +366,32 @@ COM_JOOMGALLERY_FIELDS_NUMBERING_START="Numbering start" COM_JOOMGALLERY_FIELDS_NUMBERING_START_DESC="Start numbering images with the following value." COM_JOOMGALLERY_FIELDS_REPLACE_IMAGETYPE_DESC="Select the image type to replace." COM_JOOMGALLERY_FIELDS_REPLACE_IMAGETYPE_PROCESS_DESC="Yes to process the image before replacement (resize, watermarking, ...)." +COM_JOOMGALLERY_FIELDS_SOURCE_LBL="Configuration for source data fetching" +COM_JOOMGALLERY_FIELDS_DEST_LBL="Configuration for destination data writing" +COM_JOOMGALLERY_FIELDS_SAMEJOOMLA_LABEL="Same Joomla! installation?" +COM_JOOMGALLERY_FIELDS_SAMEJOOMLA_DESC="Is the source located inside the same Joomla! installation folder?" +COM_JOOMGALLERY_FIELDS_JOOMLAPATH_LABEL="Joomla! path" +COM_JOOMGALLERY_FIELDS_JOOMLAPATH_DESC="Complete system path to the Joomla! root directory of the source.{tip}Use forward slash (/) as directoy separator. No slash at the end of the path." +COM_JOOMGALLERY_FIELDS_SAMEDB_LABEL="Same database?" +COM_JOOMGALLERY_FIELDS_SAMEDB_DESC="Is the source located inside the same Joomla! database?" +COM_JOOMGALLERY_FIELDS_DATABASE_TYPE_LABEL="Database Type" +COM_JOOMGALLERY_FIELDS_DATABASE_HOST_LABEL="Host" +COM_JOOMGALLERY_FIELDS_DATABASE_USER_LABEL="Database Username" +COM_JOOMGALLERY_FIELDS_DATABASE_PASS_LABEL="Database Password" +COM_JOOMGALLERY_FIELDS_DATABASE_NAME_LABEL="Database Name" +COM_JOOMGALLERY_FIELDS_DATABASE_PREFIX_LABEL="Database Tables Prefix" +COM_JOOMGALLERY_FIELDS_CHECKOWNER_LABEL="Check owners" +COM_JOOMGALLERY_FIELDS_CHECKOWNER_DESC="If set to yes, the owners of the migrated content are attempted to be retained. If a given owner ID cannot be found in the current Joomla! installation the owner will be set to be the fallback user of the 'Check owner plugin'." +COM_JOOMGALLERY_FIELDS_IMAGEUSAGE_LABEL="Image usage" +COM_JOOMGALLERY_FIELDS_IMAGEUSAGE_DESC="How do you want to use the images provided by the source?{tip}Direct usage: No migration of image files. Directly use the images in the source folder as they are. Only the three standard imagetypes available after migration.
Recreate: Use the original image from source for recreating the imagetypes. (Use detail if original is not available)
Copy: Copy the images from source and use them as imagetypes based on mapping.
Move: Move the images from source and use them as imagetypes based on mapping." +COM_JOOMGALLERY_FIELDS_IMAGEMAPPING_LABEL="Image mapping" +COM_JOOMGALLERY_FIELDS_IMAGEMAPPING_DESC="Apply the image mapping. Which source image should be used to create the destination imagetype. You need to add one row for each available destination imagetype!" +COM_JOOMGALLERY_FIELDS_SOURCE_IMAGE_LABEL="Source image" +COM_JOOMGALLERY_FIELDS_DEST_IMAGE_LABEL="Destination imagetype" +COM_JOOMGALLERY_FIELDS_DEST_EXTENSION_LABEL="Destination extension compatibility" +COM_JOOMGALLERY_FIELDS_SRC_EXTENSION_LABEL="Source extension compatibility" +COM_JOOMGALLERY_FIELDS_SOURCE_IDS_LABEL="Use source ID's" +COM_JOOMGALLERY_FIELDS_SOURCE_IDS_DESC="If set to yes, the ID's of the source records are used to create the destination records.{tip}In order to use this option, the ID's from source must not exist in the destination datebase table. Tip: Make sure, the destination tables are empty." ;Configuration COM_JOOMGALLERY_CONFIG_INHERITANCE_METHOD_LABEL="Configuration inheritance" @@ -334,10 +422,6 @@ COM_JOOMGALLERY_CONFIG_SHOW_IPTCDATA="Show IPTC data?" COM_JOOMGALLERY_CONFIG_SHOW_IPTCDATA_IMG_LONG="Shall the IPTC data of the image be shown if available?{tip}If \"Yes\" by the following options you can decide which information will be published. Otherwise they are ineffective." COM_JOOMGALLERY_CONFIG_OFFER_DOWNLOAD="Offer Downloads" COM_JOOMGALLERY_CONFIG_OFFER_DOWNLOAD_CAT_LONG="Here you can set whether the download is allowed in this category.{tip}This can overwrite the setting in tab 'User Access Rights->Download->Offer Downloads' in JoomGallery Configuration" -COM_JOOMGALLERY_CONFIG_ALLOW_COMMENT="Allow Comments" -COM_JOOMGALLERY_CONFIG_ALLOW_COMMENT_CAT_LONG="Here you can set whether the commenting is allowed in this category.{tip}This can overwrite the setting in tab 'User Access Rights->Comments->Allow comments' in JoomGallery Configuration" -COM_JOOMGALLERY_CONFIG_ALLOW_RATING="Allow Rating" -COM_JOOMGALLERY_CONFIG_ALLOW_RATING_CAT_LONG="Here you can set whether the rating is allowed in this category.{tip}This can overwrite the setting in tab 'User Access Rights->Ratings->Allow Rating' in JoomGallery Configuration" COM_JOOMGALLERY_CONFIG_ALLOW_WATERMARK="Add Watermark" COM_JOOMGALLERY_CONFIG_ALLOW_WATERMARK_CAT_LONG="Here you can set whether the Watermark is shown in detail/original images in this category.{tip}This can overwrite the setting in tab 'Detail View->General Settings->Add Watermark?' in JoomGallery Configuration" COM_JOOMGALLERY_CONFIG_ALLOW_WATERMARK_DOWNLOAD="Download with Watermark" @@ -370,6 +454,8 @@ COM_JOOMGALLERY_CONFIG_FTPUPLOAD_PATH="FTP Upload path" COM_JOOMGALLERY_CONFIG_FTPUPLOAD_PATH_LONG="Path to the FTP upload folder. This folder is used for FTP uploads.{tip}Images that you choose to add to the gallery via FTP go into this folder." COM_JOOMGALLERY_CONFIG_USE_REAL_PATHS="Use real paths" COM_JOOMGALLERY_CONFIG_USE_REAL_PATHS_LONG="If this option is enabled the images won't be output through the PHP script anymore so that their real paths are send to the browsers.{tip}Please note that the hit counter won't work with this setting enabled. Additionally the image files won't be protected against unauthorised access (protection would be still possible e.g via htaccess).
Enabling watermark or 'dynamic resizing in General Settings » Image processing » Image processing (dynamic images) » Settings in JoomGallery Configuration will override this setting." +COM_JOOMGALLERY_CONFIG_COMPATIBILITY_MODE="JG3 compatibility mode" +COM_JOOMGALLERY_CONFIG_COMPATIBILITY_MODE_LONG="Switch on compatibility mode to intercept some quirks of the old JoomGallery versions and thus ensure the compatibility of old content.{tip}Turning on the compatibility mode will lower the performance of the component and is therefore not recommended. In some cases, however, it is necessary in order to continue using old content correctly. For example:
  • You deactivated 'Use new folder structure' during migration. You need this mode to read out the old static system paths and continue using them.
" COM_JOOMGALLERY_CONFIG_CHECKUPDATE="Check for updates?" COM_JOOMGALLERY_CONFIG_CHECKUPDATE_LONG="Do you want the gallery to check automatically for component and extension updates?{tip}Checking for updates gallery reads an RSS feed provided at joomgalleryfriends.net." COM_JOOMGALLERY_CONFIG_REPLACE_METADATA="Use image metadata" @@ -487,6 +573,7 @@ COM_JOOMGALLERY_CONFIG_DOWNLOADWITHWATERMARK="Download with Watermark?" COM_JOOMGALLERY_CONFIG_DOWNLOADWITHWATERMARK_LONG="Do you want to add a watermark to the downloaded image?{tip}Choosing \"Yes\" will add a watermark to the downloaded image without regard whether or not a watermark is displayed in detail view." COM_JOOMGALLERY_CONFIG_RATING="Allow Rating" COM_JOOMGALLERY_CONFIG_RATING_LONG="This option displays a rating field in the detail view of the gallery and allows users to rate images.{tip}Enabling this, will display additional rating options below." +COM_JOOMGALLERY_CONFIG_RATING_CAT_LONG="Here you can set whether the rating is allowed in this category.{tip}This can overwrite the setting in tab 'User Access Rights->Ratings->Allow Rating' in JoomGallery Configuration" COM_JOOMGALLERY_CONFIG_HIGHEST_RATING="Highest rating" COM_JOOMGALLERY_CONFIG_HIGHEST_RATING_LONG="If user ratings are activated (setting above) you can specify the range of numbers here.{tip}The value you enter is the highest possible value permitted." COM_JOOMGALLERY_CONFIG_CALC_TYPE="Rating calculation type" @@ -510,6 +597,9 @@ COM_JOOMGALLERY_CONFIG_IMAGETYPES="Supported image types" COM_JOOMGALLERY_CONFIG_IMAGETYPES_LONG="List here the supported image file types as a comma separated list of file extensions.{tip}Example: jpg,jpeg,png,gif,webp" COM_JOOMGALLERY_CONFIG_PARALLEL_PROCESS="Nmb parallel processes" COM_JOOMGALLERY_CONFIG_PARALLEL_PROCESS_LONG="Number of parallel image saving processes.{tip}High values speed up the upload process in the multiple upload method, as several images are saved at the same time. However, this requires high server performance. Only increase this value to more than one if you have a powerful server available." +COM_JOOMGALLERY_CONFIG_COMMENT="Allow Commenting" +COM_JOOMGALLERY_CONFIG_COMMENT_LONG="This option displays a comment field in the detail view of the gallery showing the given comments.{tip}The current JoomGallery version does not support commenting. But already given comments will be shown, if this option is enabled." +COM_JOOMGALLERY_CONFIG_COMMENT_CAT_LONG="Here you can set whether the commenting is allowed in this category.{tip}This can overwrite the setting in tab 'User Access Rights->Comments->Allow comments' in JoomGallery Configuration" ;Services COM_JOOMGALLERY_SERVICE_READING_ERROR="Read image failed. No further processing." @@ -604,11 +694,79 @@ COM_JOOMGALLERY_SERVICE_ERROR_LOAD_CONFIG="Error: Configuration Set not properly COM_JOOMGALLERY_SERVICE_ERROR_UPLOAD_ANIMATED_WEBP="Error: Uploading WEBP files with animation is not possible." COM_JOOMGALLERY_SERVICE_ERROR_FILENOTFOUND="File not found." COM_JOOMGALLERY_SERVICE_ERROR_CREATE_FILE="File could not be created." +COM_JOOMGALLERY_SERVICE_ERROR_FILESYSTEM_NOT_A_DIRECTORY="Requested path is not a directory." COM_JOOMGALLERY_SERVICE_ERROR_FILESYSTEM_NOT_FOUND="No filesystem plugin available for the specified adapter (%s)." COM_JOOMGALLERY_SERVICE_ERROR_FILESYSTEM_ERROR="Filesystem plugin is reporting the following error: %s" - -;Plugin events -COM_JOOMGALLERY_PLUGIN_ERROR_RETURN_VALUE="Return value of the plugin event '%s' must be of type %s and contain : %s" +COM_JOOMGALLERY_SERVICE_MIGRATION_CHECK_TITLE="Check results" +COM_JOOMGALLERY_SERVICE_MIGRATION_GENERAL_PRECHECK_DESC="General checks like log file, site state, ..." +COM_JOOMGALLERY_SERVICE_MIGRATION_SOURCE_PRECHECK_DESC="Check source extension, directories and tables if they are compatible and existent." +COM_JOOMGALLERY_SERVICE_MIGRATION_DESTINATION_PRECHECK_DESC="Check destination extension, directories and tables if they are compatible, existent and writeable." +COM_JOOMGALLERY_SERVICE_MIGRATION_GENERAL_POSTCHECK_DESC="General checks to verify that migrations are completed." +COM_JOOMGALLERY_SERVICE_MIGRATION_DIRS_POSTCHECK_DESC="Check destination directory for orphans." +COM_JOOMGALLERY_SERVICE_MIGRATION_DB_POSTCHECK_DESC="Check that all needed files and folders exists." +COM_JOOMGALLERY_SERVICE_MIGRATION_DIRECTORY_SUCCESS="Directory exists and is usable." +COM_JOOMGALLERY_SERVICE_MIGRATION_LOG_DIR_LABEL="Logging directory" +COM_JOOMGALLERY_SERVICE_MIGRATION_LOGFILE_ERROR="Log file '%s' is not writable. Please check permissions." +COM_JOOMGALLERY_SERVICE_MIGRATION_LOGFILE_SUCCESS="Log file '%s' is writable." +COM_JOOMGALLERY_SERVICE_MIGRATION_LOGDIR_SUCCESS="Log file will be created in folder '%s'." +COM_JOOMGALLERY_SERVICE_MIGRATION_LOGDIR_ERROR="Log directory '%s' is not writable. Please check permissions." +COM_JOOMGALLERY_SERVICE_MIGRATION_OFFLINE_SUCCESS="Website is currently offline." +COM_JOOMGALLERY_SERVICE_MIGRATION_OFFLINE_ERROR="Website is currently online." +COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_NOT_SUPPORTED="Extension not supported (Extension: %s)" +COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_WRONG_VERSION="The current version of the extension is not supported. Current version: %s. Supported version: %s" +COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_SUCCESS="Extension is compatible (Extension: %s, Version: %s)" +COM_JOOMGALLERY_SERVICE_MIGRATION_SOURCE_XML="The XML file of your source extension could not be found. Please make sure the XML is available and readable." +COM_JOOMGALLERY_SERVICE_MIGRATION_PHP_WRONG_VERSION="The current PHP version is not supported. Current version: %s. Minimum requirement: %s" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_STEP2="Step 2: Migration pre-check failed. Failed check: %s" +COM_JOOMGALLERY_SERVICE_MIGRATION_SUCCESS_MIGRATION_STEP2="Step 2: Migration pre-check successful." +COM_JOOMGALLERY_SERVICE_MIGRATION_WARNING_MIGRATION_STEP2="Step 2: Warning appeared during migration pre-check. Affected check: %s" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_CHECKS_FAILED="Some of the checks failed." +COM_JOOMGALLERY_SERVICE_MIGRATION_SUCCESS_MIGRATION_STEP4="Step 4: Migration post-check successful." +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_STEP4="Step 4: Migration post-check failed. Failed check: %s" +COM_JOOMGALLERY_SERVICE_MIGRATION_WARNING_MIGRATION_STEP4="Step 4: Warning appeared during migration post-check. Affected check: %s" +COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES="There are already %s records in this table." +COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES_EMPTY="This table is empty." +COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES_USE_IDS_HINT="You have set 'Use source IDs' to true in step 1. But the ID's from source are not free/available in the destination.
Please delete the records with the following ID's: %s." +COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_CAT_SUCCESS="Root category exists and is set up correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_CAT_ERROR="Root category not existent or not set up correctly. Please make sure Root category is set up correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_ASSET_SUCCESS="Root asset exists and is set up correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_ASSET_ERROR="Root asset not existent or not set up correctly. Please make sure root asset is set up correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_CAT_ASSET_SUCCESS="Root category asset exists and is set up correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_CAT_ASSET_ERROR="Root category asset not existent or not set up correctly. Please make sure root category asset is set up correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_TABLE_CONN_ERROR="Connection to database not possible." +COM_JOOMGALLERY_SERVICE_MIGRATION_STEP_NOT_AVAILABLE="This step is not available. Please fulfill the previous step first." +COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_MAPPING_ERROR="Image mapping was not correctly applied in step 1. There has to be exactly one row for each available destination imagetype. Please go back to step 1 and apply the mapping correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_MAPPING_DEST_IMAGETYPE_NOT_EXIST="The destination imagetype '%s' does not exist or is used twice in the mapping. Please go back to step 1 and apply the mapping correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_MAPPING_SRC_IMAGETYPE_NOT_EXIST="The source imagetype '%s' does not exist. Please go back to step 1 and apply the mapping correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_MAPPING_IMAGETYPE_NOT_USED="The following destination imagetypes are not used in the mapping: '%s'. Please go back to step 1 and apply the mapping correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_RECORD_ID_MISSING="Starting a migration without source data ID not possible. Contact the developer of the migration script to solve this." +COM_JOOMGALLERY_SERVICE_ERROR_MIGRATION_RESUME="Migration can not be resumed. There is a problem in fetching the required info." +COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_FETCH_DATA="Data from source could not be loaded. See log file for more details." +COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_CONVERT_DATA="Data from source could not be converted to new data structure. See log file for more details." +COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_INSERT_RECORD="Data could not be inserted into destination database. See log file for more details." +COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_CREATE_IMGTYPE="Error in creating imagetypes. See log file for more details." +COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_CREATE_FOLDER="Error in creating folders. See log file for more details." +COM_JOOMGALLERY_SERVICE_MIGRATION_FILENAME_DIFF="It was noticed that two different filenames were used for detail/original and thumbnail in the image with id: %s and alias: %s. The migration script will skip the migration of this image." +COM_JOOMGALLERY_SERVICE_MIGRATION_ACTIVE="Currently in migration" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_FORMCHECK="Record migration state couldn't be modified. Please fill out form correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_SUCCESSFUL_0="Record of type %s with id=%s successfully marked as failed" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_SUCCESSFUL_1="Record of type %s with id=%s successfully marked as successful" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_SUCCESSFUL_2="Record of type %s with id=%s successfully marked as pending" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_NOT_AVAILABLE="Record with id=%s not available. State can not be changed." +COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_MIGRATIONS_ERROR="It seems that not all queued migrations are processed. Please go back to step 3 and finish performing all migrations." +COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_MIGRATIONS_SUCCESS="All queued migrations are successfully processed." +COM_JOOMGALLERY_SERVICE_MIGRATION_QUEUE="Migration queue" +COM_JOOMGALLERY_SERVICE_MIGRATION_QUEUE_ERROR="The migration queue of %s have not yet been fully processed. Please go back to step 3 and finish performing all migrations." +COM_JOOMGALLERY_SERVICE_MIGRATION_ERRORS_ERROR="Error during migration of type '%s' and ID '%s'. Error message: %s." +COM_JOOMGALLERY_SERVICE_MIGRATION_ERRORS_SUCCESS="No errors detected from migration manager." +COM_JOOMGALLERY_SERVICE_MIGRATION_SOURCE_DATA_DELETE_SUCCESSFUL="Source data successfully removed." +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_DUMMY_RECORD="Dummy record of type %s with ID=%s could not be inserted into database. Does a record with this id already exist in the table?" +COM_JOOMGALLERY_SERVICE_MIGRATION_COMMON_CHECK_HELP="If you face any problems, call for help at the Joom::Gallery Forum" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_FORMCHECK="Record migration state couldn\'t be modified. Please fill out form correctly." +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_SUCCESSFUL_0="Record of type %s with id=%s successfully marked as failed" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_SUCCESSFUL_1="Record of type %s with id=%s successfully marked as successful" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_SUCCESSFUL_2="Record of type %s with id=%s successfully marked as pending" +COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_NOT_AVAILABLE="Record with id=%s not available. State can not be changed." ;Menu COM_JOOMGALLERY_MENU_CATEGORY_VIEW_OPTIONS="Category View" @@ -629,4 +787,7 @@ COM_JOOMGALLERY_MENU_JUSTIFIED_HEIGHT_NOTE_DESC="Images are scaled while maintai COM_JOOMGALLERY_MENU_JUSTIFIED_GAP="Gap" COM_JOOMGALLERY_MENU_JUSTIFIED_GAP_NOTE_DESC="Set row and column gap." COM_JOOMGALLERY_MENU_CATEGORY_VIEW_LIGHTBOX_DESC="Use Lightbox Gallery" -COM_JOOMGALLERY_MENU_CATEGORY_VIEW_LIGHTBOX_LABEL="Use Lightbox Gallery" \ No newline at end of file +COM_JOOMGALLERY_MENU_CATEGORY_VIEW_LIGHTBOX_LABEL="Use Lightbox Gallery" + +; Plugin events +COM_JOOMGALLERY_PLUGIN_ERROR_RETURN_VALUE="Return value of the plugin event '%s' must be of type %s and contain : %s" diff --git a/administrator/com_joomgallery/language/en-GB/com_joomgallery.migration.Jg3ToJg4.ini b/administrator/com_joomgallery/language/en-GB/com_joomgallery.migration.Jg3ToJg4.ini new file mode 100644 index 00000000..ccd70189 --- /dev/null +++ b/administrator/com_joomgallery/language/en-GB/com_joomgallery.migration.Jg3ToJg4.ini @@ -0,0 +1,35 @@ +;****************************************************************************************** +;** @version 4.0.0-dev ** +;** @package com_joomgallery ** +;** @author JoomGallery::ProjectTeam ** +;** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +;** @license GNU General Public License version 3 or later ** +;*****************************************************************************************/ +; +FILES_JOOMGALLERY_MIGRATION_JG3TOJG4="JoomGallery 3.x to JoomGallery 4.x migration script" +FILES_JOOMGALLERY_MIGRATION_JG3TOJG4_TITLE="JoomGallery 3.x to JoomGallery 4.x" +FILES_JOOMGALLERY_MIGRATION_JG3TOJG4_DESC="Migration of the main content types (images, categories) from JoomGallery version 3.x to JoomGallery version 4.x." +FILES_JOOMGALLERY_MIGRATION_JG3TOJG4_ORIGPATH_DESC="Path to original image" +FILES_JOOMGALLERY_MIGRATION_JG3TOJG4_DETAILPATH_DESC="Path to detail image" +FILES_JOOMGALLERY_MIGRATION_JG3TOJG4_THUMBPATH_DESC="Path to thumbnail image" +FILES_JOOMGALLERY_MIGRATION_IMAGE_TITLE="Migration: Images" +FILES_JOOMGALLERY_MIGRATION_CATEGORY_TITLE="Migration: Categories" +FILES_JOOMGALLERY_MIGRATION_CATIMAGE_TITLE="Adjustment: Category thumbnails" +FILES_JOOMGALLERY_MIGRATION_CHECK_IMAGE_FILENAMES_TITLE="Image filenames" +FILES_JOOMGALLERY_MIGRATION_CHECK_IMAGE_FILENAMES_DESC="There are different filenames for thumbnail and detail image defined in the database of your JG3 tables. Number of affected images: %s" +FILES_JOOMGALLERY_MIGRATION_CHECK_IMAGE_FILENAMES_HELP="It was detected that for some images in the database table of the JG3 (source, #__com_joomgallery) different filenames are stored in the rows 'imgfilename' and 'imgthumbname'. This script could result in errorous migration results. We strongly advise to clean up the database manually before starting the migration. For all the affected Images you have to do the following:
  • Rename the thumbnail image in the filesystem to be the one set in the old JoomGallery image table at row 'imgfilename'.
  • In the old JoomGallery image table, set the value of the row 'imgthumbname' to be the same as in row 'imgfilename'.

ID's of affected images: (%s)" +FILES_JOOMGALLERY_MIGRATION_ERROR_TYPE_PREREQUIREMENT="Prerequirement for this content type is not fulfilled. Please make shure the following content types are completely migrated and none of them failed." +FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_ERROR="Its not possible to directly use images, if source is outside this Joomla! installation. You can not set 'Same Joomla! installation' to yes and 'Image usage' to 'Direct usage' together in step 1." +FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_IMGTYPES_ERROR="Direct usage of images is only possible with the three default imagetypes (original, detail, thumbnail) activated. Please remove all other imagetypes from the JoomGallery configuration." +FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_LOCAL_ERROR="Direct usage of images is only possible when using the local filesystem. Please switch to the local filesystem in the JoomGallery configuration if you want to use 'Direct usage'." +FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_ORIGINAL_WARNING="You have chosen direct usage of images in step 1. Please make sure the 'original' imagetype is deactivated if it was deactivated in your source/previous Joomla version." +FILES_JOOMGALLERY_MIGRATION_CHECK_CATEGORY_CATPATH="Category paths" +FILES_JOOMGALLERY_MIGRATION_CHECK_CATEGORY_CATPATH_DESC="There are inconsistent category folder paths defined in the database of your JG3 tables. Please adjust affected category paths manually, or switch off 'Use new folder structure' in step 1 to use the old static catpath instead" +FILES_JOOMGALLERY_MIGRATION_CHECK_CATEGORY_CATPATH_HELP="Some categories in the database table of the JG3 (source, #__com_joomgallery_catg) do not have consistent data entries for cid, name, alias, and catpath. This happened because of a bug in previous JoomGallery versions which was fixed in JoomGallery v3.7.0. Thus an inconsistent data basis in the database was created when renaming or moving categories without clearing the alias field. This inconsistency prevents a migration into the new folder structure and without compatibility mode activated.

There are two options to solve that:
  • Clean up the source database manually or by using the maintenance manager of JoomGallery v3.7.0. (recommended)
  • Disable 'Use new folder structure' in the migration options and activate 'JG3 compatibility mode' in the component configuration. (not recommended)
We strongly advise to clean up the database before starting the migration. This way JoomGallery can be migrated into the proper, new structure which corresponds to the Joomla way of doing it. This will prevent your system for errors and illogical behaviour patterns.

A consistent JG3 category table (#__com_joomgallery_catg) fulfils the following conditions:
  • alias = parentalias/title
    where title is converted into an URL safe string (lowercase, no special characters and umlauts, no whitespaces, ...)
  • path = parentpath/title_cid
    where title is converted just like the alias and cid is the category id.

ID's of affected categories: (%s)" +FILES_JOOMGALLERY_FIELDS_NEW_DIRS_LABEL="Use new folder structure" +FILES_JOOMGALLERY_FIELDS_NEW_DIRS_DESC="Do you want to convert your migrated categories to the new folder structure style (filesystem)?{tip}New path style (JG4+): root-of-filesystem/joomgallery/imagetype/parent-category-path/alias/
Old path style (JG3-): joomla-root/images/joomgallery/imagetype/parent-category-path/name_cid/
In JG3-, in most cases the alias is derivable from the category name. This might be wrong if you renamed category titles without regenrating the alias. In this case a convertion of the old JG3- folder structure to the new one is not possible.
If you choose to keep the old folder structure, you have to switch on the 'JG3 compatibility mode' in the components global configuration in order for the component to find images stored in the old folder structure style." +FILES_JOOMGALLERY_MIGRATION_CHECK_COMPATIBILITY_MODE="Compatibility mode" +FILES_JOOMGALLERY_MIGRATION_CHECK_COMPATIBILITY_MODE_ON_DESC="You have chosen to use the old folder structure in step 1. In order for this to work the compatibility mode has to be activated in the component configuration. Please activate 'JG3 compatibility mode' in the JoomGallery configuration." +FILES_JOOMGALLERY_MIGRATION_CHECK_COMPATIBILITY_MODE_OFF_DESC="You have chosen to use the new folder structure in step 1. In order for this to work the compatibility mode has to be deactivated in the component configuration. Please deactivate 'JG3 compatibility mode' in the JoomGallery configuration." +FILES_JOOMGALLERY_SERVICE_MIGRATION_MCR="Move/Copy/Recreate" +FILES_JOOMGALLERY_SERVICE_MIGRATION_MCR_ERROR="Move, Copy or Recreate within the same filesystem (same joomla, same filesystem) and by keeping the old folder structure is impossible. You have chosen an impossible combination of migration options. Please adjust your migration options. Impossible combination: [Image usage: Move|Copy|Recreate, Same Joomla! installation: Yes, Use new folder structure: No, (JG4 config)Filesystem: Local]" \ No newline at end of file diff --git a/administrator/com_joomgallery/language/en-GB/com_joomgallery.sys.ini b/administrator/com_joomgallery/language/en-GB/com_joomgallery.sys.ini index adba7309..65fea61f 100644 --- a/administrator/com_joomgallery/language/en-GB/com_joomgallery.sys.ini +++ b/administrator/com_joomgallery/language/en-GB/com_joomgallery.sys.ini @@ -13,14 +13,18 @@ COM_JOOMGALLERY_CONFIG_SETS="Configuration Sets" COM_JOOMGALLERY_IMAGES="Images" COM_JOOMGALLERY_MAINTENANCE="Maintenance" COM_JOOMGALLERY_UPLOAD="Upload" +COM_JOOMGALLERY_TAGS="Tags" +COM_JOOMGALLERY_VOTES="Votes" +COM_JOOMGALLERY_DOWNLOADS="Downloads" +COM_JOOMGALLERY_COMMENTS="Comments" +COM_JOOMGALLERY_MIGRATIONS="Migration" +COM_JOOMGALLERY_CHANGELOG="Changelog" COM_JOOMGALLERY_ACTION_UPLOAD_INOWN="Upload in Own" COM_JOOMGALLERY_ACTION_CREATE_INOWN="Create in Own" COM_JOOMGALLERY_ACTION_COMPONENT_UPLOAD_DESC="Allow users in the group to upload images into any categories." COM_JOOMGALLERY_ACTION_COMPONENT_UPLOAD_INOWN_DESC="Allow users in the group to upload images into their own categories." COM_JOOMGALLERY_ACTION_COMPONENT_CREATE_DESC="Allow users in the group to create categories in this extension." COM_JOOMGALLERY_ACTION_COMPONENT_CREATE_INOWN_DESC="Allow users in the group to create categories in their own categories." -COM_JOOMGALLERY_TAGS="Tags" -COM_JOOMGALLERY_CHANGELOG="Changelog" COM_JOOMGALLERY_SUCCESS_UPDATE="JoomGallery was updated to version %s successfully." COM_JOOMGALLERY_SUCCESS_INSTALL="JoomGallery has been installed successfully" COM_JOOMGALLERY_SUCCESS_INSTALL_EXT="%s with name %s was installed successfully." diff --git a/administrator/com_joomgallery/layouts/joomgallery/migrepair.php b/administrator/com_joomgallery/layouts/joomgallery/migrepair.php new file mode 100644 index 00000000..56929d15 --- /dev/null +++ b/administrator/com_joomgallery/layouts/joomgallery/migrepair.php @@ -0,0 +1,37 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +// No direct access +defined('_JEXEC') or die; + +use \Joomla\CMS\Router\Route; +use \Joomla\CMS\HTML\HTMLHelper; +?> + +
+
+
+
+ renderField('note'); ?> +
+ renderField('src_pk'); ?> + renderField('state'); ?> + renderField('dest_pk'); ?> + renderField('error'); ?> + renderField('confirmation'); ?> +
+
+ + + + + +
+
diff --git a/administrator/com_joomgallery/sql/install.mysql.utf8.sql b/administrator/com_joomgallery/sql/install.mysql.utf8.sql index 0a2f7b0c..d80a47a9 100644 --- a/administrator/com_joomgallery/sql/install.mysql.utf8.sql +++ b/administrator/com_joomgallery/sql/install.mysql.utf8.sql @@ -117,6 +117,7 @@ CREATE TABLE IF NOT EXISTS `#__joomgallery_configs` ( `jg_pathftpupload` VARCHAR(100) NOT NULL DEFAULT "administrator/components/com_joomgallery/temp/ftp_upload/", `jg_wmfile` VARCHAR(50) NOT NULL DEFAULT "media/joomgallery/images/watermark.png", `jg_use_real_paths` TINYINT(1) NOT NULL DEFAULT 0, +`jg_compatibility_mode` TINYINT(1) NOT NULL DEFAULT 0, `jg_checkupdate` TINYINT(1) NOT NULL DEFAULT 1, `jg_replaceinfo` TEXT NOT NULL, `jg_replaceshowwarning` TINYINT(1) NOT NULL DEFAULT 0, @@ -167,6 +168,7 @@ CREATE TABLE IF NOT EXISTS `#__joomgallery_configs` ( `jg_votingonlyonce` TINYINT(1) NOT NULL DEFAULT 1, `jg_report_images` TINYINT(1) NOT NULL DEFAULT 1, `jg_report_hint` TINYINT(1) NOT NULL DEFAULT 1, +`jg_showcomments` TINYINT(1) NOT NULL DEFAULT 1, PRIMARY KEY (`id`), KEY `idx_checkout` (`checked_out`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; @@ -291,6 +293,9 @@ CREATE TABLE IF NOT EXISTS `#__joomgallery_galleries` ( `modified_by` INT(11) UNSIGNED NOT NULL DEFAULT 0, `checked_out` INT(11) UNSIGNED NOT NULL DEFAULT 0, `checked_out_time` DATETIME DEFAULT NULL, +`metadesc` TEXT NOT NULL, +`metakey` TEXT NOT NULL, +`robots` VARCHAR(255) NOT NULL DEFAULT "0", PRIMARY KEY (`id`), KEY `galery_idx` (`published`,`access`), KEY `idx_access` (`access`), @@ -372,6 +377,30 @@ KEY `idx_createdby` (`created_by`) -- -------------------------------------------------------- +-- +-- Table structure for table `#__joomgallery_migration` +-- + +CREATE TABLE IF NOT EXISTS `#__joomgallery_migration` ( +`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, +`script` VARCHAR(50) NOT NULL DEFAULT "", +`type` VARCHAR(50) NOT NULL DEFAULT "", +`src_table` VARCHAR(255) NOT NULL DEFAULT "", +`src_pk` VARCHAR(25) NOT NULL DEFAULT "id", +`dst_table` VARCHAR(255) NOT NULL DEFAULT "", +`dst_pk` VARCHAR(25) NOT NULL DEFAULT "id", +`queue` TEXT NOT NULL, +`successful` TEXT NOT NULL, +`failed` TEXT NOT NULL, +`params` TEXT NOT NULL, +`created_time` DATETIME NOT NULL, +`checked_out` INT(11) UNSIGNED NOT NULL DEFAULT 0, +`checked_out_time` DATETIME DEFAULT NULL, +PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + -- -- Dumping data for table `#__content_types` -- diff --git a/administrator/com_joomgallery/sql/uninstall.mysql.utf8.sql b/administrator/com_joomgallery/sql/uninstall.mysql.utf8.sql index 37d5cd8f..f9ff3ff6 100644 --- a/administrator/com_joomgallery/sql/uninstall.mysql.utf8.sql +++ b/administrator/com_joomgallery/sql/uninstall.mysql.utf8.sql @@ -7,6 +7,7 @@ DROP TABLE IF EXISTS `#__joomgallery_fields`; DROP TABLE IF EXISTS `#__joomgallery_galleries`; DROP TABLE IF EXISTS `#__joomgallery_galleries_ref`; DROP TABLE IF EXISTS `#__joomgallery_img_types`; +DROP TABLE IF EXISTS `#__joomgallery_migration`; DROP TABLE IF EXISTS `#__joomgallery_tags`; DROP TABLE IF EXISTS `#__joomgallery_tags_ref`; DROP TABLE IF EXISTS `#__joomgallery_users`; diff --git a/administrator/com_joomgallery/sql/updates/mysql/4.0.0.sql b/administrator/com_joomgallery/sql/updates/mysql/4.0.0.sql index 0a2f7b0c..d80a47a9 100644 --- a/administrator/com_joomgallery/sql/updates/mysql/4.0.0.sql +++ b/administrator/com_joomgallery/sql/updates/mysql/4.0.0.sql @@ -117,6 +117,7 @@ CREATE TABLE IF NOT EXISTS `#__joomgallery_configs` ( `jg_pathftpupload` VARCHAR(100) NOT NULL DEFAULT "administrator/components/com_joomgallery/temp/ftp_upload/", `jg_wmfile` VARCHAR(50) NOT NULL DEFAULT "media/joomgallery/images/watermark.png", `jg_use_real_paths` TINYINT(1) NOT NULL DEFAULT 0, +`jg_compatibility_mode` TINYINT(1) NOT NULL DEFAULT 0, `jg_checkupdate` TINYINT(1) NOT NULL DEFAULT 1, `jg_replaceinfo` TEXT NOT NULL, `jg_replaceshowwarning` TINYINT(1) NOT NULL DEFAULT 0, @@ -167,6 +168,7 @@ CREATE TABLE IF NOT EXISTS `#__joomgallery_configs` ( `jg_votingonlyonce` TINYINT(1) NOT NULL DEFAULT 1, `jg_report_images` TINYINT(1) NOT NULL DEFAULT 1, `jg_report_hint` TINYINT(1) NOT NULL DEFAULT 1, +`jg_showcomments` TINYINT(1) NOT NULL DEFAULT 1, PRIMARY KEY (`id`), KEY `idx_checkout` (`checked_out`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; @@ -291,6 +293,9 @@ CREATE TABLE IF NOT EXISTS `#__joomgallery_galleries` ( `modified_by` INT(11) UNSIGNED NOT NULL DEFAULT 0, `checked_out` INT(11) UNSIGNED NOT NULL DEFAULT 0, `checked_out_time` DATETIME DEFAULT NULL, +`metadesc` TEXT NOT NULL, +`metakey` TEXT NOT NULL, +`robots` VARCHAR(255) NOT NULL DEFAULT "0", PRIMARY KEY (`id`), KEY `galery_idx` (`published`,`access`), KEY `idx_access` (`access`), @@ -372,6 +377,30 @@ KEY `idx_createdby` (`created_by`) -- -------------------------------------------------------- +-- +-- Table structure for table `#__joomgallery_migration` +-- + +CREATE TABLE IF NOT EXISTS `#__joomgallery_migration` ( +`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, +`script` VARCHAR(50) NOT NULL DEFAULT "", +`type` VARCHAR(50) NOT NULL DEFAULT "", +`src_table` VARCHAR(255) NOT NULL DEFAULT "", +`src_pk` VARCHAR(25) NOT NULL DEFAULT "id", +`dst_table` VARCHAR(255) NOT NULL DEFAULT "", +`dst_pk` VARCHAR(25) NOT NULL DEFAULT "id", +`queue` TEXT NOT NULL, +`successful` TEXT NOT NULL, +`failed` TEXT NOT NULL, +`params` TEXT NOT NULL, +`created_time` DATETIME NOT NULL, +`checked_out` INT(11) UNSIGNED NOT NULL DEFAULT 0, +`checked_out_time` DATETIME DEFAULT NULL, +PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + -- -- Dumping data for table `#__content_types` -- diff --git a/administrator/com_joomgallery/src/Controller/MigrationController.php b/administrator/com_joomgallery/src/Controller/MigrationController.php new file mode 100644 index 00000000..18595c45 --- /dev/null +++ b/administrator/com_joomgallery/src/Controller/MigrationController.php @@ -0,0 +1,1011 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Controller; + +\defined('_JEXEC') or die; + +use \Joomla\CMS\Factory; +use \Joomla\CMS\Uri\Uri; +use \Joomla\Input\Input; +use \Joomla\CMS\Log\Log; +use \Joomla\CMS\Language\Text; +use \Joomla\CMS\Router\Route; +use \Joomla\Registry\Registry; +use Joomla\CMS\Session\Session; +use \Joomla\CMS\Response\JsonResponse; +use \Joomla\CMS\Application\CMSApplication; +use \Joomla\CMS\MVC\Controller\BaseController; +use \Joomla\CMS\Form\FormFactoryAwareTrait; +use \Joomla\CMS\Form\FormFactoryInterface; +use \Joomla\CMS\Form\FormFactoryAwareInterface; +use \Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use \Joomgallery\Component\Joomgallery\Administrator\Extension\JoomgalleryComponent; + +/** + * Migration controller class. + * + * @package JoomGallery + * @since 4.0.0 + */ +class MigrationController extends BaseController implements FormFactoryAwareInterface +{ + use FormFactoryAwareTrait; + + /** + * Joomgallery\Component\Joomgallery\Administrator\Extension\JoomgalleryComponent + * + * @var JoomgalleryComponent + * @since 4.0.0 + */ + protected $component; + + /** + * The context for storing internal data, e.g. record. + * + * @var string + * @since 1.6 + */ + protected $context = _JOOM_OPTION.'.migration'; + + /** + * The URL option for the component. + * + * @var string + * @since 1.6 + */ + protected $option = _JOOM_OPTION; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = _JOOM_OPTION_UC; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * @param FormFactoryInterface $formFactory The form factory. + * + * @since 3.0 + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null, FormFactoryInterface $formFactory = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->setFormFactory($formFactory); + $this->component = $this->app->bootComponent(_JOOM_OPTION); + $this->component->createAccess(); + + // As copy should be standard on forms. + $this->registerTask('check', 'precheck'); + } + + /** + * Proxy for getModel. + * + * @param string $name Optional. Model name + * @param string $prefix Optional. Class prefix + * @param array $config Optional. Configuration array for model + * + * @return object The Model + * + * @since 4.0.0 + */ + public function getModel($name = 'Migration', $prefix = 'Administrator', $config = array()) + { + return parent::getModel($name, $prefix, array('ignore_request' => true)); + } + + /** + * Method to cancel a migration. + * + * @return boolean True on success, false otherwise + * + * @since 4.0.0 + */ + public function cancel() + { + $this->checkToken(); + + $model = $this->getModel(); + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new Exception('Requested migration script does not exist.', 1); + } + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.start'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Get migrateables if available + try + { + $migrateables = $model->getMigrateables(); + } + catch(\Exception $e) + { + $migrateables = false; + } + + // Checkin migration records of this script + if($migrateables) + { + foreach($migrateables as $mig) + { + if($mig->checked_out || \intval($mig->checked_out) > 0) + { + // Check in record + $model->checkin($mig->id); + } + } + } + + // Clean the session data and redirect. + $this->app->setUserState(_JOOM_OPTION.'.migration.script', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.params', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.noToken', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.step2.data', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.step2.results', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.step2.success', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.step3.results', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.step3.success', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.step4.results', null); + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.step4.success', null); + + // Redirect to the list screen. + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return true; + } + + /** + * Method to resume a previously paused or canceled migration. + * + * @return boolean True on success, false otherwise + * + * @since 4.0.0 + */ + public function resume() + { + $this->checkToken(); + + $model = $this->getModel(); + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new Exception('Requested migration script does not exist.', 1); + } + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.start', 'error')); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Load params + $model->setParams(); + + // Get item to resume from the request. + $cid = (array) $this->input->get('cid', [], 'int'); + $cid = \array_filter($cid); + $id = $cid[0]; + + if($id < 1) + { + $this->setMessage(Text::_('COM_JOOMGALLERY_SERVICE_ERROR_MIGRATION_RESUME', 'error')); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Attempt to load the migration item + $item = $model->getItem($id); + if(!$item || $item->script != $script) + { + $this->setMessage(Text::_('COM_JOOMGALLERY_SERVICE_ERROR_MIGRATION_RESUME', 'error')); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Check if migration item is checked out + $user = Factory::getUser(); + if(isset($item->checked_out) && !($item->checked_out == 0 || $item->checked_out == $user->get('id'))) + { + // You are not allowed to resume the migration, since it is checked out by another user + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_CHECKED_OUT_BY_ANOTHER_USER', $user->get('name')), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Set params data to user state + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.params', $item->params); + + // Set no token check to user state + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.noToken', true); + + // Redirect to the from screen (step 2). + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&task=migration.precheck&isNew=0', false)); + + return true; + } + + /** + * Method to remove one or more item from database. + * + * @return boolean True on success, false otherwise + * + * @since 4.0.0 + */ + public function delete() + { + $this->checkToken(); + + $model = $this->getModel(); + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new Exception('Requested migration script does not exist.', 1); + } + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.start'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Get items to remove from the request. + $cid = (array) $this->input->get('cid', [], 'int'); + + // Remove zero values resulting from input filter + $cid = \array_filter($cid); + + if(!empty($cid)) + { + // Get the model. + $model = $this->getModel(); + + // Load params + $model->setParams(); + + // Attempt to load the migration item + $item = $model->getItem($cid[0]); + if(!$item || $item->script != $script) + { + $this->setMessage(Text::_('COM_JOOMGALLERY_SERVICE_ERROR_MIGRATION_RESUME', 'error')); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Check if migration item is checked out + $user = Factory::getUser(); + if(isset($item->checked_out) && !($item->checked_out == 0 || $item->checked_out == $user->get('id'))) + { + // You are not allowed to resume the migration, since it is checked out by another user + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_CHECKED_OUT_BY_ANOTHER_USER', $user->get('name')), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Remove the items. + if($model->delete($cid)) + { + $this->app->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_DELETED', \count($cid))); + } + else + { + $this->app->enqueueMessage($model->getError(), 'error'); + } + } + + // Redirect to the list screen. + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return true; + } + + /** + * Method to remove migration source data (filesystem & database). + * + * @return boolean True on success, false otherwise + * + * @since 4.0.0 + */ + public function removesource() + { + $this->checkToken(); + + $model = $this->getModel(); + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new Exception('Requested migration script does not exist.', 1); + } + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.start'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + // Check if script allows source data removal + if(!$model->getSourceDeletion()) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.removesource'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step4', false)); + + return false; + } + + $postcheck = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$script.'.step4.success', false); + + // Check if no errors detected in postcheck (step 4) + if(!$postcheck) + { + // Post-checks not successful. Show error message. + $msg = Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_CHECKS_FAILED'); + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_STEP4', $msg), 'error'); + // Redirect to the step 4 screen + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step4', false)); + } + + // Get items to remove from the request. + $cid = (array) $this->input->get('cid', [], 'int'); + + // Remove zero values resulting from input filter + $cid = \array_filter($cid); + + if(!empty($cid)) + { + // Remove the source data. + if($model->deleteSource($cid)) + { + $this->app->enqueueMessage(Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_SOURCE_DATA_DELETE_SUCCESSFUL'), 'message'); + } + else + { + $this->app->enqueueMessage($model->getError(), 'error'); + } + } + + // Redirect to the step 4 screen. + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step4', false)); + + return true; + } + + /** + * Step 2 + * Validate the form input data and perform the pre migration checks. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function precheck() + { + // Get script + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + + // No token (When precheck is called on reume, no token check is needed) + $noToken = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$script.'.noToken', false); + + // Check for request forgeries + if(\is_null($noToken) && !$noToken) + { + $this->checkToken(); + } + + $model = $this->getModel(); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new \Exception('Requested migration script does not exist.', 1); + } + + $data = $this->input->post->get('jform_'.$script, [], 'array'); + $context = _JOOM_OPTION.'.migration.'.$script.'.step2'; + $task = $this->getTask(); + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.start'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + if($isNew = $this->input->get('isNew', true,'bool')) + { + // Validate the posted data. + $form = $model->getForm($data, false); + + // Send an object which can be modified through the plugin event + $objData = (object) $data; + $this->app->triggerEvent('onContentNormaliseRequestData', [$context, $objData, $form]); + $data = (array) $objData; + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if($validData === false) + { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) + { + if($errors[$i] instanceof \Exception) + { + $this->component->setWarning($errors[$i]->getMessage()); + } + else + { + $this->component->setWarning($errors[$i]); + } + } + + // Save the form data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + } + else + { + $validData = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$script.'.params', array()); + } + + // Save the script name in the session. + $this->app->setUserState(_JOOM_OPTION.'.migration.script', $script); + + // Save the migration parameters in the session. + $this->app->setUserState(_JOOM_OPTION.'.migration.'.$script.'.params', $validData); + + // Perform the pre migration checks + list($success, $res, $msg) = $model->precheck($validData); + if(!$success) + { + // Pre-checks not successful. Show error message. + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_STEP2', $msg), 'error'); + } + else + { + // Pre-checks successful. Show success message. + $this->setMessage(Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_SUCCESS_MIGRATION_STEP2')); + + if(!empty($msg)) + { + // Warnings appeared. Show warning message. + $this->app->enqueueMessage(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_WARNING_MIGRATION_STEP2', $msg), 'warning'); + } + } + + // Save the results of the pre migration checks in the session. + $this->app->setUserState($context . '.results', $res); + $this->app->setUserState($context . '.success', $success); + + // Redirect to the screen to show the results (View of Step 2) + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step2', false)); + + return; + } + + /** + * Step 3 + * Enter the migration view. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function migrate() + { + // Check for request forgeries + $this->checkToken(); + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.start'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + $model = $this->getModel(); + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new \Exception('Requested migration script does not exist.', 1); + } + + $precheck = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$script.'.step2.success', false); + + // Check if no errors detected in precheck (step 2) + if(!$precheck) + { + // Pre-checks not successful. Show error message. + $msg = Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_CHECKS_FAILED'); + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_STEP2', $msg), 'error'); + // Redirect to the step 2 screen + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step2', false)); + } + + // Redirect to the step 3 screen + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step3', false)); + } + + /** + * Step 4 + * Perform the post migration checks. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function postcheck() + { + // Check for request forgeries + $this->checkToken(); + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.start'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + $model = $this->getModel(); + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new \Exception('Requested migration script does not exist.', 1); + } + + $context = _JOOM_OPTION.'.migration.'.$script.'.step4'; + $task = $this->getTask(); + + // Perform the post migration checks + list($success, $res, $msg) = $model->postcheck(); + if(!$success) + { + // Post-checks not successful. Show error message. + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_STEP4', $msg), 'error'); + } + else + { + // Pre-checks successful. Show success message. + $this->setMessage(Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_SUCCESS_MIGRATION_STEP4')); + + if(!empty($msg)) + { + // Warnings appeared. Show warning message. + $this->app->enqueueMessage(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_WARNING_MIGRATION_STEP4', $msg), 'warning'); + } + } + + // Save the results of the post migration checks in the session. + $this->app->setUserState($context . '.results', $res); + $this->app->setUserState($context . '.success', $success); + + // Redirect to the screen to show the results (View of Step 4) + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step4', false)); + + return; + } + + /** + * Perform a migration + * Called by Ajax requests + * + * @return void + * + * @since 4.0.0 + */ + public function start() + { + // Check for request forgeries + $this->checkToken(); + + // Get request format + $format = strtolower($this->app->getInput()->getWord('format', 'json')); + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $response = $this->createRespond(null, false, Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.start')); + $this->ajaxRespond($response, $format); + + return false; + } + + $model = $this->getModel(); + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + $response = $this->createRespond(null, false, 'Requested migration script does not exist.'); + $this->ajaxRespond($response, $format); + + return false; + } + + // Check if no errors detected in precheck (step 2) + $precheck = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$script.'.step2.success', false); + if(!$precheck) + { + // Pre-checks not successful. Show error message. + $response = $this->createRespond(null, false, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_CHECKS_FAILED')); + $this->ajaxRespond($response, $format); + + return false; + } + + // Get input params for migration + $type = $this->app->getInput()->get('type', '', 'string'); + $id = $this->app->getInput()->get('id', '', 'int'); + $json = \json_decode(\base64_decode($this->app->getInput()->get('migrateable', '', 'string')), true); + + // Check if a record id to be migrated is given + if(empty($id) || $id == 0) + { + // No record id given. Show error message. + $response = $this->createRespond(null, false, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_RECORD_ID_MISSING')); + $this->ajaxRespond($response, $format); + + return false; + } + + // Start migration + //------------------ + + // Attempt to load migration record from database + $item = $model->getItem($json['id']); + + if(\is_null($item->id)) + { + // It seems that the migration record does not yet exists in the database + // Save migration record to database + if(!$model->save($json)) + { + $this->component->setError($model->getError()); + + return false; + } + + // Attempt to load migration record from database + $item = $model->getItem($model->getState('migration.id')); + } + + // Check out migration record if not already checked out + if(\is_null($item->checked_out) || \intval($item->checked_out) < 1) + { + // Check out record + $model->checkout($item->id); + } + + // Perform the migration + $table = $model->migrate($type, $id); + + // Stop automatic execution if migrateable is complete + if($table->completed) + { + $this->component->getMigration()->set('continue', false); + + // Check in record + $model->checkin($table->id); + } + + // Check for errors + if(!empty($this->component->getError())) + { + // Error during migration + $response = $this->createRespond($table, false, $this->component->getError()); + } + else + { + // Migration successful + $response = $this->createRespond($table, true); + } + + // Send migration results + $this->ajaxRespond($response, $format); + } + + /** + * Apply a migration state for a specific record manually + * + * @return void + * + * @since 4.0.0 + */ + public function applyState() + { + // Check for request forgeries + $this->checkToken(); + + // Access check. + $acl = $this->component->getAccess(); + if(!$acl->checkACL('admin', 'com_joomgallery')) + { + $this->setMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_TASK_NOT_PERMITTED', 'migration.applyState'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration', false)); + + return false; + } + + $model = $this->getModel(); + $script = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + $scripts = $model->getScripts(); + + // Check if requested script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new Exception('Requested migration script does not exist.', 1); + } + + // Check if no errors detected in precheck (step 2) + $precheck = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$script.'.step2.success', false); + if(!$precheck) + { + // Pre-checks not successful. Show error message. + $this->setMessage(Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_MIGRATION_CHECKS_FAILED'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step2', false)); + + return false; + } + + // Get input params + $type = $this->app->getInput()->get('type', '', 'string'); + $new_state = $this->app->getInput()->get('state', 0, 'int'); + $src_pk = $this->app->getInput()->get('src_pk', 0, 'int'); + $dest_pk = $this->app->getInput()->get('dest_pk', 0, 'int'); + $error_msg = $this->app->getInput()->get('error', '', 'string'); + $cofirm = $this->app->getInput()->get('confirmation', false, 'bool'); + $json = \json_decode(\base64_decode($this->app->getInput()->get('migrateable', '', 'string')), true); + + if(!$cofirm || empty($src_pk) || ($new_state === 1 && empty($dest_pk))) + { + $this->app->enqueueMessage(Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_FORMCHECK'), 'warning'); + } + else + { + // Attempt to load migration record from database + $item = $model->getItem($json['id']); + + if(\is_null($item->id)) + { + // It seems that the migration record does not yet exists in the database + // Save migration record to database + if(!$model->save($json)) + { + $this->component->setError($model->getError()); + + return false; + } + + // Attempt to load migration record from database + $item = $model->getItem($model->getState('migration.id')); + } + + // Check out migration record if not already checked out + if(\is_null($item->checked_out) || \intval($item->checked_out) < 1) + { + // Check out record + $model->checkout($item->id); + } + + // Mark the state of the specified record + $model->applyState($type, $new_state, $src_pk, $dest_pk, $error_msg); + + $this->app->enqueueMessage(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_SUCCESSFUL_'.$new_state, $type, $src_pk), 'message'); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_('index.php?option=' . _JOOM_OPTION . '&view=migration&layout=step3', false)); + + return true; + } + + /** + * Create a response object + * {success: bool, data: mixed, continue: bool, error: string|array, debug: string|array, warning: string|array} + * + * @param mixed $data The data returned to the frontend + * @param bool $success True if everything was good, false otherwise + * @param mixed $error One or multiple error messages to be printed in the frontend + * + * @return string Response json string + * + * @since 4.0.0 + */ + protected function createRespond($data, bool $success = true, $error = null): string + { + $obj = new \stdClass; + + $obj->success = $success; + $obj->data = $data; + $obj->continue = true; + $obj->error = array(); + $obj->debug = array(); + $obj->warning = array(); + + // Get value for continue + if(!\is_null($this->component->getMigration())) + { + $obj->continue = $this->component->getMigration()->get('continue', true); + } + + // Get debug output + if(!empty($debug = $this->component->getDebug())) + { + $obj->debug = $debug; + } + + // Get warning output + if(!empty($warning = $this->component->getWarning())) + { + $obj->warning = $warning; + } + + // Get error output + if(!empty($error)) + { + if(\is_array($error)) + { + $obj->error = $error; + } + else + { + \array_push($obj->error, $error); + } + } + + return \json_encode($obj, JSON_UNESCAPED_UNICODE); + } + + /** + * Returns an ajax response + * + * @param mixed $results The result to be returned + * @param string $format The format in which the result should be returned + * + * @return void + * + * @since 4.0.0 + */ + protected function ajaxRespond($results, $format=null) + { + $this->app->allowCache(false); + $this->app->setHeader('X-Robots-Tag', 'noindex, nofollow'); + + if(\is_null($format)) + { + $format = strtolower($this->app->getInput()->getWord('format', 'raw')); + } + + // Return the results in the desired format + switch($format) + { + // JSONinzed + case 'json': + echo new JsonResponse($results, null, false, $this->app->getInput()->get('ignoreMessages', true, 'bool')); + + break; + + // Raw format + default: + // Output exception + if($results instanceof \Exception) + { + // Log an error + Log::add($results->getMessage(), Log::ERROR); + + // Set status header code + $this->app->setHeader('status', $results->getCode(), true); + + // Echo exception type and message + $out = \get_class($results) . ': ' . $results->getMessage(); + } + elseif(\is_scalar($results)) + { + // Output string/ null + $out = (string) $results; + } + else + { + // Output array/ object + $out = \implode((array) $results); + } + + echo $out; + + break; + } + } +} diff --git a/administrator/com_joomgallery/src/Extension/JoomgalleryComponent.php b/administrator/com_joomgallery/src/Extension/JoomgalleryComponent.php index 1630c75c..b6811e1f 100644 --- a/administrator/com_joomgallery/src/Extension/JoomgalleryComponent.php +++ b/administrator/com_joomgallery/src/Extension/JoomgalleryComponent.php @@ -42,6 +42,8 @@ use Joomgallery\Component\Joomgallery\Administrator\Service\TusServer\TusServiceTrait; use Joomgallery\Component\Joomgallery\Administrator\Service\Uploader\UploaderServiceInterface; use Joomgallery\Component\Joomgallery\Administrator\Service\Uploader\UploaderServiceTrait; +use Joomgallery\Component\Joomgallery\Administrator\Service\Migration\MigrationServiceInterface; +use Joomgallery\Component\Joomgallery\Administrator\Service\Migration\MigrationServiceTrait; /** * Component class for Joomgallery @@ -75,6 +77,7 @@ class JoomgalleryComponent extends MVCComponent implements BootableExtensionInte use RefresherServiceTrait; use TusServiceTrait; use UploaderServiceTrait; + use MigrationServiceTrait; /** * Storage for the component cache object @@ -83,6 +86,13 @@ class JoomgalleryComponent extends MVCComponent implements BootableExtensionInte */ public $cache = false; + /** + * Storage for the xml of the current component + * + * @var \SimpleXMLElement + */ + public $xml = null; + /** * Storage for the current component version * @@ -115,10 +125,14 @@ public function boot(ContainerInterface $container) $this->cache = new JoomCache(); } + if(!$this->xml) + { + $this->xml = \simplexml_load_file(Path::clean(JPATH_ADMINISTRATOR . '/components/com_joomgallery/joomgallery.xml')); + } + if(!$this->version) { - $xml = \simplexml_load_file(Path::clean(JPATH_ADMINISTRATOR . '/components/com_joomgallery/joomgallery.xml')); - $this->version = (string) $xml->version; + $this->version = (string) $this->xml->version; } } } diff --git a/administrator/com_joomgallery/src/Extension/MessageTrait.php b/administrator/com_joomgallery/src/Extension/MessageTrait.php index 82c227bb..34453e33 100644 --- a/administrator/com_joomgallery/src/Extension/MessageTrait.php +++ b/administrator/com_joomgallery/src/Extension/MessageTrait.php @@ -90,6 +90,15 @@ trait MessageTrait */ protected $log = false; + /** + * Name of the logger to be used + * + * @var string + * + * @since 4.0.0 + */ + protected $logName = null; + /** * Adds the storages to the session * @@ -124,16 +133,25 @@ public function msgFromSession() /** * Add a JoomGallery logger to the JLog class + * + * @param string Name of the specific logger * * @return void * * @since 4.0.0 */ - protected function addLogger() + protected function addLogger(string $name = null) { if(!$this->log) { - Log::addLogger(['text_file' => 'com_joomgallery.log.php'], Log::ALL, ['com_joomgallery']); + if(\is_null($name)) + { + Log::addLogger(['text_file' => 'com_joomgallery.log.php'], Log::ALL, ['com_joomgallery']); + } + else + { + Log::addLogger(['text_file' => 'com_joomgallery.'.$name.'.log.php'], Log::ALL, ['com_joomgallery.'.$name]); + } } $this->log = true; @@ -149,9 +167,36 @@ protected function addLogger() * * @since 4.0.0 */ - protected function addLog($txt, $priority) + protected function addLog(string $txt, int $priority = 8, string $name = null) + { + if(\is_null($name) && \is_null($this->logName)) + { + Log::add($txt, $priority, 'com_joomgallery'); + } + else + { + if(\is_null($name)) + { + $name = $this->logName; + } + + Log::add($txt, $priority, 'com_joomgallery'.$name); + } + } + + /** + * Set a default logger to be used from now on + * + * @param string $name Name of the logger. Empty to use the default JoomGallery logger + * + * @return void + * + * @since 4.0.0 + */ + public function setLogger(string $name = null) { - Log::add($txt, $priority, 'com_joomgallery'); + $this->addLogger($name); + $this->logName = $name; } /** @@ -161,19 +206,20 @@ protected function addLog($txt, $priority) * @param bool $new_line True to add text to a new line (default: true) * @param bool $margin_top True to add an empty line in front (default: false) * @param bool $log True to add error message to logfile (default: false) + * @param string $name Name of the logger to be used (default: null) * * @return void * * @since 4.0.0 */ - public function addDebug($txt, $new_line=true, $margin_top=false, $log=false) + public function addDebug($txt, $new_line=true, $margin_top=false, $log=false, $name=null) { $this->setMsg($txt, 'debug', $new_line, $margin_top); if($log) { - $this->addLogger(); - $this->addLog($txt, Log::DEBUG); + $this->addLogger($name); + $this->addLog($txt, Log::DEBUG, $name); } } @@ -184,19 +230,20 @@ public function addDebug($txt, $new_line=true, $margin_top=false, $log=false) * @param bool $new_line True to add text to a new line (default: true) * @param bool $margin_top True to add an empty line in front (default: false) * @param bool $log True to add error message to logfile (default: false) + * @param string $name Name of the logger to be used (default: null) * * @return void * * @since 4.0.0 */ - public function addWarning($txt, $new_line=true, $margin_top=false, $log=false) + public function addWarning($txt, $new_line=true, $margin_top=false, $log=false, $name=null) { $this->setMsg($txt, 'warning', $new_line, $margin_top); if($log) { - $this->addLogger(); - $this->addLog($txt, Log::WARNING); + $this->addLogger($name); + $this->addLog($txt, Log::WARNING, $name); } } @@ -207,20 +254,21 @@ public function addWarning($txt, $new_line=true, $margin_top=false, $log=false) * @param bool $new_line True to add text to a new line (default: true) * @param bool $margin_top True to add an empty line in front (default: false) * @param bool $log True to add error message to logfile (default: true) + * @param string $name Name of the logger to be used (default: null) * * @return void * * @since 4.0.0 */ - public function setError($txt, $new_line=true, $margin_top=false, $log=true) + public function setError($txt, $new_line=true, $margin_top=false, $log=true, $name=null) { $this->setMsg($txt, 'error', $new_line, $margin_top); $this->error = true; if($log) { - $this->addLogger(); - $this->addLog($txt, Log::ERROR); + $this->addLogger($name); + $this->addLog($txt, Log::ERROR, $name); } } diff --git a/administrator/com_joomgallery/src/Field/ExternalconfigField.php b/administrator/com_joomgallery/src/Field/ExternalconfigField.php index a90e9c82..92c75356 100644 --- a/administrator/com_joomgallery/src/Field/ExternalconfigField.php +++ b/administrator/com_joomgallery/src/Field/ExternalconfigField.php @@ -86,7 +86,7 @@ protected function getInput() $this->value = ComponentHelper::getParams($array[0])->get($array[1]); $this->readonly = true; - $this->description = Text::_(\strval($this->external->element->attributes()->description)) . ' ('.Text::_('COM_JOOMGALLERY_IMAGE_SOURCE').': '.$array[0].')'; + $this->description = Text::_(\strval($this->external->element->attributes()->description)) . ' ('.Text::_('COM_JOOMGALLERY_SOURCE').': '.$array[0].')'; $html = ''.Text::_('JACTION_EDIT').''; $html .= ''; diff --git a/administrator/com_joomgallery/src/Helper/JoomHelper.php b/administrator/com_joomgallery/src/Helper/JoomHelper.php index e8bf53d1..48db6c16 100644 --- a/administrator/com_joomgallery/src/Helper/JoomHelper.php +++ b/administrator/com_joomgallery/src/Helper/JoomHelper.php @@ -35,7 +35,7 @@ class JoomHelper * * @var array */ - protected static $content_types = array('category' => _JOOM_TABLE_CATEGORIES, + public static $content_types = array( 'category' => _JOOM_TABLE_CATEGORIES, 'comment' => _JOOM_TABLE_COMMENTS, 'config' => _JOOM_TABLE_CONFIGS, 'faulty' => _JOOM_TABLE_FAULTIES, @@ -281,12 +281,13 @@ public static function getParent($name, $id) * * @param string $name The name of the record (available: categories,images,tags,imagetypes) * @param Object $com_obj JoomgalleryComponent object if available + * @param string $key Index the returning array by key * * @return array|bool Array on success, false on failure. * * @since 4.0.0 */ - public static function getRecords($name, $com_obj=null) + public static function getRecords($name, $com_obj=null, $key=null) { $availables = array('categories', 'images', 'tags', 'imagetypes'); @@ -297,7 +298,7 @@ public static function getRecords($name, $com_obj=null) return false; } - // get the JoomgalleryComponent object if needed + // Get the JoomgalleryComponent object if needed if(!isset($com_obj) || !\strpos('JoomgalleryComponent', \get_class($com_obj)) === false) { $com_obj = Factory::getApplication()->bootComponent('com_joomgallery'); @@ -313,6 +314,24 @@ public static function getRecords($name, $com_obj=null) // Attempt to load the record. $return = $model->getItems(); + // Indexing the array if needed + if($return && !\is_null($key)) + { + $ind_array = array(); + foreach($return as $obj) + { + if(\property_exists($obj, $key)) + { + $ind_array[$obj->{$key}] = $obj; + } + } + + if(\count($ind_array) > 0) + { + $return = $ind_array; + } + } + return $return; } @@ -681,7 +700,7 @@ public static function getRecordIDbyAliasOrFilename($record, $name) * * @since 4.0.0 */ - protected static function isAvailable($name) + public static function isAvailable($name) { if(!\in_array($name, \array_keys(self::$content_types))) { @@ -700,7 +719,7 @@ protected static function isAvailable($name) * * @since 4.0.0 */ - protected static function getImgZero($type, $url=true, $root=true) + public static function getImgZero($type, $url=true, $root=true) { if($url) { diff --git a/administrator/com_joomgallery/src/Model/MigrationModel.php b/administrator/com_joomgallery/src/Model/MigrationModel.php new file mode 100644 index 00000000..6d5c6765 --- /dev/null +++ b/administrator/com_joomgallery/src/Model/MigrationModel.php @@ -0,0 +1,1285 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Model; + +// No direct access. +defined('_JEXEC') or die; + +use \Joomla\CMS\Factory; +use \Joomla\CMS\Uri\Uri; +use \Joomla\CMS\Form\Form; +use \Joomla\CMS\Language\Text; +use \Joomla\Registry\Registry; +use \Joomla\CMS\Filesystem\Path; +use \Joomla\Utilities\ArrayHelper; +use \Joomla\CMS\Filesystem\Folder; +use \Joomla\CMS\MVC\Model\AdminModel; +use \Joomla\CMS\Language\Multilanguage; +use \Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper; +use \Joomgallery\Component\Joomgallery\Administrator\Table\MigrationTable; + +/** + * Migration model. + * + * @package JoomGallery + * @since 4.0.0 + */ +class MigrationModel extends AdminModel +{ + /** + * @var string Alias to manage history control + * + * @since 4.0.0 + */ + public $typeAlias = _JOOM_OPTION.'.migration'; + + /** + * @var string The prefix to use with controller messages + * + * @since 4.0.0 + */ + protected $text_prefix = _JOOM_OPTION_UC; + + /** + * Storage for the migration form object. + * + * @var Registry + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Name of the migration script. + * + * @var string + * + * @since 4.0.0 + */ + protected $scriptName = ''; + + /** + * Temporary storage of type name. + * + * @var string + * + * @since 4.0.0 + */ + protected $tmp_type = null; + + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * + * @since 4.0.0 + * @throws \Exception + */ + public function __construct($config = array()) + { + parent::__construct($config); + + $this->app = Factory::getApplication('administrator'); + $this->component = $this->app->bootComponent(_JOOM_OPTION); + $this->user = Factory::getUser(); + + // Create config service + $this->component->createConfig(); + } + + /** + * Method to get the migration parameters from the userstate or from the database. + * + * @return array $params The migration parameters entered in the migration form + * + * @since 4.0.0 + */ + public function getParams() + { + // Try to load params from user state + $params = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$this->scriptName.'.params', array()); + + if(!$params || empty($params)) + { + // Load params from db if there are migrateables in database + $db = $this->getDbo(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select('a.params'); + $query->from($db->quoteName(_JOOM_TABLE_MIGRATION, 'a')); + $query->where($db->quoteName('script') . ' = ' . $db->quote($this->scriptName)); + + $db->setQuery($query); + + try + { + $params_db = $db->loadResult(); + } + catch (\RuntimeException $e) + { + $this->component->setError($e->getMessage()); + } + + if($params_db && !empty($params_db)) + { + // Override params from user state with the one from db + $params = \json_decode($params_db, true); + } + } + + return $params; + } + + /** + * Method to set the migration parameters in the model and the migration script. + * + * @param array $params The migration parameters entered in the migration form + * + * @return void + * + * @since 4.0.0 + * @throws \Exception Missing migration params + */ + public function setParams($params = null) + { + $info = $this->getScript(); + + if(\is_null($params)) + { + $params = $this->getParams(); + } + + if(\is_null($params)) + { + throw new \Exception('No migration params found. Please provide some migration params.', 1); + } + + // Set the migration parameters + $this->params = new Registry($params); + $this->component->getMigration()->set('params', $this->params); + } + + /** + * Method to get info array of current migration script. + * + * @return object|boolean Migration info object. + * + * @since 4.0.0 + * @throws \Exception + */ + public function getScriptName() + { + return $this->getScript(); + } + + /** + * Method to get info array of current migration script. + * + * @return object|boolean Migration info object. + * + * @since 4.0.0 + * @throws \Exception + */ + public function getScript() + { + // Retreive script variable + $name = $this->app->getUserStateFromRequest(_JOOM_OPTION.'.migration.script', 'script', '', 'cmd'); + + if(!$name || \strlen($name) < 2 || \strlen($name) > 30) + { + $tmp = new \stdClass; + $tmp->name = ''; + $this->scriptName = ''; + + return $tmp; + } + + $this->scriptName = $name; + + if(!$this->component->getMigration()) + { + $this->component->createMigration($name); + } + + return $this->component->getMigration()->get('info'); + } + + /** + * Method to get all available migration scripts. + * + * @return array|boolean List of paths of all available scripts. + * + * @since 4.0.0 + */ + public function getScripts() + { + $files = Folder::files(JPATH_ADMINISTRATOR.'/components/'._JOOM_OPTION.'/src/Service/Migration/Scripts', '.php$', false, true); + + $scripts = array(); + foreach($files as $path) + { + $img = Uri::base().'components/'._JOOM_OPTION.'/src/Service/Migration/Scripts/'.basename($path, '.php').'.jpg'; + + $scripts[basename($path, '.php')] = array('name' => basename($path, '.php'), 'path' => $path, 'img' => $img); + } + + return $scripts; + } + + /** + * Method to fetch a list of content types which can be migrated using the selected script. + * + * @return array|boolean List of content types on success, false otherwise + * + * @since 4.0.0 + */ + public function getMigrateables() + { + // Retreive script + $script = $this->getScript(); + + if(!$script) + { + throw new \Exception('Migration script not found.'); + } + + $this->setParams(); + + return $this->component->getMigration()->getMigrateables(); + } + + /** + * Method to get a migrateable record by id. + * + * @param integer $pk The id of the primary key. + * @param bool $withQueue True to load the queue if empty. + * + * @return object|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null, $withQueue = true) + { + $item = parent::getItem($pk); + + if(!$item) + { + $item = parent::getItem(null); + } + + // Support for queue field + if(isset($item->queue)) + { + $registry = new Registry($item->queue); + $item->queue = $registry->toArray(); + $item->queue = ArrayHelper::toInteger($item->queue); + } + + // Support for successful field + if(isset($item->successful)) + { + $item->successful = new Registry($item->successful); + } + + // Support for failed field + if(isset($item->failed)) + { + $item->failed = new Registry($item->failed); + } + + // Support for params field + if(isset($item->params)) + { + $item->params = new Registry($item->params); + } + + // Add script if empty + if(empty($item->script)) + { + $item->script = $this->scriptName; + } + + // We can not go further without knowledge about the type + if(\is_null($this->tmp_type)) + { + return $item; + } + else + { + $type = $this->tmp_type; + } + + // Add type if empty + if(empty($item->type)) + { + $item->type = $type; + } + + // Add destination table info if empty + if(empty($item->dst_table)) + { + if(\key_exists($type, JoomHelper::$content_types)) + { + $item->dst_table = JoomHelper::$content_types[$type]; + } + elseif($this->params && !empty($this->params)) + { + // We have a migrateable record whos name does not correspond to the record name + $type_obj = $this->component->getMigration()->getType($type); + + $item->dst_table = JoomHelper::$content_types[$type_obj->get('recordName')]; + } + $item->dst_pk = 'id'; + } + + // We can not go further without a properly loaded migration service + if(\is_null($this->component->getMigration()) || \is_null($this->component->getMigration()->get('params'))) + { + return $item; + } + + // Add source table info if empty + if(empty($item->src_table)) + { + // Get table information + list($src_table, $src_pk) = $this->component->getMigration()->getSourceTableInfo($type); + $item->src_table = $src_table; + $item->src_pk = $src_pk; + } + + // Add queue if empty + if($withQueue && !$item->completed && (\is_null($item->queue) || empty($item->queue))) + { + // Load queue + $item->queue = $this->getQueue($type, $item); + + // Calculate completed state + if(!isset($item->completed) || ($item->completed == false && empty($item->queue))) + { + $table = $this->getTable(); + $table->queue = $item->queue; + $table->successful = $item->successful; + $table->failed = $item->failed; + + $table->clcProgress(); + + $item->completed = $table->completed; + } + } + + // Add params + $item->params = $this->component->getMigration()->get('params'); + + // Empty type storage + $this->tmp_type = null; + + return $item; + } + + /** + * Method to get a list of migration records based on current script. + * Select based on types from migration script. + * + * @return Migrationtable[] An array of migration tables + * + * @since 4.0.0 + */ + public function getItems(): array + { + // Get types from migration service + $types = $this->component->getMigration()->getTypeNames(); + + // Get available types from db + try + { + $db = $this->getDbo(); + $query = $this->getListQuery(); + + if(\is_string($query)) + { + $query = $db->getQuery(true)->setQuery($query); + } + + $db->setQuery($query); + + $tables = $db->loadObjectList('type'); + } + catch (\RuntimeException $e) + { + $this->component->setError($e->getMessage()); + + return array(); + } + + $table = $this->getTable(); + $tmp_pk = null; + if($this->app->input->exists($table->getKeyName())) + { + // Remove id from the input data + $tmp_pk = $this->app->input->get($table->getKeyName(), 'int'); + $this->app->input->set($table->getKeyName(), null); + } + + $items = array(); + foreach($types as $key => $type) + { + // Fill type storage + $this->tmp_type = $type; + + if(!empty($tables) && \key_exists($type, $tables)) + { + // Load item based on id. + $item = $this->getItem($tables[$type]->id); + } + else + { + // Load empty item. + $item = $this->getItem(0); + } + + // Empty type storage + $this->tmp_type = null; + + // Check for a table object error. + if($item === false) + { + $this->component->setError($e->getMessage()); + + return array(); + } + + //array_push($items, $item); + $items[$type] = $item; + } + + // Reset id to input data + if(!\is_null($tmp_pk)) + { + $this->app->input->set($table->getKeyName(), $tmp_pk); + } + + return $items; + } + + /** + * Method to get a list of available migration IDs based on current script. + * Select from #__joomgallery_migration only. + * + * @return array List of IDs + * + * @since 4.0.0 + */ + public function getIdList(): array + { + // Create a new query object. + try + { + $db = $this->getDbo(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select(array('a.id', 'a.script', 'a.type', 'a.checked_out')); + $query->from($db->quoteName(_JOOM_TABLE_MIGRATION, 'a')); + + $db->setQuery($query); + + $list = $db->loadObjectList(); + } + catch (\RuntimeException $e) + { + $this->component->setError($e->getMessage()); + + return array(); + } + + $ids = array(); + foreach($list as $key => $value) + { + if(!array_key_exists($value->script, $ids)) + { + $ids[$value->script] = array($value); + } + else + { + array_push($ids[$value->script], $value); + } + } + + return $ids; + } + + /** + * Method to get the sourceDeletion flag from migration script + * + * @return bool True to offer the task migration.removesource + * + * @since 4.0.0 + */ + public function getSourceDeletion(): bool + { + // Retreive script + $script = $this->getScript(); + + if(!$script) + { + throw new \Exception('Migration script not found.'); + } + + $this->setParams(); + + return $this->component->getMigration()->get('sourceDeletion', false); + } + + /** + * Load the current queue of ids from table + * + * @param string $type Content type + * @param object $table Object containing migration item properties + * + * @return array + * + * @since 4.0.0 + */ + public function getQueue($type, $table=null): array + { + return $this->component->getMigration()->getQueue($type, $table); + } + + /** + * Method to get the migration form. + * + * @param array $data An optional array of data for the form to interogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A \JForm object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Retreive script + $script = $this->getScript(); + + if(!$script) + { + return false; + } + + // Add migration form paths + Form::addFormPath(JPATH_ADMINISTRATOR.'/components/'._JOOM_OPTION.'/src/Service/Migration/Scripts'); + Form::addFormPath(JPATH_ADMINISTRATOR.'/components/'._JOOM_OPTION.'/forms'); + + // Get the form file path + $file = Path::find(Form::addFormPath(), strtolower($script->name) . '.xml'); + if(!is_file($file)) + { + $file = Path::find(Form::addFormPath(), $script->name . '.xml'); + } + + if(!is_file($file)) + { + $this->component->setError('Migration form XML could not be found. XML filename: ' . $script->name . '.xml'); + return false; + } + + // Get the form. + $name = _JOOM_OPTION.'.migration.'.$this->component->getMigration()->get('name'); + $form = $this->loadForm($name, $file, array('control' => 'jform_'.$script->name, 'load_data' => true)); + + if(empty($form)) + { + return false; + } + + return $form; + } + + /** + * Method to perform the pre migration checks. + * + * @param array $params The migration parameters entered in the migration form + * + * @return array An array containing the precheck results. + * + * @since 4.0.0 + */ + public function precheck($params) + { + $script = $this->getScript(); + + if(!$script) + { + throw new \Exception('Migration script not found.'); + } + + // Set the migration parameters + $this->setParams($params); + + // Perform the prechecks + return $this->component->getMigration()->precheck(); + } + + /** + * Method to perform the post migration checks. + * + * @return array|boolean An array containing the postcheck results on success. + * + * @since 4.0.0 + */ + public function postcheck() + { + // Retreive script + $script = $this->getScript(); + + if(!$script) + { + throw new \Exception('Migration script not found.'); + } + + // Set the migration parameters + $this->setParams(); + + // Perform the postchecks + return $this->component->getMigration()->postcheck(); + } + + /** + * Method to perform the migration of one record. + * + * @param string $type Name of the content type to migrate. + * @param integer $pk The primary key of the source record. + * + * @return object The object containing the migration results. + * + * @since 4.0.0 + */ + public function migrate(string $type, int $pk): object + { + // Retreive script + $script = $this->getScript(); + + if(!$script) + { + throw new \Exception('Migration script not found.'); + } + + // Initialise variables + $new_pk = $pk; + $success = true; + $error_msg = ''; + + // Prepare migration service and return migrateable object + $this->setParams(); + $mig = $this->component->getMigration()->prepareMigration($type); + + // Perform the migration of the element if needed + if($this->component->getMigration()->needsMigration($type, $pk)) + { + // Get record data from source + if($data = $this->component->getMigration()->getData($type, $pk)) + { + // Copy source record data + $src_data = (array) clone (object) $data; + + // Convert record data into structure needed for JoomGallery v4+ + $data = $this->component->getMigration()->convertData($type, $data); + + if(!$data) + { + $success = false; + $error_msg = Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_CONVERT_DATA'); + } + else + { + // Create new record at destination based on converted data + $autoIDs = !\boolval($mig->params->get('source_ids', 0)); + $record = $this->insertRecord($type, (array) $data, $autoIDs); + + if(!$record) + { + $success = false; + $error_msg = Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_INSERT_RECORD'); + } + else + { + // Set primary key value of new created record + $new_pk = $record->id; + + // Migration in the filesystem + switch($type) + { + case 'image': + $res = $this->component->getMigration()->migrateFiles($record, $src_data); + $error_msg_end = 'CREATE_IMGTYPE'; + break; + + case 'category': + $res = $this->component->getMigration()->migrateFolder($record, $src_data); + $error_msg_end = 'CREATE_FOLDER'; + + if(!$res) + { + // Stop automatic migration if something went wrong in the filesystem + $this->component->getMigration()->set('continue', false); + } + break; + + default: + $res = true; + break; + } + + if(!$res) + { + $record = $this->deleteRecord($type, $new_pk); + $success = false; + $error_msg = Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_'.$error_msg_end); + } + } + } + } + else + { + $success = false; + $error_msg = Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_FAILED_FETCH_DATA'); + } + } + + // Load migration data table + $table = $this->getTable(); + if(!$table->load($mig->id)) + { + $this->component->setError($table->getError()); + + return $mig; + } + + // Remove migrated primary key from queue + if(($key = \array_search($pk, $table->queue)) !== false) + { + unset($table->queue[$key]); + } + + if($success) + { + // Add migrated primary key to successful object + $table->successful->set($pk, $new_pk); + } + else + { + // Add migrated primary key to failed object + $table->failed->set($pk, $error_msg); + } + + // Add errors + if($error_msg !== '') + { + $this->component->setError($error_msg); + } + + // Calculate progress and completed state + $table->clcProgress(); + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if(!$table->check()) + { + $this->component->setError($table->getError()); + + return false; + } + + $ret_table = clone $table; + + // Save table + if(!$table->store()) + { + $this->component->setError($table->getError()); + + return $mig; + } + + return $ret_table; + } + + /** + * Method to manually apply a state for one record of one migrateable. + * + * @param string $type Name of the content type. + * @param integer $state The new state to be applied. (0: failed, 1:success, 2:pending) + * @param integer $src_pk The primary key of the source record. + * @param integer $dest_pk The primary key of the migrated record at destination. + * @param string $error The error message in case of failed state. + * + * @return object The object containing the migration results. + * + * @since 4.0.0 + */ + public function applyState(string $type, int $state, int $src_pk, int $dest_pk = 0, string $error = ''): object + { + // Retreive script + $script = $this->getScript(); + + if(!$script) + { + throw new \Exception('Migration script not found.'); + } + + // Prepare migration service and return migrateable object + $this->setParams(); + $mig = $this->component->getMigration()->prepareMigration($type); + + // Load migration data table + $table = $this->getTable(); + if(!$table->load($mig->id)) + { + $this->component->setError($table->getError()); + + return $mig; + } + + $removed = false; + switch($state) + { + // apply successful state + case 1: + // Remove primary key from queue + if(($key = \array_search($src_pk, $table->queue)) !== false) + { + unset($table->queue[$key]); + $removed = true; + } + + //Remove primary key from failed + if($table->failed->exists($src_pk)) + { + $table->failed->remove($src_pk); + $removed = true; + } + + // Add migrated primary key to successful object + if($removed) + { + $table->successful->set($src_pk, $dest_pk); + } + break; + + // apply pending state + case 2: + //Remove primary key from successful + if($table->successful->exists($src_pk)) + { + $table->successful->remove($src_pk); + $removed = true; + } + + //Remove primary key from failed + if($table->failed->exists($src_pk)) + { + $table->failed->remove($src_pk); + $removed = true; + } + + // Add primary key to queue + if($removed) + { + \array_push($table->queue, $src_pk); + } + + // Reordering queue + $table->queue = $this->getQueue($type, $table); + + break; + + // apply failed state + default: + // Remove primary key from queue + if(($key = \array_search($src_pk, $table->queue)) !== false) + { + unset($table->queue[$key]); + $removed = true; + } + + //Remove primary key from successful + if($table->successful->exists($src_pk)) + { + $table->successful->remove($src_pk); + $removed = true; + } + + // Add migrated primary key to failed object + if($removed) + { + $table->failed->set($src_pk, $error); + } + break; + } + + // Add errors + if($error !== '') + { + $this->component->setError($error); + } + + if(!$removed) + { + $this->component->setWarning(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_APPLYSTATE_NOT_AVAILABLE', $src_pk)); + } + + // Calculate progress and completed state + $table->clcProgress(); + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if(!$table->check()) + { + $this->component->setError($table->getError()); + + return false; + } + + $ret_table = clone $table; + + // Save table + if(!$table->store()) + { + $this->component->setError($table->getError()); + + return $mig; + } + + return $ret_table; + } + + /** + * Method to delete migration source data. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 4.0.0 + */ + public function deleteSource() + { + // Retreive script + $script = $this->getScript(); + + if(!$script) + { + throw new \Exception('Migration script not found.'); + } + + $this->setParams(); + + // Delete sources + return $this->component->getMigration()->deleteSource(); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + if(!$this->component->getMigration()) + { + $this->getScript(); + } + + // Check the session for previously entered form data. + $name = _JOOM_OPTION.'.migration.'.$this->component->getMigration()->get('name'); + $data = $this->app->getUserState($name.'.step2.data', array()); + + // Check the session for validated migration parameters + $params = $this->getParams(); + + return (empty($params)) ? $data : $params; + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getListQuery() + { + // Retreive script + $script = $this->getScript(); + + if(!$script) + { + throw new \Exception('Migration script not found.'); + } + + // Create a new query object. + $db = $this->getDbo(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select(array('a.id', 'a.type')); + $query->from($db->quoteName(_JOOM_TABLE_MIGRATION, 'a')); + + // Filter for the current script + $query->where($db->quoteName('a.script') . ' = ' . $db->quote($script->name)); + + return $query; + } + + /** + * Method to insert a content type record from migration data. + * + * @param string $type Name of the content type to insert. + * @param array $data The record data gathered from the migration source. + * @param bool $autoID True to auto-increment the id in the database + * + * @return object Inserted record object on success, False on error. + * + * @since 4.0.0 + */ + protected function insertRecord(string $type, array $data, bool $autoID = true) + { + $recordType = $this->component->getMigration()->get('types')[$type]->get('recordName'); + + // Check content type + JoomHelper::isAvailable($recordType); + + // Create table + if(!$table = $this->getMVCFactory()->createTable($recordType, 'administrator')) + { + $this->component->setError(Text::sprintf('COM_JOOMGALLERY_ERROR_IMGTYPE_TABLE_NOT_EXISTING', $type)); + + return false; + } + + // We assume that the record gets newly created during migration step + $isNew = true; + + // Get table primary key name + $key = $table->getKeyName(); + + // Special case: Only modification no creation of record + if(!$this->component->getMigration()->get('types')[$type]->get('insertRecord') && $data[$key] > 0) + { + if($table->load($data[$key])) + { + // Table successfully loaded + $isNew = false; + } + } + + // Special case: Use source IDs. Insert dummy record with JDatabase before binding data on it. + if($isNew && !$autoID && \in_array($key, \array_keys($data))) + { + if(!$this->insertDummyRecord($type, $data[$key])) + { + // Insert dummy failed. Stop migration. + return false; + } + + if(!$table->load($data[$key])) + { + $this->component->setError($table->getError()); + + return false; + } + } + + // Change language to 'All' if multilanguage is not enabled + if($isNew && !Multilanguage::isEnabled()) + { + $data['language'] = '*'; + } + + // Reset task + $tmp_task = $this->app->input->get('task', '', 'cmd'); + $this->app->input->set('task', 'save'); + + if($isNew && $this->component->getMigration()->get('types')[$type]->get('nested')) + { + // Assumption: parent primary key name for all nested types at destination is 'parent_id' + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Bind migrated data to table object + if(!$table->bind($data)) + { + $this->component->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if(!$table->check()) + { + $this->component->setError($table->getError()); + + return false; + } + + // Trigger the onMigrationBeforeSave event + $event = new \Joomla\Event\Event('onMigrationBeforeSave', ['com_joomgallery.'.$recordType, $table]); + $this->getDispatcher()->dispatch($event->getName(), $event); + $results = $event->getArgument('result', []); + + // Store the data. + if(\in_array(false, $results, true) || !$table->store()) + { + $this->component->setError($table->getError()); + + return false; + } + + // Restore task + $this->app->input->set('task', $tmp_task); + + return $table; + } + + /** + * Method to delete a content type record in destination table. + * + * @param string $type Name of the content type to insert. + * @param array $data The record data gathered from the migration source. + * @param bool $newID True to auto-increment the id in the database + * + * @return bool True if record was successfully deleted, false otherwise. + * + * @since 4.0.0 + */ + protected function deleteRecord(string $type, int $pk): bool + { + // Check content type + JoomHelper::isAvailable($type); + + // Create table + if(!$table = $this->getMVCFactory()->createTable($type, 'administrator')) + { + $this->component->setError(Text::sprintf('COM_JOOMGALLERY_ERROR_IMGTYPE_TABLE_NOT_EXISTING', $type)); + + return false; + } + + // Load the table. + $table->load($pk); + + if($type === 'image') + { + // Delete corresponding imagetypes + $manager = JoomHelper::getService('FileManager'); + + if(!$manager->deleteImages($table)) + { + $this->component->setError($this->component->getDebug(true)); + + return false; + } + } + + if(!$table->delete($pk)) + { + $this->component->setError($table->getError()); + + return false; + } + + return true; + } + + /** + * Method to insert an empty dummy record with a given primary key + * + * @param string $type Name of the content type to insert. + * @param int $key Primary key to use. + * + * @return bool|int Primary key of the created dummy record or false on failure + * + * @since 4.0.0 + */ + protected function insertDummyRecord(string $type, int $key) + { + list($db, $dbPrefix) = $this->component->getMigration()->getDB('destination'); + $date = Factory::getDate(); + + // Create and populate a dummy object. + $record = new \stdClass(); + $record->id = $key; + + $needed = array('category'); + if(\in_array($type, $needed)) + { + $record->lft = 2147483644; + $record->rgt = 2147483645; + } + + $needed = array('image', 'category', 'comment', 'gallery', 'tag'); + if(\in_array($type, $needed)) + { + $record->description = ''; + } + + $needed = array('image'); + if(\in_array($type, $needed)) + { + $record->date = $date->toSql(); + $record->imgmetadata = ''; + $record->filename = ''; + } + + $needed = array('image', 'category', 'imagetype', 'user'); + if(\in_array($type, $needed)) + { + $record->params = ''; + } + + $needed = array('image', 'category', 'gallery'); + if(\in_array($type, $needed)) + { + $record->metadesc = ''; + $record->metakey = ''; + } + + $needed = array('image', 'category', 'field', 'tag', 'gallery', 'user', 'vote', 'comment'); + if(\in_array($type, $needed)) + { + $record->created_time = $date->toSql(); + } + + $needed = array('image', 'category', 'tag', 'gallery', 'comment'); + if(\in_array($type, $needed)) + { + $record->modified_time = $date->toSql(); + } + + // Insert the object into the user profile table. + if(!$db->insertObject(JoomHelper::$content_types[$type], $record)) + { + $this->component->setError(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_ERROR_DUMMY_RECORD', $type, $key)); + + return false; + } + else + { + return $key; + } + } +} diff --git a/administrator/com_joomgallery/src/Service/FileManager/FileManager.php b/administrator/com_joomgallery/src/Service/FileManager/FileManager.php index e4ca0df1..064bfa6b 100644 --- a/administrator/com_joomgallery/src/Service/FileManager/FileManager.php +++ b/administrator/com_joomgallery/src/Service/FileManager/FileManager.php @@ -1071,23 +1071,24 @@ public function getImgPath($img, $type, $catid=false, $filename=false, $root=fal /** * Returns the path to a category without root path. * - * @param object|int|string $cat Category object, category ID or category alias (new categories: ID=0) - * @param string|bool $type Imagetype if needed - * @param object|int|string|bool $parent Parent category object, parent category ID, parent category alias or parent category path (default: false) - * @param string|bool $alias The category alias (default: false) - * @param boolean $root True to add the system root to the path + * @param object|int|string $cat Category object, category ID or category alias (new categories: ID=0) + * @param string|bool $type Imagetype if needed + * @param object|int|string|bool $parent Parent category object, parent category ID, parent category alias or parent category path (default: false) + * @param string|bool $alias The category alias (default: false) + * @param boolean $root True to add the system root to the path + * @param boolean $compatibility Take into account the compatibility mode when creating the path * * * @return mixed Path to the category on success, false otherwise * * @since 4.0.0 */ - public function getCatPath($cat, $type=false, $parent=false, $alias=false, $root=false) + public function getCatPath($cat, $type=false, $parent=false, $alias=false, $root=false, $compatibility=true) { // We got a valid category object - if(\is_object($cat) && isset($cat->path)) + if(\is_object($cat) && \property_exists($cat, 'path')) { - $path = $cat->path; + $path = $this->catReadPath($cat, $compatibility); } // We got a category path elseif(\is_string($cat) && $this->is_path($cat)) @@ -1112,21 +1113,21 @@ public function getCatPath($cat, $type=false, $parent=false, $alias=false, $root return false; } - $path = $cat->path; + $path = $this->catReadPath($cat, $compatibility); } // We got a parent category plus alias elseif($parent && $alias) { // We got a valid parent category object - if(\is_object($parent) && isset($parent->path)) + if(\is_object($parent) && \property_exists($parent, 'path')) { - if(empty($parent->path)) + if(empty($this->catReadPath($parent, $compatibility))) { $path = $alias; } else { - $path = $parent->path.\DIRECTORY_SEPARATOR.$alias; + $path = $this->catReadPath($parent, $compatibility).\DIRECTORY_SEPARATOR.$alias; } } // We got a parent category path @@ -1152,13 +1153,13 @@ public function getCatPath($cat, $type=false, $parent=false, $alias=false, $root return false; } - if(empty($parent->path)) + if(empty($this->catReadPath($parent, $compatibility))) { $path = $alias; } else { - $path = $parent->path.\DIRECTORY_SEPARATOR.$alias; + $path = $this->catReadPath($parent, $compatibility).\DIRECTORY_SEPARATOR.$alias; } } } @@ -1348,4 +1349,28 @@ protected function is_path($string) return true; } + + /** + * Get path from category object + * + * @param object $cat Category object + * @param boolean $compatibility Take into account the compatibility mode + * + * @return string Path to the category + * + * @since 4.0.0 + */ + protected function catReadPath(object $cat, bool $compatibility): string + { + if($compatibility && $this->component->getConfig()->get('jg_compatibility_mode', 0) && !empty($cat->static_path)) + { + // Compatibility mode active + return $cat->static_path; + } + else + { + // Standard method + return $cat->path; + } + } } diff --git a/administrator/com_joomgallery/src/Service/Migration/Checks.php b/administrator/com_joomgallery/src/Service/Migration/Checks.php new file mode 100644 index 00000000..248250d8 --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/Checks.php @@ -0,0 +1,349 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Service\Migration; + +// No direct access +\defined('_JEXEC') or die; + +use \Joomla\CMS\Language\Text; + +/** + * Migration Checks Class + * Providing a structure for the results of migration checks + * + * @package JoomGallery + * @since 4.0.0 + */ +class Checks +{ + /** + * List of assets available in the check-objects + * array(categoryName.checkName => categoryKey.checkKey) + * + * @var array + * + * @since 4.0.0 + */ + private $assets = []; + + /** + * The array of check-objects + * + * @var \stdClass[] + * + * @since 4.0.0 + */ + private $objects = []; + + /** + * The overall success of all checks + * True if all checks were successful, false otherwise + * + * @var bool + * + * @since 4.0.0 + */ + private $success = true; + + /** + * The overall error message + * This message is displayed on top of the results display + * + * @var string + * + * @since 4.0.0 + */ + private $message = ''; + + /** + * Register a new category or modify an existing one + * + * @param string $name The name of the category + * @param string $title Optional: Title of the category + * @param string $desc Optional: Description of the category + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function addCategory(string $name, string $title = '', string $desc = '', string $colTitle = '') + { + // Make category name lowercase + $name = \strtolower(\trim($name)); + + if(!\in_array($name, \array_keys($this->assets))) + { + // Category not yet existing, create a new one + $cat = new \stdClass(); + $cat->name = $name; + $cat->title = $title; + $cat->desc = $desc; + $cat->colTitle = (empty($colTitle)) ? Text::_('COM_JOOMGALLERY_CHECK') : $colTitle; + $cat->checks = []; + + // Add category to check-objects array + $key = $this->array_push($this->objects, $cat); + + // Add category to assets array + $this->assets[$name] = $key; + } + else + { + // You try to add a category already existing + throw new \Exception('You try to add a category that already exists. If you want to modify it, use "modCategory()" instead.', 1); + } + } + + /** + * Modify an existing category + * + * @param string $name The name of the category + * @param string $title Optional: Title of the category + * @param string $desc Optional: Description of the category + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function modCategory(string $name, $title = null, $desc = null) + { + // Make category name lowercase + $name = \strtolower(\trim($name)); + + if(!\in_array($name, \array_keys($this->assets))) + { + // You try to modify a category which does not exist + throw new \Exception('You try to modify a category which does not exists. Please add the category first.', 1); + } + else + { + $key = $this->assets[$name]; + + // Modify title and/or description + if(!\is_null($title)) + { + $this->objects[$key]->title = (string) $title; + } + + if(!\is_null($desc)) + { + $this->objects[$key]->desc = (string) $desc; + } + } + } + + /** + * Add a new check beeing performed + * + * @param string $category The category of the check + * @param string $name The name of the check + * @param bool $result True if the check was successful, false otherwise + * @param bool $warning True if the check should be displayed as a warning + * @param string $title Optional: Title of the check + * @param string $desc Optional: Description of the check + * @param string $help Optional: URL to a help-site or help-text + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function addCheck(string $category, string $name, bool $result, bool $warning = false, string $title = '', string $desc = '', string $help = '') + { + // Make category and check name lowercase + $category = \strtolower(\trim($category)); + $name = \strtolower(\trim($name)); + $asset = $category.'.'.$name; + + // Check if category exists + if(!\in_array($category, \array_keys($this->assets))) + { + throw new \Exception('You try to add a check to a category which is not existing. Please add the category first.', 1); + } + + // Check if asset exists + if(!\in_array($asset, \array_keys($this->assets))) + { + // Get category key + $catKey = $this->assets[$category]; + + // Asset not yet existing, create a new one + $check = new \stdClass(); + $check->name = $name; + $check->result = $result; + $check->warning = $warning; + $check->title = $title; + $check->desc = $desc; + $check->help = $help; + + // Add check to check-objects array + $key = $this->array_push($this->objects[$catKey]->checks, $check); + + // Add check to assets array + $this->assets[$asset] = $catKey.'.'.$key; + + // Modify the overall success if needed + if($result === false) + { + $this->success = false; + + if($this->message === '') + { + $this->message = $title; + } + } + else + { + if($warning && $this->message === '') + { + // Add message if there is a warning + $this->message = $title; + } + } + } + else + { + // You try to add a check already existing + throw new \Exception('You try to add a check that already exists. If you want to modify it, use "modCheck()" instead.', 2); + } + } + + /** + * Modify an existing check + * + * @param string $category The category of the check + * @param string $name The name of the check + * @param bool $result True if the check was successful, false otherwise + * @param bool $warning True if the check should be displayed as a warning + * @param string $title Optional: Title of the check + * @param string $desc Optional: Description of the check + * @param string $help Optional: URL to a help-site or help-text + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function modCheck(string $category, string $name, $result = null, $warning = null, $title = null, $desc = null, $help = null) + { + // Make category and check name lowercase + $category = \strtolower(\trim($category)); + $name = \strtolower(\trim($name)); + $asset = $category.'.'.$name; + + // Check if category exists + if(!\in_array($category, \array_keys($this->assets))) + { + throw new \Exception('You try to modify a check in a category which is not existing. Please add the category first.', 1); + } + + // Check if asset exists + if(!\in_array($asset, \array_keys($this->assets))) + { + // You try to modify a check which does not exist + throw new \Exception('You try to modify a check which does not exists. Please add the check first.', 2); + } + else + { + $key = $this->assets[$asset]; + list($catKey, $checkKey) = \explode('.', $key, 2); + + // Modify the result + if(!\is_null($result)) + { + $this->objects[$catKey]->checks[$checkKey]->result = \boolval($result); + + // Modify the overall success if needed + if(\boolval($result) === false) + { + $this->success = false; + } + } + + // Modify the warning status + if(!\is_null($warning)) + { + $this->objects[$catKey]->checks[$checkKey]->warning = \boolval($warning); + } + + // Modify the title + if(!\is_null($title)) + { + $this->objects[$catKey]->checks[$checkKey]->title = (string) $title; + } + + // Modify the description + if(!\is_null($desc)) + { + $this->objects[$catKey]->checks[$checkKey]->desc = (string) $desc; + } + + // Modify the description + if(!\is_null($help)) + { + $this->objects[$catKey]->checks[$checkKey]->help = (string) $help; + } + } + } + + /** + * Returns all registered checks + * + * @return array A list of checks + * + * @since 4.0.0 + */ + public function getChecks(): array + { + return $this->objects; + } + + /** + * Returns the overall success of the checks + * + * @return bool True if all checks were successful, false otherwise + * + * @since 4.0.0 + */ + public function getSuccess(): bool + { + return $this->success; + } + + /** + * Returns the registered checks and the overall success + * + * @return array array($this->success, $this->objects, $this->message) + * + * @since 4.0.0 + */ + public function getAll(): array + { + return array($this->success, $this->objects, $this->message); + } + + /** + * Wrapper for the php function 'array_push' with new created key as return value + * + * @return int Key of the new created array entry + * + * @since 4.0.0 + */ + protected function array_push(array &$array, $item): int + { + $next = \count($array); + $array[$next] = $item; + + return $next; + } +} \ No newline at end of file diff --git a/administrator/com_joomgallery/src/Service/Migration/Migration.php b/administrator/com_joomgallery/src/Service/Migration/Migration.php new file mode 100644 index 00000000..c7c409d6 --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/Migration.php @@ -0,0 +1,1711 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Service\Migration; + +// No direct access +\defined('_JEXEC') or die; + +use \Joomla\CMS\Factory; +use \Joomla\Registry\Registry; +use \Joomla\CMS\Language\Text; +use \Joomla\CMS\Filesystem\Path; +use \Joomla\Database\DatabaseFactory; +use \Joomla\Database\DatabaseInterface; +use \Joomla\Component\Media\Administrator\Exception\FileNotFoundException; +use \Joomgallery\Component\Joomgallery\Administrator\Table\ImageTable; +use \Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper; +use \Joomgallery\Component\Joomgallery\Administrator\Table\CategoryTable; +use \Joomgallery\Component\Joomgallery\Administrator\Table\MigrationTable; +use \Joomgallery\Component\Joomgallery\Administrator\Extension\ServiceTrait; +use \Joomgallery\Component\Joomgallery\Administrator\Service\Migration\Checks; +use \Joomgallery\Component\Joomgallery\Administrator\Service\Migration\MigrationInterface; + +/** + * Migration Base Class + * + * @package JoomGallery + * @since 4.0.0 + */ +abstract class Migration implements MigrationInterface +{ + use ServiceTrait; + + /** + * Storage for the migration form object. + * + * @var Registry + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Storage for the migration info object. + * + * @var object + * + * @since 4.0.0 + */ + protected $info = null; + + /** + * Name of the migration script. + * + * @var string + * + * @since 4.0.0 + */ + protected $name = ''; + + /** + * True to offer the task migration.removesource for this script + * + * @var boolean + * + * @since 4.0.0 + */ + protected $sourceDeletion = false; + + /** + * Is the migration performed from the command line + * + * @var boolean + * + * @since 4.0.0 + */ + protected $isCli = false; + + /** + * List of content types which can be migrated with this script + * Use the singular form of the content type (e.g image, not images) + * + * @var Types[] + * + * @since 4.0.0 + */ + protected $types = array(); + + /** + * List of migrateables processed/migrated with this script + * + * @var MigrationTable[] + * + * @since 4.0.0 + */ + protected $migrateables = array(); + + /** + * True, if the migration process of the current content type should be continued + * False to stop the automatic migration process. + * + * @var boolean + * + * @since 4.0.0 + */ + protected $continue = true; + + /** + * Constructor + * + * @return void + * + * @since 4.0.0 + */ + public function __construct() + { + // Load application + $this->getApp(); + + // Load component + $this->getComponent(); + + // Try to load language file of the migration script + $this->app->getLanguage()->load('com_joomgallery.migration.'.$this->name, _JOOM_PATH_ADMIN); + + // Set logger + $this->component->setLogger('migration'); + + // Fill info object + $this->info = new \stdClass; + $this->info->name = $this->name; + $this->info->title = Text::_('FILES_JOOMGALLERY_MIGRATION_'.strtoupper($this->name).'_TITLE'); + $this->info->description = Text::_('FILES_JOOMGALLERY_MIGRATION_'.strtoupper($this->name).'_DESC'); + } + + /** + * Destructor + * + * @return void + * + * @since 4.0.0 + */ + public function __destruct() + { + // Reset logger to default + $this->component->setLogger(); + } + + /** + * A list of content type definitions depending on migration source + * (Required in migration scripts. The order of the content types must correspond to its migration order) + * + * ------ + * This method is multiple times, when the migration types are loaded. The first time it is called without + * the $type param, just to retrieve the array of source types info. The next times it is called with a + * $type param to load the optional type infos like ownerFieldname. + * + * Needed: tablename, primarykey, isNested, isCategorized + * Optional: ownerFieldname, dependent_on, pkstoskip, insertRecord, queueTablename, recordName + * + * Assumption for insertrecord: + * If insertrecord == true assumes, that type is a migration; Means reading data from source db and write it to destination db (default) + * If insertrecord == false assumes, that type is an adjustment; Means reading data from destination db adjust it and write it back to destination db + * + * Attention: + * Order of the content types must correspond to the migration order + * Pay attention to the dependent_on when ordering here !!! + * + * @param bool $names_only True to load type names only. No migration parameters required. + * @param Type $type Type object to set optional definitions + * + * @return array The source types info, array(tablename, primarykey, isNested, isCategorized) + * + * @since 4.0.0 + */ + public function defineTypes($names_only=false, &$type=null): array + { + /* Example: + $types = array( 'category' => array('#__joomgallery_catg', 'cid', true, false), + 'image' => array('#__joomgallery', 'id', false, true) + ); + */ + + return array(); + } + + /** + * Converts data from source into the structure needed for JoomGallery. + * (Optional in migration scripts, but highly recommended.) + * + * ------ + * How mappings work: + * - Key not in the mapping array: Nothing changes. Field value can be magrated as it is. + * - 'old key' => 'new key': Field name has changed. Old values will be inserted in field with the provided new key. + * - 'old key' => false: Field does not exist anymore or value has to be emptied to create new record in the new table. + * - 'old key' => array(string, string, bool): Field will be merget into another field of type json. + * 1. ('destination field name'): Name of the field to be merged into. + * 2. ('new field name'): New name of the field created in the destination field. (default: false / retain field name) + * 3. ('create child'): True, if a child node shall be created in the destination field containing the field values. (default: false / no child) + * + * + * @param string $type Name of the content type + * @param array $data Source data received from getData() + * + * @return array Converted data to save into JoomGallery + * + * @since 4.0.0 + */ + public function convertData(string $type, array $data): array + { + return $data; + } + + /** + * - Load a queue of ids from a specific migrateable object + * - Reload/Reorder the queue if migrateable object already has queue + * + * @param string $type Content type + * @param object $migrateable Mibrateable object + * + * @return array + * + * @since 4.0.0 + */ + public function getQueue(string $type, object $migrateable=null): array + { + if(\is_null($migrateable)) + { + if(!$migrateable = $this->getMigrateable($type)) + { + return array(); + } + } + + $this->loadTypes(); + + // Queue gets always loaded from source db + $tablename = $this->types[$type]->get('queueTablename'); + $primarykey = $this->types[$type]->get('pk'); + + // Get db object + list($db, $prefix) = $this->getDB('source'); + + // Initialize query object + $query = $db->getQuery(true); + + // Create the query + $query->select($db->quoteName($primarykey)) + ->from($db->quoteName($tablename)) + ->order($db->quoteName($primarykey) . ' ASC'); + + // Apply id filter + // Reorder the queue if queue is not empty + if(\property_exists($migrateable, 'queue') && !empty($migrateable->queue)) + { + $queue = (array) $migrateable->get('queue', array()); + $query->where($db->quoteName($primarykey) . ' IN (' . implode(',', $queue) .')'); + } + + // Gather migration types info + if(empty($this->get('types'))) + { + $this->getSourceTableInfo($type); + } + + // Apply ordering based on level if it is a nested type + if($this->get('types')[$type]->get('nested')) + { + $query->order($db->quoteName('level') . ' ASC'); + } + + $db->setQuery($query); + + // Attempt to load the queue + try + { + return $db->loadColumn(); + } + catch(\Exception $e) + { + $this->component->setError($e->getMessage()); + + return array(); + } + } + + /** + * Returns an associative array containing the record data from source. + * + * @param string $type Name of the content type + * @param int $pk The primary key of the content type + * + * @return array Associated array of a record data + * + * @since 4.0.0 + */ + public function getData(string $type, int $pk): array + { + $this->loadTypes(); + + if($this->get('types')[$type]->get('insertRecord')) + { + // When insertRecord is set to true, we assume that data gets loaded from source table + list($tablename, $primarykey) = $this->getSourceTableInfo($type); + + // Get db object + list($db, $prefix) = $this->getDB('source'); + } + else + { + // We assume that this migration is just a data adjustment inside the destination table + $tablename = JoomHelper::$content_types[$this->get('types')[$type]->get('recordName')]; + $primarykey = 'id'; + + // Get db object + list($db, $prefix) = $this->getDB('destination'); + } + + // Initialize query object + $query = $db->getQuery(true); + + // Create the query + $query->select('*') + ->from($db->quoteName($tablename)) + ->where($db->quoteName($primarykey) . ' = ' . $db->quote($pk)); + + // Reset the query using our newly populated query object. + $db->setQuery($query); + + // Attempt to load the array + try + { + return $db->loadAssoc(); + } + catch(\Exception $e) + { + $this->component->setError($e->getMessage()); + + return array(); + } + } + + /** + * Performs the neccessary steps to migrate an image in the filesystem + * + * @param ImageTable $img ImageTable object, already stored + * @param array $data Source data received from getData() + * + * @return bool True on success, false otherwise + * + * @since 4.0.0 + */ + public function migrateFiles(ImageTable $img, array $data): bool + { + // Default: Recreate images based on source image + $this->component->createFileManager(); + + // Get source image + $img_source = $this->getImageSource($data); + + // Update catid based on migrated categories + $migrated_cats = $this->get('migrateables')['category']->successful; + $migrated_catid = $migrated_cats->get($img->catid); + + // Create imagetypes + return $this->component->getFileManager()->createImages($img_source, $img->filename, $migrated_catid); + } + + /** + * Performs the neccessary steps to migrate a category in the filesystem + * + * @param CategoryTable $cat CategoryTable object, already stored + * @param array $data Source data received from getData() + * + * @return bool True on success, false otherwise + * + * @since 4.0.0 + */ + public function migrateFolder(CategoryTable $cat, array $data): bool + { + // Default: Create new folders + $this->component->createFileManager(); + return $this->component->getFileManager()->createCategory($cat->alias, $cat->parent_id); + } + + /** + * Returns a list of content types which can be migrated. + * + * @return Migrationtable[] List of content types + * + * @since 4.0.0 + */ + public function getMigrateables(): array + { + if(empty($this->migrateables)) + { + // Get MigrationModel + $model = $this->component->getMVCFactory()->createModel('migration', 'administrator'); + + // Load migrateables + $this->migrateables = $model->getItems(); + } + + return $this->migrateables; + } + + /** + * Returns an object of a specific content type which can be migrated. + * + * @param string $type Name of the content type + * @param string $withQueue True to load the queue if not available + * + * @return Migrationtable|bool Object of the content types on success, false otherwise + * + * @since 4.0.0 + */ + public function getMigrateable(string $type, bool $withQueue = true) + { + if( !\key_exists($type, $this->migrateables) || empty($this->migrateables[$type]) || + ($withQueue && empty($this->migrateables[$type]->queue)) + ) + { + // Get MigrationModel + $model = $this->component->getMVCFactory()->createModel('migration', 'administrator'); + + // Get list of migration ids + $mig_ids = $model->getIdList(); + + if(!empty($mig_ids)) + { + // Detect id of the requested type + $id = 0; + foreach($mig_ids[$this->name] as $key => $mig) + { + if($mig->type == $type) + { + $id = $mig->id; + } + } + + // Load migrateable + if($id > 0) + { + $this->migrateables[$type] = $model->getItem($id, $withQueue); + + return $this->migrateables[$type]; + } + } + + } + + return false; + } + + /** + * Prepare the migration. + * + * @param string $type Name of the content type + * + * @return MigrationTable The currently processed migrateable + * + * @since 4.0.0 + */ + public function prepareMigration(string $type): object + { + // Load migrateables to migration service + $this->getMigrateables(); + + // Set the migration parameters + $migrateableKey = 0; + foreach($this->migrateables as $key => $migrateable) + { + if($migrateable->type == $type) + { + $this->setParams($migrateable->params); + $migrateableKey = $key; + + continue; + } + } + + return $this->migrateables[$migrateableKey]; + } + + /** + * Step 2 + * Perform pre migration checks. + * + * @return object[] An array containing the precheck results. + * + * @since 4.0.0 + */ + public function precheck(): array + { + // Instantiate a new checks class + $checks = new Checks(); + + // Check general requirements + $checks->addCategory('general', Text::_('COM_JOOMGALLERY_GENERAL'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_GENERAL_PRECHECK_DESC')); + $this->checkLogFile($checks, 'general'); + $this->checkSiteState($checks, 'general'); + + // Check source extension (version, compatibility) + $checks->addCategory('source', Text::_('COM_JOOMGALLERY_SOURCE'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_SOURCE_PRECHECK_DESC')); + $this->checkSourceExtension($checks, 'source'); + + // Check existance and writeability of source directories + $this->checkSourceDir($checks, 'source'); + + // Check existence and integrity of source database tables + $this->checkSourceTable($checks, 'source'); + + // Check destination extension (version, compatibility) + $checks->addCategory('destination', Text::_('COM_JOOMGALLERY_DESTINATION'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_DESTINATION_PRECHECK_DESC')); + $this->checkDestExtension($checks, 'destination'); + + // Check existance and writeability of destination directories + $this->checkDestDir($checks, 'destination'); + + // Check existence and integrity of destination database tables + $this->checkDestTable($checks, 'destination'); + + // Check image mapping + if($this->params->get('image_usage', 0) > 1) + { + $this->checkImageMapping($checks, 'destination'); + } + + // Perform some script specific checks + $this->scriptSpecificChecks('pre', $checks, 'general'); + + return $checks->getAll(); + } + + /** + * Step 4 + * Perform post migration checks. + * + * @return object[] An array containing the postcheck results. + * + * @since 4.0.0 + */ + public function postcheck() + { + // Instantiate a new checks class + $checks = new Checks(); + + // Check general migration + $checks->addCategory('general', Text::_('COM_JOOMGALLERY_GENERAL'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_GENERAL_POSTCHECK_DESC')); + + // Check if all queues have been addressed and migrated + $this->checkMigrationQueues($checks, 'general'); + // Check if there are still errors in the migration + $this->checkMigrationErrors($checks, 'general'); + + // Check database + // $checks->addCategory('database', Text::_('JLIB_FORM_VALUE_SESSION_DATABASE'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_DB_POSTCHECK_DESC')); + // $this->checkCategories($checks, 'database'); + // $this->checkImages($checks, 'database'); + + // Check filesystem + // $checks->addCategory('directories', Text::_('COM_JOOMGALLERY_DIRECTORIES'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_DIRS_POSTCHECK_DESC')); + // $this->checkFolder($checks, 'directories'); + + // Perform some script specific checks + $this->scriptSpecificChecks('post', $checks, 'general'); + + return $checks->getAll(); + } + + /** + * Delete migration source data. + * It's recommended to use delete source data by uninstalling source extension if possible. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 4.0.0 + */ + public function deleteSource() + { + return true; + } + + /** + * Get a database object + * + * @param string $target The target (source or destination) + * + * @return array list($db, $dbPrefix) + * + * @since 4.0.0 + * @throws \Exception + */ + public function getDB(string $target): array + { + if(!in_array($target, array('source', 'destination'))) + { + throw new \Exception('Taget has to be eighter "source" or "destination". Given: ' . $target, 1); + } + + if($target === 'destination' || $this->params->get('same_db')) + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + $dbPrefix = $this->app->get('dbprefix'); + } + else + { + $options = array ('driver' => $this->params->get('dbtype'), 'host' => $this->params->get('dbhost'), 'user' => $this->params->get('dbuser'), 'password' => $this->params->get('dbpass'), 'database' => $this->params->get('dbname'), 'prefix' => $this->params->get('dbprefix')); + $dbFactory = new DatabaseFactory(); + $db = $dbFactory->getDriver($this->params->get('dbtype'), $options); + $dbPrefix = $this->params->get('dbprefix'); + } + + return array($db, $dbPrefix); + } + + /** + * Set params to object + * + * @param mixed $params Array or object of params + * + * @since 4.0.0 + */ + public function setParams($params) + { + $this->params = new Registry($params); + } + + /** + * Returns the Joomla root path of the source. + * + * @return string Source Joomla root path + * + * @since 4.0.0 + */ + protected function getSourceRootPath(): string + { + if($this->params->get('same_joomla', 1)) + { + $root = Path::clean(JPATH_ROOT . '/'); + } + else + { + $root = Path::clean($this->params->get('joomla_path')); + + if(\substr($root, -1) != '/') + { + $root = Path::clean($root . '/'); + } + } + + return $root; + } + + /** + * Loads all available content types to Migration object. + * Gets available with the function defineTypes() from migration script. + * + * @return void + * + * @since 4.0.0 + */ + protected function loadTypes() + { + if(empty($this->types)) + { + if(\is_null($this->params)) + { + throw new \Exception('Migration parameters need to be set in order to load types.', 1); + } + + // First call of defineTypes(): Retrieve the array of source types info + $types = $this->defineTypes(); + + // Create Types objects + foreach($types as $key => $list) + { + $type = new Type($key, $list); + + // Pass $type by reference + // Next calls of defineTypes(): Define optional type infos + $this->defineTypes(false, $type); + + $this->types[$key] = $type; + } + + // Fill the dependent_of based on $types->dependent_on + foreach($this->types as $key => $type) + { + $type->setDependentOf($this->types); + } + } + } + + /** + * Returns a list of involved source tables. + * + * @return array List of table names (Joomla style, e.g #__joomgallery) + * array('image' => '#__joomgallery', ...) + * + * @since 4.0.0 + */ + public function getSourceTables(): array + { + $this->loadTypes(); + + $tables = array(); + foreach($this->types as $key => $type) + { + $tables[$key] = $this->types[$key]->get('queueTablename'); + } + + return $tables; + } + + /** + * Returns tablename and primarykey name of the source table + * + * @param string $type The content type name + * + * @return array The corresponding source table info + * list(tablename, primarykey) + * + * @since 4.0.0 + */ + public function getSourceTableInfo(string $type): array + { + $this->loadTypes(); + + return array($this->types[$type]->get('tablename'), $this->types[$type]->get('pk')); + } + + /** + * Returns a list of content type names available in this migration script. + * + * @return Type[] List of type names + * array('image', 'category', ...) + * + * @since 4.0.0 + */ + public function getTypeNames(): array + { + if(\is_null($this->params)) + { + throw new \Exception('Migration parameters need to be set in order to load types.', 1); + } + + $types = $this->defineTypes(true); + + return $types; + } + + /** + * Returns a type object based on type name. + * + * @param string $type The content type name + * + * @return Type Type object + * + * @since 4.0.0 + */ + public function getType(string $name): Type + { + $this->loadTypes(); + + return $this->types[$name]; + } + + /** + * True if the given record has to be migrated + * False to skip the migration for this record + * + * @param string $type Name of the content type + * @param int $pk The primary key of the content type + * + * @return bool True to continue migration, false to skip it + * + * @since 4.0.0 + */ + public function needsMigration(string $type, int $pk): bool + { + $this->loadTypes(); + + // Content types that require another type beeing migrated completely + if(!empty($this->types[$type])) + { + foreach($this->types[$type]->get('dependent_on') as $key => $req) + { + if(!$this->migrateables[$req] || !$this->migrateables[$req]->completed || $this->migrateables[$req]->failed->count() > 0) + { + $this->continue = false; + $this->component->setError(Text::sprintf('FILES_JOOMGALLERY_MIGRATION_PREREQUIREMENT_ERROR', \implode(', ', $this->types[$type]->get('dependent_on')))); + + return false; + } + } + } + + // Specific record primary keys which can be skiped + foreach($this->types[$type]->get('pkstoskip') as $skip) + { + if($pk == $skip) + { + return false; + } + } + + return true; + } + + /** + * Precheck: Check logfile and add check to checks array. + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkLogFile(Checks &$checks, string $category) + { + $log_dir = Path::clean($this->app->get('log_path')); + + if(\is_dir($log_dir)) + { + $log_file = Path::clean($log_dir . '/' . 'com_joomgallery.log.php'); + + if(\is_file($log_file)) + { + if(\is_writable($log_dir)) + { + $checks->addCheck($category, 'log_file', true, false, Text::_('COM_JOOMGALLERY_LOGFILE'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_LOGFILE_SUCCESS', $log_file)); + } + else + { + $checks->addCheck($category, 'log_file', false, false, Text::_('COM_JOOMGALLERY_LOGFILE'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_LOGFILE_ERROR', $log_file)); + } + } + else + { + if(\is_writable($log_dir)) + { + $checks->addCheck($category, 'log_dir', true, false, Text::_('COM_JOOMGALLERY_LOGDIRECTORY'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_LOGDIR_SUCCESS', $log_dir)); + } + else + { + $checks->addCheck($category, 'log_dir', false, false, Text::_('COM_JOOMGALLERY_LOGDIRECTORY'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_LOGDIR_ERROR', $log_dir)); + } + } + } + else + { + $checks->addCheck($category, 'log_dir', false, false, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_LOG_DIR_LABEL'), Text::_('Logging directory not existent.')); + } + + } + + /** + * Precheck: Check the source extension to be the correct one for this migration script + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkSourceExtension(Checks &$checks, string $category) + { + $src_info = $this->getTargetinfo('source'); + + if(!($src_xml = $this->getSourceXML())) + { + // Source XML not found + $checks->addCheck($category, 'src_xml', false, false, Text::_('COM_JOOMGALLERY_FIELDS_SRC_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_SOURCE_XML', $src_xml)); + return; + } + + if(\version_compare(PHP_VERSION, $src_info->get('php_min'), '<')) + { + // PHP version not supported + $checks->addCheck($category, 'src_extension', false, false, Text::_('COM_JOOMGALLERY_FIELDS_SRC_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_PHP_WRONG_VERSION', PHP_VERSION, $src_info->get('php_min'))); + } + elseif(\strval($src_xml->name) !== $src_info->get('extension')) + { + // Wrong source extension + $checks->addCheck($category, 'src_extension', false, false, Text::_('COM_JOOMGALLERY_FIELDS_SRC_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_NOT_SUPPORTED', \strval($src_xml->name))); + } + elseif(\version_compare($src_xml->version, $src_info->get('min'), '<') || \version_compare($src_xml->version, $src_info->get('max'), '>')) + { + // Version not correct + $checks->addCheck($category, 'src_extension', false, false, Text::_('COM_JOOMGALLERY_FIELDS_SRC_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_WRONG_VERSION', $src_xml->version, $src_info->get('min') . ' - ' . $src_info->get('max'))); + } + else + { + // Check successful + $checks->addCheck($category, 'src_extension', true, false, Text::_('COM_JOOMGALLERY_FIELDS_SRC_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_SUCCESS', \strval($src_xml->name), $src_xml->version)); + } + } + + /** + * Precheck: Check the destination extension to be the correct one for this migration script + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkDestExtension(Checks &$checks, string $category) + { + $dest_info = $this->getTargetinfo('destination'); + $version = \str_replace('-dev', '', $this->component->version); + + if(\version_compare(PHP_VERSION, $dest_info->get('php_min'), '<')) + { + // PHP version not supported + $checks->addCheck($category, 'dest_extension', false, false, Text::_('COM_JOOMGALLERY_FIELDS_SRC_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_PHP_WRONG_VERSION', PHP_VERSION, $dest_info->get('php_min'))); + } + elseif(\strval($this->component->xml->name) !== $dest_info->get('extension')) + { + // Wrong destination extension + $checks->addCheck($category, 'dest_extension', false, false, Text::_('COM_JOOMGALLERY_FIELDS_DEST_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_NOT_SUPPORTED', \strval($this->component->xml->name))); + } + elseif(\version_compare($version, $dest_info->get('min'), '<') || \version_compare($version, $dest_info->get('max'), '>')) + { + // Version not correct + $checks->addCheck($category, 'dest_extension', false, false, Text::_('COM_JOOMGALLERY_FIELDS_DEST_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_WRONG_VERSION', $this->component->version, $dest_info->get('min') . ' - ' . $dest_info->get('max'))); + } + else + { + // Check successful + $checks->addCheck($category, 'dest_extension', true, false, Text::_('COM_JOOMGALLERY_FIELDS_DEST_EXTENSION_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_EXTENSION_SUCCESS', \strval($this->component->xml->name), $this->component->version)); + } + } + + /** + * Precheck: Check site state and add check to checks array. + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkSiteState(Checks &$checks, string $category) + { + if($this->app->get('offline')) + { + $checks->addCheck($category, 'offline', true, false, Text::_('COM_JOOMGALLERY_SITE_OFFLINE'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_OFFLINE_SUCCESS')); + } + else + { + $checks->addCheck($category, 'offline', false, false, Text::_('COM_JOOMGALLERY_SITE_OFFLINE'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_OFFLINE_ERROR')); + } + } + + /** + * Precheck: Check directories of the source to be existent + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkSourceDir(Checks &$checks, string $category) + { + // Retrieve a list of source directories involved in migration + $directories = $this->getSourceDirs(); + $root = $this->getSourceRootPath(); + + $dirs_checked = array(); + foreach($directories as $dir) + { + // Make sure, we check each directory only once + if(!\in_array($dir, $dirs_checked)) + { + \array_push($dirs_checked, $dir); + } + else + { + // Table already checked. Skip check. + continue; + } + + $check_name = 'src_dir_' . \basename($dir); + + if(!\is_dir($root . $dir)) + { + // Path is not a directory + $checks->addCheck($category, $check_name, false, false, Text::_('COM_JOOMGALLERY_DIRECTORY') . ': ' . $dir, Text::_('COM_JOOMGALLERY_SERVICE_ERROR_FILESYSTEM_NOT_A_DIRECTORY')); + } + else + { + $checks->addCheck($category, $check_name, true, false, Text::_('COM_JOOMGALLERY_DIRECTORY') . ': ' . $dir, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_DIRECTORY_SUCCESS')); + } + } + } + + /** + * Precheck: Check directories of the destination to be existent and writeable + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkDestDir(Checks &$checks, string $category) + { + // Instantiate filesystem service + $this->component->createFilesystem($this->component->getConfig()->get('jg_filesystem','local-images')); + + // Get all imagetypes + $imagetypes = JoomHelper::getRecords('imagetypes', $this->component); + + $dirs_checked = array(); + foreach($imagetypes as $imagetype) + { + // Make sure, we check each directory only once + if(!\in_array($imagetype, $dirs_checked)) + { + \array_push($dirs_checked, $imagetype); + } + else + { + // Table already checked. Skip check. + continue; + } + + $check_name = 'dest_dir_' . $imagetype->typename; + $error = false; + + try + { + $dir_info = $this->component->getFilesystem()->getFile($imagetype->path); + } + catch(FileNotFoundException $msg) + { + // Path doesn't exist + $checks->addCheck($category, $check_name, false, false, Text::_('COM_JOOMGALLERY_DIRECTORY') . ': ' . $imagetype->path, Text::_('COM_JOOMGALLERY_ERROR_PATH_NOT_EXISTING')); + $error = true; + } + catch(\Exception $msg) + { + // Error in filesystem + $checks->addCheck($category, $check_name, false, false, Text::_('COM_JOOMGALLERY_DIRECTORY') . ': ' . $imagetype->path, Text::sprintf('COM_JOOMGALLERY_SERVICE_ERROR_FILESYSTEM_ERROR', $msg)); + $error = true; + } + + if(!$error) + { + if($dir_info->type !== 'dir') + { + // Path is not a directory + $checks->addCheck($category, $check_name, false, false, Text::_('COM_JOOMGALLERY_DIRECTORY') . ': ' . $imagetype->path, Text::_('COM_JOOMGALLERY_SERVICE_ERROR_FILESYSTEM_NOT_A_DIRECTORY')); + } + else + { + $checks->addCheck($category, $check_name, true, false, Text::_('COM_JOOMGALLERY_DIRECTORY') . ': ' . $imagetype->path, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_DIRECTORY_SUCCESS')); + } + } + } + } + + /** + * Precheck: Check db and tables of the source + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkSourceTable(Checks &$checks, string $category) + { + list($db, $dbPrefix) = $this->getDB('source'); + + // Check connection to database + try + { + $tableList = $db->getTableList(); + } + catch (\Exception $msg) + { + $checks->addCheck($category, 'src_table_connect', true, Text::_('JLIB_FORM_VALUE_SESSION_DATABASE'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_TABLE_CONN_ERROR')); + } + + // Check required tables + $tables = $this->getSourceTables(); + $tables_checked = array(); + foreach($tables as $tablename) + { + // Make sure, we check each table only once + if(!\in_array($tablename, $tables_checked)) + { + \array_push($tables_checked, $tablename); + } + else + { + // Table already checked. Skip check. + continue; + } + + $check_name = 'src_table_' . $tablename; + + // Check if required tables exists + if(!\in_array(\str_replace('#__', $dbPrefix, $tablename), $tableList)) + { + $checks->addCheck($category, $check_name, false, false, Text::_('COM_JOOMGALLERY_TABLE') . ': ' . $tablename, Text::_('COM_JOOMGALLERY_ERROR_TABLE_NOT_EXISTING')); + continue; + } + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($tablename); + $db->setQuery($query); + + $count = $db->loadResult(); + + // Check number of records in tables + $check_name = 'dest_table_' . $tablename . '_count'; + if($count == 0) + { + $checks->addCheck($category, $check_name, true, false, Text::_('COM_JOOMGALLERY_TABLE') . ': ' . $tablename, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES_EMPTY')); + } + else + { + $checks->addCheck($category, $check_name, true, false, Text::_('COM_JOOMGALLERY_TABLE') . ': ' . $tablename, Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES', $count)); + } + } + } + + /** + * Precheck: Check db and tables of the destination + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkDestTable(Checks &$checks, string $category) + { + // Get table info + list($db, $dbPrefix) = $this->getDB('destination'); + $tables = JoomHelper::$content_types; + $tableList = $db->getTableList(); + + // Check whether root category exists + $rootCat = false; + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName(_JOOM_TABLE_CATEGORIES)) + ->where($db->quoteName('id') . ' = 1') + ->where($db->quoteName('title') . ' = ' . $db->quote('Root')) + ->where($db->quoteName('parent_id') . ' = 0'); + $db->setQuery($query); + + if($db->loadResult()) + { + $checks->addCheck($category, 'dest_root_cat', true, false, Text::_('COM_JOOMGALLERY_ROOT_CATEGORY'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_CAT_SUCCESS')); + $rootCat = true; + } + else + { + $checks->addCheck($category, 'dest_root_cat', false, false, Text::_('COM_JOOMGALLERY_ROOT_CATEGORY'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_CAT_ERROR')); + } + + // Check whether root asset exists + $query = $db->getQuery(true) + ->select('id') + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = ' . $db->quote(_JOOM_OPTION)) + ->where($db->quoteName('parent_id') . ' = 1'); + $db->setQuery($query); + + if($rootAssetID = $db->loadResult()) + { + $checks->addCheck($category, 'dest_root_asset', true, false, Text::_('COM_JOOMGALLERY_ROOT_ASSET'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_ASSET_SUCCESS')); + } + else + { + $checks->addCheck($category, 'dest_root_asset', false, false, Text::_('COM_JOOMGALLERY_ROOT_ASSET'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_ASSET_ERROR')); + } + + // Check whether root category asset exists + $query = $db->getQuery(true) + ->select('id') + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = ' . $db->quote('com_joomgallery.category.1')) + ->where($db->quoteName('parent_id') . ' = ' . $db->quote($rootAssetID)); + $db->setQuery($query); + + if($db->loadResult()) + { + $checks->addCheck($category, 'dest_root_cat_asset', true, false, Text::_('COM_JOOMGALLERY_ROOT_CAT_ASSET'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_CAT_ASSET_SUCCESS')); + } + else + { + $checks->addCheck($category, 'dest_root_cat_asset', false, false, Text::_('COM_JOOMGALLERY_ROOT_CAT_ASSET'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ROOT_CAT_ASSET_ERROR')); + } + + // Check required tables + $tables_checked = array(); + foreach($tables as $tablename) + { + // Make sure, we check each table only once + if(!\in_array($tablename, $tables_checked)) + { + \array_push($tables_checked, $tablename); + } + else + { + // Table already checked. Skip check. + continue; + } + + $check_name = 'dest_table_' . $tablename; + + // Check if required tables exists + if(!\in_array( \str_replace('#__', $dbPrefix, $tablename), $tableList)) + { + $checks->addCheck($category, $check_name, false, false, Text::_('COM_JOOMGALLERY_TABLE') . ': ' . $tablename, Text::_('COM_JOOMGALLERY_ERROR_TABLE_NOT_EXISTING')); + continue; + } + + // Check number of records in tables + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($tablename); + $db->setQuery($query); + + $count = $db->loadResult(); + + if($tablename == _JOOM_TABLE_CATEGORIES && $rootCat) + { + $count = $count - 1; + } + + $check_name = 'dest_table_' . $tablename . '_count'; + if($count == 0) + { + $checks->addCheck($category, $check_name, true, false, Text::_('COM_JOOMGALLERY_TABLE') . ': ' . $tablename, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES_EMPTY')); + } + elseif($this->params->get('source_ids', 0) > 0 && $count > 0) + { + $checks->addCheck($category, $check_name, true, false, Text::_('COM_JOOMGALLERY_TABLE') . ': ' . $tablename, Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES', $count)); + $this->checkDestTableIdAvailability($checks, $category, $tablename); + } + else + { + $checks->addCheck($category, $check_name, true, false, Text::_('COM_JOOMGALLERY_TABLE') . ': ' . $tablename, Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES', $count)); + } + } + } + + /** + * Precheck: Check destination tables for already existing ids + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * @param string $tablename The table to be checked + * + * @return void + * + * @since 4.0.0 + */ + protected function checkDestTableIdAvailability(Checks &$checks, string $category, string $tablename) + { + // Get content type to check + $type = ''; + foreach(JoomHelper::$content_types as $type => $table) + { + if($table === $tablename) + { + break; + } + + $type = ''; + } + + // Get migrateable to check + $this->getMigrateables(); + $migrateable = null; + foreach($this->migrateables as $key => $migrateable) + { + if($migrateable->get('type', false) === $type) + { + break; + } + + $migrateable = null; + } + + if(!$migrateable) + { + // Table does not correspont to a migrateable. Exit method. + return; + } + + // Get destination database + list($db, $dbPrefix) = $this->getDB('destination'); + + // Get a list of used ids from destination database + $destQuery = $db->getQuery(true); + $destQuery->select($db->quoteName('id')) + ->from($db->quoteName($tablename)); + $destQuery_string = \trim($destQuery->__toString()); + + if($this->params->get('same_db', 1)) + { + // Get list of used ids from source databse + $srcQuery = $db->getQuery(true); + $srcQuery->select($db->quoteName($migrateable->get('src_pk'), 'id')) + ->from($db->quoteName($migrateable->get('src_table'))); + $srcQuery_string = \trim($srcQuery->__toString()); + + // Get a list of ids used in both source and destination + $query = $db->getQuery(true); + $query->select($db->quoteName('ids.id')) + ->from('(' . $srcQuery_string . ') ids') + ->where($db->quoteName('ids.id') . ' IN (' . $destQuery_string . ')'); + $db->setQuery($query); + } + else + { + // Get source database + list($src_db, $src_dbPrefix) = $this->getDB('source'); + + // Get list of used ids from the source database + $query = $src_db->getQuery(true); + $query->select($db->quoteName($migrateable->get('src_pk'), 'id')) + ->from($db->quoteName($migrateable->get('src_table'))); + $src_db->setQuery($query); + + // Load list from source database + $src_list = $src_db->loadColumn(); + + if(\count($src_list) < 1) + { + // There are no records in the source tabele. Exit method. + return; + } + + // Create UNION query string + foreach($src_list as $i => $id) + { + ${'query' . $i} = $db->getQuery(true); + ${'query' . $i}->select($db->quote($id) . ' AS ' . $db->quoteName('id')); + if($i > 0) + { + $query0->unionAll(${'query' . $i}); + } + } + $srcQuery_string = \trim($query0->__toString()); + + // Get a list of ids used in both source and destination + $query = $db->getQuery(true); + $query->select($db->quoteName('ids.id')) + ->from('(' . $srcQuery_string . ') ids') + ->where($db->quoteName('ids.id') . ' IN (' . $destQuery_string . ')'); + $db->setQuery($query); + } + + // Load list of Id's used in both tables (source and destination) + $list = $db->loadColumn(); + + // Exception for root category + if($tablename == _JOOM_TABLE_CATEGORIES) + { + $list = \array_diff($list, array(1, '1')); + } + + if(!empty($list)) + { + $checks->addCheck($category, 'dest_table_' . $tablename . '_ids', false, false, Text::_('COM_JOOMGALLERY_TABLE') . ': ' . $tablename, Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_TABLES_USE_IDS_HINT', \implode(',', $list))); + } + } + + /** + * Precheck: Check the configured image mapping + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkImageMapping(Checks &$checks, string $category) + { + $mapping = $this->params->get('image_mapping'); + $dest_imagetypes = JoomHelper::getRecords('imagetypes', $this->component); + $src_imagetypes = array(); + + // Check if mapping contains enough elements + if(\count((array)$mapping) != \count($dest_imagetypes)) + { + $checks->addCheck($category, 'mapping_count', false, false, Text::_('COM_JOOMGALLERY_FIELDS_IMAGEMAPPING_LABEL'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_MAPPING_ERROR')); + return; + } + + // Load source imagetypes from xml file + $xml = \simplexml_load_file(JPATH_ADMINISTRATOR.'/components/'._JOOM_OPTION.'/src/Service/Migration/Scripts/'. $this->name . '.xml'); + $element = $xml->xpath('/form/fieldset/field[@name="image_mapping"]/form/field[@name="source"]'); + + foreach($element[0]->option as $option) + { + \array_push($src_imagetypes, (string) $option['value']); + } + + // Prepare destination imagetypes + $tmp_dest_imagetypes = array(); + foreach($dest_imagetypes as $key => $type) + { + \array_push($tmp_dest_imagetypes, (string) $type->typename); + } + + // Check if all imagetypes are correctly set in the mapping + foreach($mapping as $key => $mapVal) + { + if(\in_array($mapVal->destination, $tmp_dest_imagetypes)) + { + // Remove imagetype from tmp_dest_imagetypes array + $tmp_dest_imagetypes = \array_diff($tmp_dest_imagetypes, array($mapVal->destination)); + } + else + { + // Destination imagetype in mapping does not exist + $checks->addCheck($category, 'mapping_dest_types_'.$mapVal->destination, false, false, Text::_('COM_JOOMGALLERY_FIELDS_IMAGEMAPPING_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_MAPPING_DEST_IMAGETYPE_NOT_EXIST', Text::_('COM_JOOMGALLERY_' . \strtoupper($mapVal->destination)))); + return; + } + + if(!\in_array($mapVal->source, $src_imagetypes)) + { + // Source imagetype in mapping does not exist + $checks->addCheck($category, 'mapping_src_types_'.$mapVal->source, false, false, Text::_('COM_JOOMGALLERY_FIELDS_IMAGEMAPPING_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_MAPPING_IMAGETYPE_NOT_EXIST', Text::_('COM_JOOMGALLERY_' . \strtoupper($mapVal->source)))); + return; + } + } + + if(!empty($tmp_dest_imagetypes)) + { + // Destination imagetype not used in the mapping + $checks->addCheck($category, 'mapping_dest_types', false, false, Text::_('COM_JOOMGALLERY_FIELDS_IMAGEMAPPING_LABEL'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_MAPPING_IMAGETYPE_NOT_USED', \implode(', ', $tmp_dest_imagetypes))); + } + } + + /** + * Postcheck: Check if all queues are completed + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkMigrationQueues(Checks &$checks, string $category) + { + $types = $this->getTypeNames(); + $migrateables = $this->getMigrateables(); + + // Check if all types are existent in migrateables + if(\count(\array_diff($types, \array_keys($migrateables))) > 0) + { + $checks->addCheck($category, 'types_migrated', false, false, Text::_('COM_JOOMGALLERY_MIGRATIONS'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_MIGRATIONS_ERROR')); + return; + } + + // Check if all queues in migrateables are empty + $empty = true; + foreach($migrateables as $key => $mig) + { + if(\count($mig->queue) > 0) + { + $checks->addCheck($category, 'queue_' . $mig->type, false, false, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_QUEUE'), Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_MIGRATIONS_ERROR', $mig->type)); + $empty = false; + } + } + + if($empty) + { + $checks->addCheck($category, 'queues', true, false, Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_QUEUE'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_COUNT_MIGRATIONS_SUCCESS')); + } + } + + /** + * Postcheck: Check if all migrateables are error free + * + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + protected function checkMigrationErrors(Checks &$checks, string $category) + { + $migrateables = $this->getMigrateables(); + + // Check if all migrateables are error free + $errors = false; + foreach($migrateables as $key => $mig) + { + if($mig->failed->count() > 0) + { + foreach($mig->failed->toArray()as $id => $error) + { + $checks->addCheck($category, 'error_' . $mig->type . '_' . $id, false, false, Text::_('ERROR') . ': ' . $mig->type, Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_ERRORS_ERROR', $mig->type, $id, $error)); + $errors = true; + } + } + } + + if(!$errors) + { + $checks->addCheck($category, 'errors', true, false, Text::_('SUCCESS'), Text::_('COM_JOOMGALLERY_SERVICE_MIGRATION_ERRORS_SUCCESS')); + } + } + + /** + * Perform script specific checks at the end of pre and postcheck. + * + * @param string $type Type of checks (pre or post) + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + public function scriptSpecificChecks(string $type, Checks &$checks, string $category) + { + return; + } + + /** + * Converts a data array based on a mapping. + * + * @param array $data Data received from getData() method. + * @param array $mapping Mapping array telling how to convert data. + * @param string $pk_name Name of the destination primary key. (default: 'id') + * + * @return array Converted data array + * + * @since 4.0.0 + */ + protected function applyConvertData(array $data, array $mapping, string $pk_name = 'id'): array + { + // Convert data only if a mapping is available + if(empty($mapping)) + { + return $data; + } + + // Loop through the data provided + foreach($data as $key => $value) + { + // Key not in the mapping array --> Nothing to do. + if(!\key_exists($key, $mapping)) + { + continue; + } + + // Mapping from an old to a new key ('old key' => 'new key') + if(\is_string($mapping[$key]) && !empty($mapping[$key])) + { + $data[$mapping[$key]] = $value; + + if($key !== $mapping[$key]) + { + unset($data[$key]); + } + + continue; + } + + // Remove content from data array element ('old key' => false) + if($mapping[$key] === false) + { + $data[$key] = null; + + continue; + } + + // Content gets merged into anothter data array element ('old key' => array(string, string, bool)) + // array('destination field name', 'new field name', 'create child') + if(\is_array($mapping[$key])) + { + $destFieldName = $mapping[$key][0]; + + // Prepare destField + if(!\key_exists($destFieldName, $data) || empty($data[$destFieldName])) + { + // Field does not exist or is empty + $data[$destFieldName] = new Registry(); + } + elseif(!($data[$destFieldName] instanceof Registry)) + { + // Field exists and is already of type Registry + $data[$destFieldName] = new Registry($data[$destFieldName]); + } + + // Create new field name + $newKey = $key; + if(\count($mapping[$key]) > 1 && !empty($mapping[$key][1])) + { + $newKey = $mapping[$key][1]; + } + + // Prepare srcField + if(\count($mapping[$key]) > 2 && !empty($mapping[$key][2])) + { + // Add as a child node + $child = new Registry($value); + $value = new Registry(array($newKey=> $child)); + } + else + { + // Add directly + $srcLenght = 1; + $isJson = false; + + // Detect we have a json string + if(\is_string($value)) + { + \json_decode($value); + if(json_last_error() === JSON_ERROR_NONE) + { + $isJson = true; + } + } + + // Get source lenght + elseif(\is_array($value)) + { + $srcLenght = \count($value); + } + elseif(\is_object($value)) + { + if($value instanceof \Countable) + { + $srcLenght = $value->count(); + } + else + { + $srcLenght = \count(\get_object_vars($value)); + } + } + + if($srcLenght > 1 || $isJson) + { + // We are trying to add a json or an object directly without adding a child + // Here 'new field name' has no effect + $value = new Registry($value); + } + else + { + if(\is_array($value)) + { + $value = $value[0]; + } + elseif(\is_object($value)) + { + $keys = \array_keys(\get_object_vars($value)); + $value = $value[$keys[0]]; + } + + // Create registry with only one key value pair + $value = new Registry(array($newKey=> $value)); + } + } + + // Apply merge + $data[$destFieldName]->merge($value); + + if($key != $destFieldName) + { + unset($data[$key]); + } + + continue; + } + } + + // Make sure the primary key field is available in the data array + if(!\array_key_exists($pk_name, $data)) + { + $data[$pk_name] = null; + } + + return $data; + } +} diff --git a/administrator/com_joomgallery/src/Service/Migration/MigrationInterface.php b/administrator/com_joomgallery/src/Service/Migration/MigrationInterface.php new file mode 100644 index 00000000..b72f3c48 --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/MigrationInterface.php @@ -0,0 +1,339 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Service\Migration; + +// No direct access +\defined('_JEXEC') or die; + +use \Joomgallery\Component\Joomgallery\Administrator\Table\ImageTable; +use \Joomgallery\Component\Joomgallery\Administrator\Table\CategoryTable; + +/** + * Interface for the migration service class + * + * @package JoomGallery + * @since 4.0.0 + */ +interface MigrationInterface +{ + // /** + // * Name of the migration script to be used. + // * (Required in migration scripts.) + // * + // * @var string + // * + // * @since 4.0.0 + // */ + // protected $name = 'scriptName'; + + // /** + // * True to offer the task migration.removesource for this script + // * (Required in migration scripts.) + // * + // * @var boolean + // * + // * @since 4.0.0 + // */ + // protected $sourceDeletion = false; + + /** + * Returns an object with compatibility info for this migration script. + * (Required in migration scripts.) + * + * @param string $type Select if you get source or destination info + * + * @return Targetinfo Compatibility info object + * + * @since 4.0.0 + */ + public function getTargetinfo(string $type = 'source'): Targetinfo; + + /** + * Returns the XML object of the source extension + * (Required in migration scripts. Source extension XML must at least provide name and version info.) + * + * @return \SimpleXMLElement Extension XML object or False on failure + * + * @since 4.0.0 + */ + public function getSourceXML(); + + /** + * A list of content type definitions depending on migration source + * (Required in migration scripts. The order of the content types must correspond to its migration order) + * + * ------ + * This method is multiple times, when the migration types are loaded. The first time it is called without + * the $type param, just to retrieve the array of source types info. The next times it is called with a + * $type param to load the optional type infos like ownerFieldname. + * + * Needed: tablename, primarykey, isNested, isCategorized + * Optional: ownerFieldname, dependent_on, pkstoskip, insertRecord, queueTablename, recordName + * + * Assumption for insertrecord: + * If insertrecord == true assumes, that type is a migration; Means reading data from source db and write it to destination db (default) + * If insertrecord == false assumes, that type is an adjustment; Means reading data from destination db adjust it and write it back to destination db + * + * Attention: + * Order of the content types must correspond to the migration order + * Pay attention to the dependent_on when ordering here !!! + * + * @param bool $names_only True to load type names only. No migration parameters required. + * @param Type $type Type object to set optional definitions + * + * @return array The source types info, array(tablename, primarykey, isNested, isCategorized) + * + * @since 4.0.0 + */ + public function defineTypes($names_only=false, &$type=null): array; + + /** + * Returns a list of involved source directories. + * (Required in migration scripts.) + * + * @return array List of paths + * + * @since 4.0.0 + */ + public function getSourceDirs(): array; + + /** + * Fetches an array of images from source to be used for creating the imagetypes + * for the current image. + * (Required in migration scripts.) + * + * @param array $data Source record data received from getData() - before convertData() + * + * @return array List of images from sources used to create the new imagetypes + * 1. If imagetypes get recreated: array('image/source/path') + * 2. If imagetypes get copied/moved: array('original' => 'image/source/path1', 'detail' => 'image/source/path2', ...) + * + * @since 4.0.0 + */ + public function getImageSource(array $data): array; + + /** + * Converts data from source into the structure needed for JoomGallery. + * (Optional in migration scripts, but highly recommended.) + * + * ------ + * How mappings work: + * - Key not in the mapping array: Nothing changes. Field value can be magrated as it is. + * - 'old key' => 'new key': Field name has changed. Old values will be inserted in field with the provided new key. + * - 'old key' => false: Field does not exist anymore or value has to be emptied to create new record in the new table. + * - 'old key' => array(string, string, bool): Field will be merget into another field of type json. + * 1. ('destination field name'): Name of the field to be merged into. + * 2. ('new field name'): New name of the field created in the destination field. (default: false / retain field name) + * 3. ('create child'): True, if a child node shall be created in the destination field containing the field values. (default: false / no child) + * + * + * @param string $type Name of the content type + * @param array $data Source data received from getData() + * + * @return array Converted data to save into JoomGallery + * + * @since 4.0.0 + */ + public function convertData(string $type, array $data): array; + + /** + * Load the a queue of ids from a specific migrateable object + * (Optional in migration scripts, but needed if queues have to be specially threated.) + * + * @param string $type Content type + * @param object $migrateable Mibrateable object + * + * @return array + * + * @since 4.0.0 + */ + public function getQueue(string $type, object $migrateable=null): array; + + /** + * Returns an associative array containing the record data from source. + * (Optional in migration scripts, can be overwritten if required.) + * + * @param string $type Name of the content type + * @param int $pk The primary key of the content type + * + * @return array Record data + * + * @since 4.0.0 + */ + public function getData(string $type, int $pk): array; + + /** + * Perform pre migration checks. + * (Optional in migration scripts, can be overwritten if required.) + * + * @return array|boolean An array containing the precheck results on success. + * + * @since 4.0.0 + */ + public function precheck(): array; + + /** + * Perform post migration checks. + * (Optional in migration scripts, can be overwritten if required.) + * + * @return void + * + * @since 4.0.0 + */ + public function postcheck(); + + /** + * Get a database object + * (Optional in migration scripts, can be overwritten if required.) + * + * @param string $target The target (source or destination) + * + * @return array list($db, $dbPrefix) + * + * @since 4.0.0 + * @throws \Exception + */ + public function getDB(string $target): array; + + /** + * Returns a list of content types which can be migrated. + * (Optional in migration scripts, can be overwritten if required.) + * + * @return array List of content types + * + * @since 4.0.0 + */ + public function getMigrateables(): array; + + /** + * Returns an object of a specific content type which can be migrated. + * + * @param string $type Name of the content type + * @param string $withQueue True to load the queue if not available + * + * @return Migrationtable|bool Object of the content types on success, false otherwise + * + * @since 4.0.0 + */ + public function getMigrateable(string $type, bool $withQueue = true); + + /** + * Returns tablename and primarykey name of the source table + * (Optional in migration scripts, can be overwritten if required.) + * + * @param string $type The content type name + * + * @return array The corresponding source table info + * list(tablename, primarykey) + * + * @since 4.0.0 + */ + public function getSourceTableInfo(string $type): array; + + /** + * Returns a list of involved source tables. + * (Optional in migration scripts, can be overwritten if required.) + * + * @return array List of table names (Joomla style, e.g #__joomgallery) + * array('image' => '#__joomgallery', ...) + * + * @since 4.0.0 + */ + public function getSourceTables(): array; + + /** + * Returns a list of content type names available in this migration script. + * (Optional in migration scripts, can be overwritten if required.) + * + * @return Type[] List of type names + * array('image', 'category', ...) + * + * @since 4.0.0 + */ + public function getTypeNames(): array; + + /** + * Returns a type object based on type name. + * (Optional in migration scripts, can be overwritten if required.) + * + * @param string $type The content type name + * + * @return Type Type object + * + * @since 4.0.0 + */ + public function getType(string $name): Type; + + /** + * True if the given record has to be migrated + * False to skip the migration for this record + * (Optional in migration scripts, can be overwritten if required.) + * + * @param string $type Name of the content type + * @param int $pk The primary key of the content type + * + * @return bool True to continue migration, false to skip it + * + * @since 4.0.0 + */ + public function needsMigration(string $type, int $pk): bool; + + /** + * Performs the neccessary steps to migrate an image in the filesystem + * (Optional in migration scripts, can be overwritten if required.) + * + * @param ImageTable $img ImageTable object, already stored + * @param array $data Source data received from getData() + * + * @return bool True on success, false otherwise + * + * @since 4.0.0 + */ + public function migrateFiles(ImageTable $img, array $data): bool; + + /** + * Performs the neccessary steps to migrate a category in the filesystem + * (Optional in migration scripts, can be overwritten if required.) + * + * @param CategoryTable $cat CategoryTable object, already stored + * @param array $data Source data received from getData() + * + * @return bool True on success, false otherwise + * + * @since 4.0.0 + */ + public function migrateFolder(CategoryTable $cat, array $data): bool; + + /** + * Perform script specific checks at the end of pre and postcheck. + * (Optional in migration scripts, can be overwritten if required.) + * + * @param string $type Type of checks (pre or post) + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + public function scriptSpecificChecks(string $type, Checks &$checks, string $category); + + /** + * Delete migration source data. + * It's recommended to use delete source data by uninstalling source extension if possible. + * (Optional in migration scripts, can be overwritten if required.) + * + * @return boolean True if successful, false if an error occurs. + * + * @since 4.0.0 + */ + public function deleteSource(); +} diff --git a/administrator/com_joomgallery/src/Service/Migration/MigrationServiceInterface.php b/administrator/com_joomgallery/src/Service/Migration/MigrationServiceInterface.php new file mode 100644 index 00000000..887f7da0 --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/MigrationServiceInterface.php @@ -0,0 +1,50 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Service\Migration; + +\defined('_JEXEC') or die; + +/** +* The Migration service +* +* @since 4.0.0 +*/ +interface MigrationServiceInterface +{ + /** + * Storage for the migration service class. + * + * @var MigrationInterface + * + * @since 4.0.0 + */ + private $migration; + + /** + * Creates the migration service class + * + * @param string $script Name of the migration script to be used + * + * @return void + * + * @since 4.0.0 + */ + public function createMigration($script): void; + + /** + * Returns the migration service class. + * + * @return MigrationInterface + * + * @since 4.0.0 + */ + public function getMigration(): MigrationInterface; +} diff --git a/administrator/com_joomgallery/src/Service/Migration/MigrationServiceTrait.php b/administrator/com_joomgallery/src/Service/Migration/MigrationServiceTrait.php new file mode 100644 index 00000000..a9d75fc4 --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/MigrationServiceTrait.php @@ -0,0 +1,101 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Service\Migration; + +\defined('_JEXEC') or die; + +use \Joomla\CMS\Uri\Uri; +use \Joomla\CMS\Language\Text; +use \Joomla\CMS\Filesystem\Folder; +use \Joomla\CMS\Filesystem\Path; + +/** +* Trait to implement MigrationServiceInterface +* +* @since 4.0.0 +*/ +trait MigrationServiceTrait +{ + /** + * Storage for the migration service class. + * + * @var MigrationInterface + * + * @since 4.0.0 + */ + private $migration = null; + + /** + * Creates the migration service class + * + * @param string $script Name of the migration script to be used + * + * @return void + * + * @since 4.0.0 + * @throws Exception + */ + public function createMigration($script) + { + // Get list of scripts + $scripts = $this->getScripts(); + + // Check if selected script exists + if(!\in_array($script, \array_keys($scripts))) + { + // Requested script does not exists + throw new \Exception(Text::_('COM_JOOMGALLERY_MIGRATION_SCRIPT_NOT_EXIST'), 1); + } + + // Create migration service based on provided migration script name + require_once $scripts[$script]['path']; + + $namespace = '\\Joomgallery\\Component\\Joomgallery\\Administrator\\Service\\Migration\\Scripts'; + $fully_qualified_class_name = $namespace.'\\'.$script; + $this->migration = new $fully_qualified_class_name; + + return; + } + + /** + * Returns the migration service class. + * + * @return MigrationInterface + * + * @since 4.0.0 + */ + public function getMigration() + { + return $this->migration; + } + + /** + * Method to get all available migration scripts. + * + * @return array|boolean List of paths of all available scripts. + * + * @since 4.0.0 + */ + protected function getScripts() + { + $files = Folder::files(JPATH_ADMINISTRATOR.'/components/'._JOOM_OPTION.'/src/Service/Migration/Scripts', '.php$', false, true); + + $scripts = array(); + foreach($files as $path) + { + $img = Uri::base().'components/'._JOOM_OPTION.'/src/Service/Migration/Scripts/'.basename($path, '.php').'.jpg'; + + $scripts[basename($path, '.php')] = array('path' => Path::clean($path), 'img' => $img); + } + + return $scripts; + } +} diff --git a/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.jpg b/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.jpg new file mode 100644 index 00000000..86cd9178 Binary files /dev/null and b/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.jpg differ diff --git a/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.php b/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.php new file mode 100644 index 00000000..241a7a95 --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.php @@ -0,0 +1,1009 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Service\Migration\Scripts; + +// No direct access +\defined('_JEXEC') or die; + +use \Joomla\CMS\Factory; +use \Joomla\CMS\Language\Text; +use \Joomla\CMS\Filesystem\Path; +use \Joomla\CMS\Filesystem\File; +use \Joomla\CMS\User\UserFactoryInterface; +use \Joomla\Component\Media\Administrator\Exception\FileExistsException; +use \Joomgallery\Component\Joomgallery\Administrator\Table\ImageTable; +use \Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper; +use \Joomgallery\Component\Joomgallery\Administrator\Table\CategoryTable; +use \Joomgallery\Component\Joomgallery\Administrator\Service\Migration\Checks; +use \Joomgallery\Component\Joomgallery\Administrator\Service\Migration\Migration; +use \Joomgallery\Component\Joomgallery\Administrator\Service\Migration\Targetinfo; +use \Joomgallery\Component\Joomgallery\Administrator\Service\Migration\MigrationInterface; + +/** + * Migration script class + * JoomGallery 3.x to JoomGallery 4.x + * + * @package JoomGallery + * @since 4.0.0 + */ +class Jg3ToJg4 extends Migration implements MigrationInterface +{ + /** + * Name of the migration script to be used. + * + * @var string + * + * @since 4.0.0 + */ + protected $name = 'Jg3ToJg4'; + + /** + * True to offer the task migration.removesource for this script + * + * @var boolean + * + * @since 4.0.0 + */ + protected $sourceDeletion = true; + + /** + * Constructor + * + * @return void + * + * @since 4.0.0 + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Returns an object with compatibility info for this migration script. + * + * @param string $type Select if you get source or destination info + * + * @return Targetinfo Compatibility info object + * + * @since 4.0.0 + */ + public function getTargetinfo(string $type = 'source'): Targetinfo + { + $info = new Targetinfo(); + + $info->set('target', $type); + $info->set('type','component'); + + if($type === 'source') + { + $info->set('extension','JoomGallery'); + $info->set('min', '3.6.0'); + $info->set('max', '3.6.99'); + $info->set('php_min', '5.6.0'); + } + elseif($type === 'destination') + { + $info->set('extension','com_joomgallery'); + $info->set('min', '4.0.0'); + $info->set('max', '5.99.99'); + $info->set('php_min', '7.4.0'); + } + else + { + throw new \Exception('Type must be eighter "source" or "destination", but "'.$type.'" given.', 1); + } + + return $info; + } + + /** + * Returns the XML object of the source extension + * + * @return \SimpleXMLElement|string Extension XML object or XML path on failure + * + * @since 4.0.0 + */ + public function getSourceXML() + { + if($this->params->get('same_joomla')) + { + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/com_joomgallery/joomgallery_old.xml'); + $xml = \simplexml_load_file($path); + + if($xml){return $xml;}else{return $path;} + } + else + { + $joomla_root = $this->params->get('joomla_path'); + + // Remove directory separator at the end + if(\substr($joomla_root, -1) == '/' || \substr($joomla_root, -1) == \DIRECTORY_SEPARATOR) + { + $joomla_root = \substr($joomla_root, 0, -1); + } + + if(\file_exists(Path::clean($joomla_root . '/administrator/components/com_joomgallery/joomgallery.xml'))) + { + $path = Path::clean($joomla_root . '/administrator/components/com_joomgallery/joomgallery.xml'); + $xml = \simplexml_load_file($path); + + if($xml){return $xml;}else{return $path;} + } + else + { + $path = Path::clean($joomla_root . '/administrator/components/com_joomgallery/joomgallery_old.xml'); + $xml = \simplexml_load_file($path); + + if($xml){return $xml;}else{return $path;} + } + } + } + + /** + * Returns a list of involved source directories. + * + * @return array List of paths + * + * @since 4.0.0 + */ + public function getSourceDirs(): array + { + $dirs = array( $this->params->get('orig_path'), + $this->params->get('detail_path'), + $this->params->get('thumb_path') + ); + + return $dirs; + } + + /** + * A list of content type definitions depending on migration source + * (Required in migration scripts. The order of the content types must correspond to its migration order) + * + * ------ + * This method is multiple times, when the migration types are loaded. The first time it is called without + * the $type param, just to retrieve the array of source types info. The next times it is called with a + * $type param to load the optional type infos like ownerFieldname. + * + * Needed: tablename, primarykey, isNested, isCategorized + * Optional: ownerFieldname, dependent_on, pkstoskip, insertRecord, queueTablename, recordName + * + * Assumption for insertrecord: + * If insertrecord == true assumes, that type is a migration; Means reading data from source db and write it to destination db (default) + * If insertrecord == false assumes, that type is an adjustment; Means reading data from destination db adjust it and write it back to destination db + * + * Attention: + * Order of the content types must correspond to the migration order + * Pay attention to the dependent_on when ordering here !!! + * + * @param bool $names_only True to load type names only. No migration parameters required. + * @param Type $type Type object to set optional definitions + * + * @return array The source types info, array(tablename, primarykey, isNested, isCategorized) + * + * @since 4.0.0 + */ + public function defineTypes($names_only=false, &$type=null): array + { + $types = array( 'category' => array('#__joomgallery_catg', 'cid', true, false, true), + 'image' => array('#__joomgallery', 'id', false, true, true), + 'catimage' => array(_JOOM_TABLE_CATEGORIES, 'cid', false, false, false) + ); + + if($this->params->get('source_ids', 0) == 1) + { + // Special case: When using ids from source, category images don't have to be adjusted. + unset($types['catimage']); + } + + if($names_only) + { + return \array_keys($types); + } + //------- First point of return: Return names only + + // add suffix, if source tables are in the same db with *_old at the end + $source_db_suffix = ''; + if($this->params->get('same_db')) + { + $source_db_suffix = '_old'; + } + + foreach($types as $key => $value) + { + if(\count($value) < 5 || (\count($value) > 4 && $value[4])) + { + // tablename is from source db and has to be checked + $types[$key][0] = $value[0] . $source_db_suffix; + } + } + + // Return here if type is not given + if(\is_null($type)) + { + return $types; + } + //------- Second point of return: Don't load optional type infos + + // Load the optional type infos: + // ownerFieldname, dependent_on, pkstoskip, insertRecord, queueTablename, recordName + switch($type->name) + { + case 'category': + $type->set('pkstoskip', array(1)); + break; + + case 'image': + $type->set('dependent_on', array('category')); + break; + + case 'catimage': + $type->set('dependent_on', array('category', 'image')); + $type->set('pkstoskip', array(1)); + $type->set('insertRecord', false); + $type->set('queueTablename', '#__joomgallery_catg' . $source_db_suffix); + $type->set('recordName', 'category'); + break; + + default: + // No optional type infos needed + break; + } + + return $types; + } + + /** + * Converts data from source into the structure needed for JoomGallery. + * (Optional in migration scripts, but highly recommended.) + * + * ------ + * How mappings work: + * - Key not in the mapping array: Nothing changes. Field value can be magrated as it is. + * - 'old key' => 'new key': Field name has changed. Old values will be inserted in field with the provided new key. + * - 'old key' => false: Field does not exist anymore or value has to be emptied to create new record in the new table. + * - 'old key' => array(string, string, bool): Field will be merget into another field of type json. + * 1. ('destination field name'): Name of the field to be merged into. + * 2. ('new field name'): New name of the field created in the destination field. (default: false / retain field name) + * 3. ('create child'): True, if a child node shall be created in the destination field containing the field values. (default: false / no child) + * + * + * @param string $type Name of the content type + * @param array $data Source data received from getData() + * + * @return array Converted data to save into JoomGallery + * + * @since 4.0.0 + */ + public function convertData(string $type, array $data): array + { + // Parameter dependet mapping fields + $id = \boolval($this->params->get('source_ids', 0)) ? 'id' : false; + $owner = \boolval($this->params->get('check_owner', 1)) ? $this->types[$type]->get('ownerFieldname') : false; + + // Configure mapping for each content type + switch($type) + { + case 'category': + // Apply mapping for category table + $mapping = array( 'cid' => $id, 'asset_id' => false, 'name' => 'title', 'alias' => false, 'lft' => false, 'rgt' => false, 'level' => false, + 'owner' => $owner, 'img_position' => false, 'catpath' => 'static_path', 'params' => array('params', false, false), + 'allow_download' => array('params', 'jg_download', false), 'allow_comment' => array('params', 'jg_showcomment', false), + 'allow_rating' => array('params', 'jg_showrating', false), 'allow_watermark' => array('params', 'jg_dynamic_watermark', false), + 'allow_watermark_download' => array('params', 'jg_downloadwithwatermark', false) + ); + + // Adjust parent_id based on already created categories + if(!\boolval($this->params->get('source_ids', 0)) && $data['parent_id'] > 0) + { + $data['parent_id'] = $this->migrateables['category']->successful->get($data['parent_id']); + } + + break; + + case 'image': + // Apply mapping for image table + $mapping = array( 'id' => $id, 'asset_id' => false, 'alias' => false, 'imgtitle' => 'title', 'imgtext' => 'description', 'imgauthor' => 'author', + 'imgdate' => 'date', 'imgfilename' => 'filename', 'imgvotes' => 'votes', 'imgvotesum' => 'votesum', 'imgthumbname' => false, + 'owner' => $owner, 'params' => array('params', false, false) + ); + + // Check difference between imgfilename and imgthumbname + if($data['imgfilename'] !== $data['imgthumbname']) + { + $this->component->setError(Text::sprintf('COM_JOOMGALLERY_SERVICE_MIGRATION_FILENAME_DIFF', $data['id'], $data['alias'])); + + return false; + } + + // Adjust catid with new created categories + if(!\boolval($this->params->get('source_ids', 0))) + { + $data['catid'] = $this->migrateables['category']->successful->get($data['catid']); + } + + break; + + case 'catimage': + // Dont change the record data + $mapping = array(); + + // Adjust category thumbnail + if(!empty($data['thumbnail'])) + { + if($this->migrateables['image']->successful->get($data['thumbnail'], false)) + { + // Change category thumbnail id based on migrated image id + $data['thumbnail'] = $this->migrateables['image']->successful->get($data['thumbnail']); + } + else + { + // Migrated image id not available, set id to 0 + $data['thumbnail'] = 0; + } + } + + break; + + default: + // The table structure is the same + $mapping = array('id' => $id, 'owner' => $owner); + + break; + } + + // Strip zero values for owners (owner=0) + if(isset($data[$this->types[$type]->get('ownerFieldname')]) && !$data[$this->types[$type]->get('ownerFieldname')]) + { + // Owner is currently set to zero or empty. Set it to be null + $data[$this->types[$type]->get('ownerFieldname')] = null; + } + + // Apply mapping + return $this->applyConvertData($data, $mapping); + } + + /** + * - Load a queue of ids from a specific migrateable object + * - Reload/Reorder the queue if migrateable object already has queue + * + * @param string $type Content type + * @param object $migrateable Mibrateable object + * + * @return array + * + * @since 4.0.0 + */ + public function getQueue(string $type, object $migrateable=null): array + { + if(\is_null($migrateable)) + { + if(!$migrateable = $this->getMigrateable($type)) + { + return array(); + } + } + + $this->loadTypes(); + + // Queue gets always loaded from source db + $tablename = $this->types[$type]->get('queueTablename'); + $primarykey = $this->types[$type]->get('pk'); + + // Get db object + list($db, $prefix) = $this->getDB('source'); + + // Initialize query object + $query = $db->getQuery(true); + + // Create the query + $query->select($db->quoteName($primarykey)) + ->from($db->quoteName($tablename)); + + // Apply additional where clauses for specific content types + if($type == 'catimage') + { + $query->where($db->quoteName($primarykey) . ' > 1'); + $query->where($db->quoteName('thumbnail') . ' > 0'); + } + + // Apply id filter + // Reorder the queue if queue is not empty + if(\property_exists($migrateable, 'queue') && !empty($migrateable->queue)) + { + $queue = (array) $migrateable->get('queue', array()); + $query->where($db->quoteName($primarykey) . ' IN (' . implode(',', $queue) .')'); + } + + // Gather migration types info + if(empty($this->get('types'))) + { + $this->getSourceTableInfo($type); + } + + // Apply ordering based on level if it is a nested type + if($this->get('types')[$type]->get('nested')) + { + //$query->order($db->quoteName('level') . ' ASC'); + $query->order($db->quoteName('lft') . ' ASC'); + } + else + { + $query->order($db->quoteName($primarykey) . ' ASC'); + } + + $db->setQuery($query); + + // Attempt to load the queue + $queue = array(); + try + { + $queue = $db->loadColumn(); + } + catch(\Exception $e) + { + $this->component->setError($e->getMessage()); + } + + // Postprocessing the queue + $needs_postprocessing = array('catimage'); + if(!empty($queue) && \in_array($type, $needs_postprocessing)) + { + if($type == 'catimage' && !\boolval($this->params->get('source_ids', 0))) + { + $mig_cat = $this->getMigrateable('category', false); + + if($mig_cat && $mig_cat->id > 0) + { + // Adjust catid with new created/migrates categories + foreach($queue as $key => $old_id) + { + $queue[$key] = $mig_cat->successful->get($old_id); + } + } + } + } + + return $queue; + } + + /** + * Performs the neccessary steps to migrate an image in the filesystem + * + * @param ImageTable $img ImageTable object, already stored + * @param array $data Source data received from getData() + * + * @return bool True on success, false otherwise + * + * @since 4.0.0 + */ + public function migrateFiles(ImageTable $img, array $data): bool + { + $img_source = $this->getImageSource($data); + + // Set old catid for image creation + $img->catid = $data['catid']; + + if($this->params->get('image_usage', 0) == 1) + { + // Recreate imagetypes based on given image + $res = $this->createImages($img, $img_source[0]); + } + elseif($this->params->get('image_usage', 0) == 2 || $this->params->get('image_usage', 0) == 3) + { + $copy = false; + if($this->params->get('image_usage', 0) == 2) + { + $copy = true; + } + + // Copy/Move images from source based on mapping + $res = $this->reuseImages($img, $img_source, $copy); + } + else + { + // Direct usage of images + // Nothing to do + $res = true; + } + + return $res; + } + + /** + * Performs the neccessary steps to migrate a category in the filesystem + * + * @param CategoryTable $cat CategoryTable object, already stored + * @param array $data Source data received from getData() + * + * @return bool True on success, false otherwise + * + * @since 4.0.0 + */ + public function migrateFolder(CategoryTable $cat, array $data): bool + { + // Create file manager service + $this->component->createFileManager(); + + if($this->params->get('image_usage', 0) == 0) + { + // Direct usage + if($this->params->get('same_joomla', 1) == 0) + { + // Direct usage from other source is impossible + $this->component->setError('FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_ERROR'); + + return false; + } + + // Rename folders if needed + if($this->params->get('new_dirs', 1) == 1) + { + // Create new/JG4 folder structure + // Get new foldername out of path + $newName = \basename($cat->path); + + // Get old foldername out of static_path + $oldName = \basename($cat->static_path); + + // Create dummy object for category Renaming + $tmp_cat = new \stdClass(); + $tmp_cat->path = \substr($cat->path, 0, strrpos($cat->path, \basename($cat->path))) . $oldName; + + // Rename existing folders + return $this->component->getFileManager()->renameCategory($tmp_cat, $newName); + } + } + else + { + // Recreate, copy or move + if($this->params->get('new_dirs', 1) == 1) + { + // Create new/JG4 folder structure (based on alias) + return $this->component->getFileManager()->createCategory($cat->alias, $cat->parent_id); + } + else + { + // Create old/JG3 folder structure (based on static_path) + if( $this->params->get('same_joomla', 1) == 1 && + $this->component->getConfig()->get('jg_filesystem', 'local-images') == 'local-images' + ) + { + // Recreate, copy or move within the same filesystem by keeping the old folder structure is impossible + $this->component->setError('FILES_JOOMGALLERY_SERVICE_MIGRATION_MCR_ERROR'); + + return false; + } + + return $this->component->getFileManager()->createCategory(\basename($cat->static_path), $cat->parent_id); + } + } + + return true; + } + + /** + * Fetches an array of images from source to be used for creating the imagetypes + * for the current image. + * + * @param array $data Source record data received from getData() - before convertData() + * + * @return array List of images from sources used to create the new imagetypes + * 1. If imagetypes get recreated: array('image/source/path') + * 2. If imagetypes get copied/moved: array('original' => 'image/source/path1', 'detail' => 'image/source/path2', ...) + * + * @since 4.0.0 + */ + public function getImageSource(array $data): array + { + $directories = $this->getSourceDirs(); + $cat = $this->getData('category', $data['catid']); + + switch($this->params->get('image_usage', 0)) + { + // Recreate images + case 1: + if(!empty($directories[0])) + { + // use original image if not empty + $dir = $directories[0]; + } + else + { + // use detail image + $dir = $directories[1]; + } + + // Assemble path to source image with complete system root + return array(Path::clean($this->getSourceRootPath() . '/' . $dir . '/' . $cat['catpath'] . '/' . $data['imgfilename'])); + break; + + // Copy/Move images + case 2: + case 3: + $imagetypes = JoomHelper::getRecords('imagetypes', $this->component); + $dirs_map = array('original' => 0, 'detail' => 1, 'thumbnail' => 2); + + $paths = array(); + foreach($imagetypes as $key => $type) + { + // Choose source type based on params + $source_type = 'detail'; + foreach($this->params->get('image_mapping') as $key => $map) + { + if($map['destination'] == $type->typename) + { + $source_type = $map['source']; + break; + } + } + + // Assemble path to source image + $paths[$type->typename] = Path::clean($this->getSourceRootPath() . '/' . $directories[$dirs_map[$source_type]]. '/' . $cat['catpath'] . '/' . $data['imgfilename']); + } + + return $paths; + break; + + // Direct usage + default: + return array(); + break; + } + } + + /** + * Creation of imagetypes based on one source file. + * Source file has to be given with a full system path. + * + * @param ImageTable $img ImageTable object, already stored + * @param string $source Source file with which the imagetypes shall be created + * + * @return bool True on success, false otherwise + * + * @since 4.0.0 + */ + protected function createImages(ImageTable $img, string $source): bool + { + // Create file manager service + $this->component->createFileManager(); + + // Update catid based on migrated categories + $migrated_cats = $this->get('migrateables')['category']->successful; + $migrated_catid = $migrated_cats->get($img->catid); + + // Create imagetypes + return $this->component->getFileManager()->createImages($source, $img->filename, $migrated_catid); + } + + /** + * Creation of imagetypes based on images already available on the server. + * Source files has to be given for each imagetype with a full system path. + * + * @param ImageTable $img ImageTable object, already stored + * @param array $sources List of source images for each imagetype availabe in JG4 + * @param bool $copy True: copy, False: move + * + * @return bool True on success, false otherwise + * + * @since 4.0.0 + * @throws \Exception + */ + protected function reuseImages(ImageTable $img, array $sources, bool $copy = false): bool + { + // Create services + $this->component->createFileManager(); + $this->component->createFilesystem($this->component->getConfig()->get('jg_filesystem','local-images')); + + // Fetch available imagetypes from destination + $imagetypes = JoomHelper::getRecords('imagetypes', $this->component, 'typename'); + + // Check the source mapping + if(\count(\array_diff_key($imagetypes, $sources)) !== 0 || \count(\array_diff_key($sources, $imagetypes)) !== 0) + { + throw new \Exception('Imagetype mapping from migration script does not match component configuration!', 1); + } + + // Update catid based on migrated categories + $migrated_cats = $this->get('migrateables')['category']->successful; + $migrated_catid = $migrated_cats->get($img->catid); + + // Loop through all sources + $error = false; + foreach($imagetypes as $type => $tmp) + { + // Get image source path (with system root) + $img_src = $sources[$type]; + + // Get category destination path + $cat_dst = $this->component->getFileManager()->getCatPath($migrated_catid, $type); + + // Create image destination path + $img_dst = $cat_dst . '/' . $img->filename; + + // Create destination folder if not existent + $folder_dst = \dirname($img_dst); + try + { + $this->component->getFilesystem()->createFolder(\basename($folder_dst), \dirname($folder_dst)); + } + catch(FileExistsException $e) + { + // Do nothing + } + catch(\Exception $e) + { + // Debug info + $this->component->addDebug(Text::sprintf('COM_JOOMGALLERY_SERVICE_ERROR_CREATE_CATEGORY', \ucfirst($folder_dst))); + $error = true; + + continue; + } + + // Move / Copy image + try + { + if($this->component->getFilesystem()->get('filesystem') == 'local-images') + { + // Sorce and destination on the local filesystem + if($img_src == Path::clean(JPATH_ROOT . '/' . $img_dst)) + { + // Sorce and destination are identical. Do nothing. + continue; + } + + if($copy) + { + if(!File::copy($img_src, Path::clean(JPATH_ROOT . '/' . $img_dst))) + { + $this->component->addDebug(Text::sprintf('COM_JOOMGALLERY_SERVICE_ERROR_COPY_IMAGETYPE', \basename($img_src), $type)); + $error = true; + continue; + } + } + else + { + if(!File::move($img_src, Path::clean(JPATH_ROOT . '/' . $img_dst))) + { + $this->component->addDebug(Text::sprintf('COM_JOOMGALLERY_SERVICE_ERROR_MOVE_IMAGETYPE', \basename($img_src), $type)); + $error = true; + continue; + } + } + } + else + { + // Destination not on the local filesystem. Upload required + $this->component->getFilesystem()->createFile($img->filename, $cat_dst, \file_get_contents($img_src)); + + if(!$copy) + { + // When image shall be moved, source have to be deleted + File::delete($img_src); + } + } + } + catch(\Exception $e) + { + // Operation failed + if($copy) + { + $this->component->addDebug(Text::sprintf('COM_JOOMGALLERY_SERVICE_ERROR_COPY_IMAGETYPE', \basename($img_src), $type)); + } + else + { + $this->component->addDebug(Text::sprintf('COM_JOOMGALLERY_SERVICE_ERROR_MOVE_IMAGETYPE', \basename($img_src), $type)); + } + + $error = true; + + continue; + } + } + + if($error) + { + return false; + } + else + { + return true; + } + } + + /** + * Perform script specific checks at the end of pre and postcheck. + * + * @param string $type Type of checks (pre or post) + * @param Checks $checks The checks object + * @param string $category The checks-category into which to add the new check + * + * @return void + * + * @since 4.0.0 + */ + public function scriptSpecificChecks(string $type, Checks &$checks, string $category) + { + $this->component->createConfig(); + + if($type == 'pre') + { + // Get source db info + list($db, $dbPrefix) = $this->getDB('source'); + list($tablename, $pkname) = $this->getSourceTableInfo('image'); + list($cattablename, $catpkname) = $this->getSourceTableInfo('category'); + + //------------------------ + + // Check if imgfilename and imgthumbname are the same + $query = $db->getQuery(true) + ->select($db->quoteName(array('id'))) + ->from($db->quoteName($tablename)) + ->where($db->quoteName('imgfilename') . ' != ' . $db->quoteName('imgthumbname')); + $db->setQuery($query); + + // Load a list of ids that have different values for imgfilename and imgthumbname + $res = $db->loadColumn(); + + if(!empty(\count($res))) + { + $checks->addCheck($category, 'src_table_image_filename', true, true, Text::_('FILES_JOOMGALLERY_MIGRATION_CHECK_IMAGE_FILENAMES_TITLE'), Text::sprintf('FILES_JOOMGALLERY_MIGRATION_CHECK_IMAGE_FILENAMES_DESC', \count($res)), Text::sprintf('FILES_JOOMGALLERY_MIGRATION_CHECK_IMAGE_FILENAMES_HELP', \implode(', ', $res))); + } + + //------------------------ + + if($this->params->get('new_dirs', 1) == 1) + { + // We want to use the new folder structure style + // Check catpath of JG3 category table if they are consistent and convertable + $query = $db->getQuery(true) + ->select($db->quoteName(array('cid', 'alias', 'parent_id', 'catpath'))) + ->from($db->quoteName($cattablename)) + ->where($db->quoteName('level') . ' > 0 '); + $db->setQuery($query); + + // Load a list of category objects + $cats = $db->loadObjectList(); + + // Check them for inconsistency + $inconsistent = array(); + foreach($cats as $key => $cat) + { + if(!$this->checkCatpath($cat)) + { + \array_push($inconsistent, $cat->cid); + } + } + + if(\count($inconsistent) > 0) + { + $checks->addCheck($category, 'src_table_cat_path', false, false, Text::_('FILES_JOOMGALLERY_MIGRATION_CHECK_CATEGORY_CATPATH'), Text::_('FILES_JOOMGALLERY_MIGRATION_CHECK_CATEGORY_CATPATH_DESC'), Text::sprintf('FILES_JOOMGALLERY_MIGRATION_CHECK_CATEGORY_CATPATH_HELP', \implode(', ', $inconsistent))); + } + + // Check if compatibility mode is deactivated + if($this->component->getConfig()->get('jg_compatibility_mode', 0) == 1) + { + $checks->addCheck($category, 'compatibility_mode', false, false, Text::_('FILES_JOOMGALLERY_MIGRATION_CHECK_COMPATIBILITY_MODE'), Text::_('FILES_JOOMGALLERY_MIGRATION_CHECK_COMPATIBILITY_MODE_OFF_DESC')); + } + } + else + { + // We want to use the old folder structure style + // Check if compatibility mode is activated + if($this->component->getConfig()->get('jg_compatibility_mode', 0) == 0) + { + $checks->addCheck($category, 'compatibility_mode', false, false, Text::_('FILES_JOOMGALLERY_MIGRATION_CHECK_COMPATIBILITY_MODE'), Text::_('FILES_JOOMGALLERY_MIGRATION_CHECK_COMPATIBILITY_MODE_ON_DESC')); + } + } + + //------------------------ + + // Check use case: Direct usage + if($this->params->get('image_usage', 0) == 0) + { + if($this->params->get('same_joomla', 1) == 0) + { + // Direct usage is not possible when source is outside this joomla installation + $checks->addCheck($category, 'direct_usage_joomla', false, false, Text::_('COM_JOOMGALLERY_DIRECT_USAGE'), Text::_('FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_ERROR')); + } + else + { + $dest_imagetypes = JoomHelper::getRecords('imagetypes', $this->component); + + if(\count($dest_imagetypes) !== 3) + { + // Direct usage only possible with the three standard imagetypes + $checks->addCheck($category, 'direct_usage_imgtypes', false, false, Text::_('COM_JOOMGALLERY_DIRECT_USAGE'), Text::_('FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_IMGTYPES_ERROR')); + } + else + { + // Make sure that original is deactivated is it was the case in JG3 + $checks->addCheck($category, 'direct_usage_orig', true, true, Text::_('COM_JOOMGALLERY_DIRECT_USAGE'), Text::_('FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_ORIGINAL_WARNING')); + } + + $this->component->createConfig(); + if($this->component->getConfig()->get('jg_filesystem') !== 'local-images') + { + // Direct usage is only possible with local filesystem + $checks->addCheck($category, 'direct_usage_local', false, false, Text::_('COM_JOOMGALLERY_DIRECT_USAGE'), Text::_('FILES_JOOMGALLERY_SERVICE_MIGRATION_DIRECT_USAGE_LOCAL_ERROR')); + } + } + } + + //------------------------ + + // Check use case: Move/Copy/Recreate at same joomla, local filesystem at old folder structure + if($this->params->get('image_usage', 0) > 0) + { + if( $this->params->get('same_joomla', 1) == 1 && + $this->params->get('new_dirs', 1) == 0 && + $this->component->getConfig()->get('jg_filesystem', 'local-images') == 'local-images' + ) + { + // Move/Copy/Recreate is not possible since source and destination folders are identical + $checks->addCheck($category, 'copy_identical_folders', false, false, Text::_('FILES_JOOMGALLERY_SERVICE_MIGRATION_MCR'), Text::_('FILES_JOOMGALLERY_SERVICE_MIGRATION_MCR_ERROR')); + } + } + } + + if($type == 'post') + { + + } + + return; + } + + /** + * Check if catpath is correct due to scheme 'parent-path/alias_cid'. + * + * @param \stdClass $cat Category object + * + * @return bool True if catpath is correct, false otherwise + * + * @since 4.0.0 + */ + protected function checkCatpath(\stdClass $cat): bool + { + // Prepare catpath + $catpath = \basename($cat->catpath); + $parentpath = \substr($cat->catpath, 0, -1 * \strlen('/'.$catpath)); + + // Prepare alias + $alias = \basename($cat->alias); + + // Check for alias_cid + if($catpath !== $alias.'_'.$cat->cid) + { + return false; + } + + // Get path of parent category + list($db, $dbPrefix) = $this->getDB('source'); + list($tablename, $pkname) = $this->getSourceTableInfo('category'); + + $query = $db->getQuery(true) + ->select($db->quoteName('catpath')) + ->from($db->quoteName($tablename)) + ->where($db->quoteName('cid') . ' = '. $db->quote($cat->parent_id)); + $db->setQuery($query); + + $path = $db->loadResult(); + + // Check for parent-path + if($parentpath !== $path) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.xml b/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.xml new file mode 100644 index 00000000..d1e5285f --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/Scripts/Jg3ToJg4.xml @@ -0,0 +1,161 @@ + +
+
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/administrator/com_joomgallery/src/Service/Migration/Targetinfo.php b/administrator/com_joomgallery/src/Service/Migration/Targetinfo.php new file mode 100644 index 00000000..852b2957 --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/Targetinfo.php @@ -0,0 +1,87 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Service\Migration; + +// No direct access +\defined('_JEXEC') or die; + +use \Joomgallery\Component\Joomgallery\Administrator\Extension\ServiceTrait; + +/** + * Targetinfo Class + * Providing compatibility information about source or destination extension + * + * @package JoomGallery + * @since 4.0.0 + */ +class Targetinfo +{ + use ServiceTrait; + + /** + * Does this object provide info about source or destination extension? + * + * @var string + * + * @since 4.0.0 + */ + private $target = 'source'; + + /** + * Extension name + * + * @var string + * + * @since 4.0.0 + */ + private $extension = 'com_joomgallery'; + + /** + * Type of the extension + * + * @var string + * + * @since 4.0.0 + */ + private $type = 'component'; + + /** + * Minimum compatible version + * - Version string must be compatible with \version_compare() + * - If there is no limit, add '-' as version string + * + * @var string + * + * @since 4.0.0 + */ + private $min = '1.0.0'; + + /** + * Maximum compatible version + * - Version string must be compatible with \version_compare() + * - If there is no limit, add '-' as version string + * + * @var string + * + * @since 4.0.0 + */ + private $max = '-'; + + /** + * Minimum compatible PHP version + * - Version string must be compatible with \version_compare() + * + * @var string + * + * @since 4.0.0 + */ + private $php_min = '7.4.0'; +} \ No newline at end of file diff --git a/administrator/com_joomgallery/src/Service/Migration/Type.php b/administrator/com_joomgallery/src/Service/Migration/Type.php new file mode 100644 index 00000000..f463963e --- /dev/null +++ b/administrator/com_joomgallery/src/Service/Migration/Type.php @@ -0,0 +1,226 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Service\Migration; + +// No direct access +\defined('_JEXEC') or die; + +use \Joomgallery\Component\Joomgallery\Administrator\Extension\ServiceTrait; + +/** + * Type Class + * Providing information about a content type beeing migrated + * + * @package JoomGallery + * @since 4.0.0 + */ +class Type +{ + use ServiceTrait; + + /** + * Name of this content type + * + * @var string + * + * @since 4.0.0 + */ + public $name = 'image'; + + /** + * Name of the corresponding record + * + * @var string + * + * @since 4.0.0 + */ + public $recordName = 'image'; + + /** + * Source table name of this content type + * + * @var string + * + * @since 4.0.0 + */ + protected $tablename = '#__joomgallery'; + + /** + * Name of the primary key + * + * @var string + * + * @since 4.0.0 + */ + protected $pk = 'id'; + + /** + * Name of the owner field + * + * @var string + * + * @since 4.0.0 + */ + protected $ownerFieldname = 'created_by'; + + /** + * True if this content type is nested + * + * @var boolean + * + * @since 4.0.0 + */ + protected $nested = false; + + /** + * True if this content type has categories + * + * @var boolean + * + * @since 4.0.0 + */ + protected $categorized = true; + + /** + * List of primary keys that dont need migration and can be skipped. + * + * @var array + * + * @since 4.0.0 + */ + protected $pkstoskip = array(0); + + /** + * List of types this type depends on. + * They have to be migrated before this one. + * + * @var array + * + * @since 4.0.0 + */ + protected $dependent_on = array(); + + /** + * List of types depending on this type. + * This type has to be migrated before the dependent ones. + * + * @var array + * + * @since 4.0.0 + */ + protected $dependent_of = array(); + + /** + * Table name used to load the queue + * + * @var string + * + * @since 4.0.0 + */ + protected $queueTablename = '#__joomgallery'; + + /** + * Do we have to create/inert new database records for this type? + * If no this type is just an adjustment of data within the destination table. + * + * @var boolean + * + * @since 4.0.0 + */ + protected $insertRecord = true; + + /** + * Constructor + * + * @param string $name Name of this content type + * @param array $list Source types info created by Migration::defineTypes() + * + * @return void + * + * @since 4.0.0 + */ + public function __construct($name, $list) + { + $this->name = $name; + $this->recordName = $name; + + if(\count($list) < 4) + { + throw new \Exception('Type object needs a list of at least 4 entries as the second argument.', 1); + } + + $this->tablename = $list[0]; + $this->queueTablename = $list[0]; + $this->pk = $list[1]; + $this->nested = $list[2]; + $this->categorized = $list[3]; + } + + /** + * Pushes types to the dependent_of based on the provided list of Type objects. + * + * @param Type[] $types List of Type objects + * + * @return void + * + * @since 4.0.0 + */ + public function setDependentOf($types) + { + // search for types depending on this type + if(!empty($types)) + { + foreach($types as $type_name => $type) + { + if($type && \count($type->get('dependent_on')) > 0 && \in_array($this->name, $type->get('dependent_on'))) + { + \array_push($this->dependent_of, $type_name); + } + } + } + } + + /** + * Modifies a property of the object, creating it if it does not already exist. + * + * @param string $property The name of the property. + * @param mixed $value The value of the property to set. + * + * @return mixed Previous value of the property. + * + * @since 4.0.0 + */ + public function set($property, $value = null) + { + switch($property) + { + case 'pkstoskip': + $previous = $this->$property ?? null; + + if(\is_array($value)) + { + $this->$property = \array_merge($this->$property, $value); + } + else + { + \array_push($this->$property, $value); + } + break; + + default: + $previous = $this->$property ?? null; + $this->$property = $value; + break; + } + + return $previous; + } +} \ No newline at end of file diff --git a/administrator/com_joomgallery/src/Table/CategoryTable.php b/administrator/com_joomgallery/src/Table/CategoryTable.php index 912ecce1..a6693bff 100644 --- a/administrator/com_joomgallery/src/Table/CategoryTable.php +++ b/administrator/com_joomgallery/src/Table/CategoryTable.php @@ -34,6 +34,7 @@ class CategoryTable extends Table implements VersionableTableInterface { use JoomTableTrait; + use MigrationTableTrait; /** * Object property to hold the path of the new location reference node. @@ -174,7 +175,7 @@ public function bind($array, $ignore = '') $array['created_time'] = $date->toSql(); } - if($array['id'] == 0 && empty($array['created_by'])) + if(!\key_exists('created_by', $array) || empty($array['created_by'])) { $array['created_by'] = Factory::getUser()->id; } @@ -225,9 +226,6 @@ public function bind($array, $ignore = '') } } - // Support for multiple field: robots - $this->multipleFieldSupport($array, 'robots'); - if(isset($array['params']) && is_array($array['params'])) { $registry = new Registry; @@ -284,6 +282,13 @@ public function store($updateNulls = true) { $this->setPathWithLocation(); + // Support for params field + if(isset($this->params) && !is_string($this->params)) + { + $registry = new Registry($this->params); + $this->params = (string) $registry; + } + return parent::store($updateNulls); } @@ -348,13 +353,37 @@ public function check() // Create new path based on alias and parent category $manager = JoomHelper::getService('FileManager'); - $this->path = $manager->getCatPath(0, false, $this->parent_id, $this->alias); + $filesystem = JoomHelper::getService('Filesystem'); + $this->path = $manager->getCatPath($this->id, false, $this->parent_id, $this->alias, false, false); + $this->path = $filesystem->cleanPath($this->path, '/'); - // Support for subform field params - if(is_array($this->params)) - { - $this->params = json_encode($this->params, JSON_UNESCAPED_UNICODE); - } + // Support for subform field params + if(empty($this->params)) + { + $this->params = $this->loadDefaultField('params'); + } + if(isset($this->params)) + { + $this->params = new Registry($this->params); + } + + // Support for field description + if(empty($this->description)) + { + $this->description = $this->loadDefaultField('description'); + } + + // Support for field metadesc + if(empty($this->metadesc)) + { + $this->metadesc = $this->loadDefaultField('metadesc'); + } + + // Support for field metakey + if(empty($this->metakey)) + { + $this->metakey = $this->loadDefaultField('metakey'); + } return parent::check(); } diff --git a/administrator/com_joomgallery/src/Table/CommentsTable.php b/administrator/com_joomgallery/src/Table/CommentsTable.php index 635a261a..60d4aaf8 100644 --- a/administrator/com_joomgallery/src/Table/CommentsTable.php +++ b/administrator/com_joomgallery/src/Table/CommentsTable.php @@ -70,7 +70,7 @@ public function bind($array, $ignore = '') $array['created_time'] = $date->toSql(); } - if($array['id'] == 0 && empty($array['created_by'])) + if(!\key_exists('created_by', $array) || empty($array['created_by'])) { $array['created_by'] = Factory::getUser()->id; } diff --git a/administrator/com_joomgallery/src/Table/ConfigTable.php b/administrator/com_joomgallery/src/Table/ConfigTable.php index def4ff11..0996077d 100644 --- a/administrator/com_joomgallery/src/Table/ConfigTable.php +++ b/administrator/com_joomgallery/src/Table/ConfigTable.php @@ -71,7 +71,7 @@ public function bind($array, $ignore = '') } } - if($array['id'] == 0 && empty($array['created_by'])) + if(!\key_exists('created_by', $array) || empty($array['created_by'])) { $array['created_by'] = Factory::getUser()->id; } diff --git a/administrator/com_joomgallery/src/Table/FaultiesTable.php b/administrator/com_joomgallery/src/Table/FaultiesTable.php index adbe8571..7986ef77 100644 --- a/administrator/com_joomgallery/src/Table/FaultiesTable.php +++ b/administrator/com_joomgallery/src/Table/FaultiesTable.php @@ -61,4 +61,22 @@ public function bind($array, $ignore = '') return parent::bind($array, $ignore); } + + /** + * Overloaded check function + * + * @return bool + */ + public function check() + { + // Support for subform field paths + if(empty($this->paths)) + { + $this->paths = $this->loadDefaultField('paths'); + } + elseif(is_array($this->paths)) + { + $this->paths = json_encode($this->paths, JSON_UNESCAPED_UNICODE); + } + } } diff --git a/administrator/com_joomgallery/src/Table/FieldsTable.php b/administrator/com_joomgallery/src/Table/FieldsTable.php index 6e3aa727..50c2f65d 100644 --- a/administrator/com_joomgallery/src/Table/FieldsTable.php +++ b/administrator/com_joomgallery/src/Table/FieldsTable.php @@ -66,4 +66,22 @@ public function bind($array, $ignore = '') return parent::bind($array, $ignore); } + + /** + * Overloaded check function + * + * @return bool + */ + public function check() + { + // Support for subform field value + if(empty($this->value)) + { + $this->value = $this->loadDefaultField('value'); + } + elseif(is_array($this->value)) + { + $this->value = json_encode($this->value, JSON_UNESCAPED_UNICODE); + } + } } diff --git a/administrator/com_joomgallery/src/Table/GalleriesTable.php b/administrator/com_joomgallery/src/Table/GalleriesTable.php index b433d717..561b5ca9 100644 --- a/administrator/com_joomgallery/src/Table/GalleriesTable.php +++ b/administrator/com_joomgallery/src/Table/GalleriesTable.php @@ -101,7 +101,7 @@ public function bind($array, $ignore = '') $array['created_time'] = $date->toSql(); } - if($array['id'] == 0 && (!\key_exists('created_by', $array) || empty($array['created_by']))) + if(!\key_exists('created_by', $array) || empty($array['created_by'])) { $array['created_by'] = Factory::getUser()->id; } diff --git a/administrator/com_joomgallery/src/Table/ImageTable.php b/administrator/com_joomgallery/src/Table/ImageTable.php index 02cb316c..3b5e59b3 100644 --- a/administrator/com_joomgallery/src/Table/ImageTable.php +++ b/administrator/com_joomgallery/src/Table/ImageTable.php @@ -33,6 +33,7 @@ class ImageTable extends Table implements VersionableTableInterface { use JoomTableTrait; + use MigrationTableTrait; /** * Constructor @@ -202,7 +203,7 @@ public function bind($array, $ignore = '') $array['created_time'] = $date->toSql(); } - if($array['id'] == 0 && (!\key_exists('created_by', $array) || empty($array['created_by']))) + if(!\key_exists('created_by', $array) || empty($array['created_by'])) { $array['created_by'] = Factory::getUser()->id; } @@ -227,9 +228,6 @@ public function bind($array, $ignore = '') $array['modified_by'] = Factory::getUser()->id; } - // Support for multiple field: robots - $this->multipleFieldSupport($array, 'robots'); - // Support for empty date field: date if(!\key_exists('date', $array) || $array['date'] == '0000-00-00' || empty($array['date'])) { @@ -295,6 +293,13 @@ public function bind($array, $ignore = '') */ public function store($updateNulls = true) { + // Support for params field + if(isset($this->params) && !is_string($this->params)) + { + $registry = new Registry($this->params); + $this->params = (string) $registry; + } + $success = parent::store($updateNulls); if($success) @@ -365,10 +370,16 @@ public function check() { $this->params = $this->loadDefaultField('params'); } - elseif(\is_array($this->params)) - { - $this->params = json_encode($this->params, JSON_UNESCAPED_UNICODE); - } + if(isset($this->params)) + { + $this->params = new Registry($this->params); + } + + // Support for field description + if(empty($this->description)) + { + $this->description = $this->loadDefaultField('description'); + } // Support for field metadesc if(empty($this->metadesc)) diff --git a/administrator/com_joomgallery/src/Table/ImagetypeTable.php b/administrator/com_joomgallery/src/Table/ImagetypeTable.php index 19697ecd..c3b011c3 100644 --- a/administrator/com_joomgallery/src/Table/ImagetypeTable.php +++ b/administrator/com_joomgallery/src/Table/ImagetypeTable.php @@ -14,6 +14,7 @@ defined('_JEXEC') or die; use \Joomla\CMS\Table\Table; +use \Joomla\Registry\Registry; use \Joomla\Database\DatabaseDriver; /** diff --git a/administrator/com_joomgallery/src/Table/JoomTableTrait.php b/administrator/com_joomgallery/src/Table/JoomTableTrait.php index 0ef49a13..c87e6910 100644 --- a/administrator/com_joomgallery/src/Table/JoomTableTrait.php +++ b/administrator/com_joomgallery/src/Table/JoomTableTrait.php @@ -94,15 +94,46 @@ public function check() } } + // Support for field description + if(property_exists($this, 'description')) + { + if(empty($this->description)) + { + $this->description = $this->loadDefaultField('description'); + } + } + // Support for subform field params if(property_exists($this, 'params')) { - if(is_array($this->params)) + if(empty($this->params)) + { + $this->params = $this->loadDefaultField('params'); + } + elseif(is_array($this->params)) { $this->params = json_encode($this->params, JSON_UNESCAPED_UNICODE); } } + // Support for field metadesc + if(property_exists($this, 'metadesc')) + { + if(empty($this->metadesc)) + { + $this->metadesc = $this->loadDefaultField('metadesc'); + } + } + + // Support for field metakey + if(property_exists($this, 'metakey')) + { + if(empty($this->metakey)) + { + $this->metakey = $this->loadDefaultField('metakey'); + } + } + return parent::check(); } diff --git a/administrator/com_joomgallery/src/Table/MigrationTable.php b/administrator/com_joomgallery/src/Table/MigrationTable.php new file mode 100644 index 00000000..9d8c3f45 --- /dev/null +++ b/administrator/com_joomgallery/src/Table/MigrationTable.php @@ -0,0 +1,292 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Table; + +// No direct access +defined('_JEXEC') or die; + +use \Joomla\CMS\Factory; +use \Joomla\CMS\Table\Table; +use \Joomla\Registry\Registry; +use \Joomla\Utilities\ArrayHelper; +use \Joomla\Database\DatabaseDriver; + +/** + * Migration table + * + * @package JoomGallery + * @since 4.0.0 + */ +class MigrationTable extends Table +{ + /** + * Migration progress (0-100) + * + * @var int + * + * @since 4.0.0 + */ + public $progress = 0; + + /** + * True if migration of this migrateable is completed + * + * @var bool + * + * @since 4.0.0 + */ + public $completed = false; + + + /** + * Constructor + * + * @param JDatabase &$db A database connector object + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = _JOOM_OPTION.'.migration'; + + parent::__construct(_JOOM_TABLE_MIGRATION, 'id', $db); + + // Initialize queue, successful and failed + $this->queue = array(); + $this->successful = new Registry(); + $this->failed = new Registry(); + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } + + /** + * Method to store a row in the database from the Table instance properties. + * + * If a primary key value is set the row with that primary key value will be updated with the instance property values. + * If no primary key value is set a new row will be inserted into the database with the properties from the Table instance. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + // Support for queue field + if(isset($this->queue) && !\is_string($this->queue)) + { + $this->queue = \json_encode(array_values($this->queue), JSON_UNESCAPED_UNICODE); + } + + // Support for successful field + if(isset($this->successful) && !\is_string($this->successful)) + { + $registry = new Registry($this->successful); + $this->successful = (string) $registry; + } + + // Support for failed field + if(isset($this->failed) && !is_string($this->failed)) + { + $registry = new Registry($this->failed); + $this->failed = (string) $registry; + } + + // Support for params field + if(isset($this->params) && !is_string($this->params)) + { + $registry = new Registry($this->params); + $this->params = (string) $registry; + } + + return parent::store($updateNulls); + } + + /** + * Overloaded bind function to pre-process the params. + * + * @param array $array Named array + * @param mixed $ignore Optional array or list of parameters to ignore + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function bind($array, $ignore = '') + { + $date = Factory::getDate(); + + // Support for queue field + if(isset($array['queue']) && is_array($array['queue'])) + { + $array['queue'] = \json_encode($array['queue'], JSON_UNESCAPED_UNICODE); + } + + // Support for successful field + if(isset($array['successful']) && is_array($array['successful'])) + { + $registry = new Registry; + $registry->loadArray($array['successful']); + $array['successful'] = (string) $registry; + } + + // Support for failed field + if(isset($array['failed']) && is_array($array['failed'])) + { + $registry = new Registry; + $registry->loadArray($array['failed']); + $array['failed'] = (string) $registry; + } + + // Support for params field + if(isset($array['params']) && is_array($array['params'])) + { + $registry = new Registry; + $registry->loadArray($array['params']); + $array['params'] = (string) $registry; + } + + if($array['id'] == 0) + { + $array['created_time'] = $date->toSql(); + } + + return parent::bind($array, array('progress', 'completed')); + } + + /** + * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database. + * + * Child classes should override this method to make sure the data they are storing in the database is safe and as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @since 4.0.0 + */ + public function check() + { + // Support for queue field + if(isset($this->queue)) + { + if(\is_string($this->queue)) + { + $this->queue = \json_decode($this->queue); + } + elseif(\is_object($this->queue)) + { + $this->queue = ArrayHelper::fromObject($this->queue); + } + + $this->queue = ArrayHelper::toInteger($this->queue); + } + + // Support for successful field + if(isset($this->successful)) + { + if(\is_string($this->successful)) + { + $this->successful = \json_decode($this->successful); + } + + if(\is_object($this->successful)) + { + if($this->successful instanceof Registry) + { + $this->successful = $this->successful->toArray(); + } + else + { + $this->successful = ArrayHelper::fromObject($this->successful); + } + } + + // Convert values to integer + $this->successful = ArrayHelper::toInteger($this->successful); + $this->successful = new Registry($this->successful); + } + + // Support for failed field + if(isset($this->failed)) + { + $this->failed = new Registry($this->failed); + } + + // Support for params field + if(isset($this->params)) + { + $this->params = new Registry($this->params); + } + + return parent::check(); + } + + /** + * Method to load a row from the database by primary key and bind the fields to the Table instance properties. + * + * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. + * If not set the instance property value is used. + * @param boolean $reset True to reset the default values before loading the new row. + * + * @return boolean True if successful. False if row not found. + * + * @see Table:bind + * @since 4.0.0 + */ + public function load($keys = null, $reset = true) + { + $success = parent::load($keys, $reset); + + if($success) + { + // Bring table to the correct form + $this->check(); + + // Calculate progress and completed state + $this->clcProgress(); + } + + return $success; + } + + /** + * Method to calculate progress and completed state. + * + * @return void + * + * @since 4.0.0 + */ + public function clcProgress() + { + // Calculate progress property + $total = \count($this->queue) + $this->successful->count() + $this->failed->count(); + $finished = $this->successful->count() + $this->failed->count(); + + if($total > 0) + { + $this->progress = (int) \round((100 / $total) * ($finished)); + } + + // Update completed property + if($total === $finished || $total == 0) + { + $this->completed = true; + } + } +} diff --git a/administrator/com_joomgallery/src/Table/MigrationTableTrait.php b/administrator/com_joomgallery/src/Table/MigrationTableTrait.php new file mode 100644 index 00000000..49fb1b9b --- /dev/null +++ b/administrator/com_joomgallery/src/Table/MigrationTableTrait.php @@ -0,0 +1,60 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\Table; + +\defined('_JEXEC') or die; + +/** +* Add functionality for tables of migrateable records +* +* @since 4.0.0 +*/ +trait MigrationTableTrait +{ + /** + * True to insert the provided value of the primary key + * Needed if you want to create a new record with a given ID + * + * @var bool + */ + protected $_insertID = false; + + /** + * Validate that the primary key has been set. + * + * @return boolean True if the primary key(s) have been set. + * + * @since 3.1.4 + */ + public function hasPrimaryKey() + { + if($this->_insertID) + { + return false; + } + else + { + return parent::hasPrimaryKey(); + } + } + + /** + * Method to set force using the provided ID when storing a new record. + * + * @return void + * + * @since 4.0.0 + */ + public function insertID() + { + $this->_insertID = true; + } +} diff --git a/administrator/com_joomgallery/src/Table/TagTable.php b/administrator/com_joomgallery/src/Table/TagTable.php index 6e2ed093..4e9eb277 100644 --- a/administrator/com_joomgallery/src/Table/TagTable.php +++ b/administrator/com_joomgallery/src/Table/TagTable.php @@ -108,7 +108,7 @@ public function bind($array, $ignore = '') $array['created_time'] = $date->toSql(); } - if($array['id'] == 0 && empty($array['created_by'])) + if(!\key_exists('created_by', $array) || empty($array['created_by'])) { $array['created_by'] = Factory::getUser()->id; } diff --git a/administrator/com_joomgallery/src/Table/UsersTable.php b/administrator/com_joomgallery/src/Table/UsersTable.php index 57490544..a4f68264 100644 --- a/administrator/com_joomgallery/src/Table/UsersTable.php +++ b/administrator/com_joomgallery/src/Table/UsersTable.php @@ -59,6 +59,11 @@ public function bind($array, $ignore = '') $array['created_time'] = $date->toSql(); } + if(!\key_exists('created_by', $array) || empty($array['created_by'])) + { + $array['created_by'] = Factory::getUser()->id; + } + // Support for galleries if(!isset($this->galleries)) { diff --git a/administrator/com_joomgallery/src/View/Migration/HtmlView.php b/administrator/com_joomgallery/src/View/Migration/HtmlView.php new file mode 100644 index 00000000..6982fc20 --- /dev/null +++ b/administrator/com_joomgallery/src/View/Migration/HtmlView.php @@ -0,0 +1,143 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +namespace Joomgallery\Component\Joomgallery\Administrator\View\Migration; + +// No direct access +defined('_JEXEC') or die; + +use \Joomla\CMS\Factory; +use \Joomla\CMS\Language\Text; +use \Joomla\CMS\Toolbar\Toolbar; +use \Joomla\CMS\Toolbar\ToolbarHelper; +use \Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper; +use \Joomgallery\Component\Joomgallery\Administrator\View\JoomGalleryView; + +/** + * View class for a single Tag. + * + * @package JoomGallery + * @since 4.0.0 + */ +class HtmlView extends JoomGalleryView +{ + protected $scripts; + + /** + * Display the view + * + * @param string $tpl Template name + * + * @return void + * + * @throws Exception + */ + public function display($tpl = null) + { + $this->script = $this->get('Script'); + $this->scripts = $this->get('Scripts'); + $this->layout = $this->app->input->get('layout', 'default', 'cmd'); + $this->error = array(); + + // Add page title + ToolbarHelper::title(Text::_('COM_JOOMGALLERY_MIGRATION'), 'migration'); + + if($this->layout != 'default') + { + $this->app->input->set('hidemainmenu', true); + ToolbarHelper::cancel('migration.cancel', 'COM_JOOMGALLERY_MIGRATION_INERRUPT_MIGRATION'); + ToolbarHelper::help('', false, Text::sprintf('COM_JOOMGALLERY_WEBSITE_HELP_URL', 'migration/'. \strtolower($this->script->name) . '?tmpl=component')); + + // Check if requested script exists + if(!\in_array($this->script->name, \array_keys($this->scripts))) + { + // Requested script does not exists + \array_push($this->error, 'COM_JOOMGALLERY_MIGRATION_SCRIPT_NOT_EXIST'); + } + else + { + // Try to load the migration params + $this->params = $this->get('Params'); + + // Check if migration params exist + if(\is_null($this->params) && $this->layout != 'step1') + { + // Requested script does not exists + \array_push($this->error, 'COM_JOOMGALLERY_SERVICE_MIGRATION_STEP_NOT_AVAILABLE'); + } + } + + switch($this->layout) + { + case 'step1': + // Load migration form + $this->form = $this->get('Form'); + break; + + case 'step2': + // Load precheck results + $this->precheck = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$this->script->name.'.step2.results', array()); + $this->success = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$this->script->name.'.step2.success', false); + break; + + case 'step3': + // Data for the migration view + $this->precheck = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$this->script->name.'.step2.success', false); + $this->migrateables = $this->get('Migrateables'); + $this->migration = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$this->script->name.'.step3.results', array()); + $this->dependencies = array(); + foreach ($this->migrateables as $key => $value) + { + $this->dependencies[$key] = $this->component->getMigration()->getType($key)->get('dependent_of'); + } + break; + + case 'step4': + // Load postcheck results + $this->postcheck = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$this->script->name.'.step4.results', array()); + $this->success = $this->app->getUserState(_JOOM_OPTION.'.migration.'.$this->script->name.'.step4.success', false); + $this->sourceDeletion = $this->get('sourceDeletion'); + + $this->openMigrations = $this->get('IdList'); + if(!empty($this->openMigrations) && \key_exists($this->script->name, $this->openMigrations)) + { + $this->openMigrations = $this->openMigrations[$this->script->name]; + } + else + { + $this->openMigrations = array(); + } + break; + + default: + break; + } + } + else + { + // default view + foreach($this->scripts as $script) + { + $this->app->getLanguage()->load('com_joomgallery.migration.'.$script['name'], _JOOM_PATH_ADMIN); + } + + // ID list of open migrations + $this->openMigrations = $this->get('IdList'); + } + + // Check for errors. + if(count($errors = $this->get('Errors'))) + { + throw new \Exception(implode("\n", $errors)); + } + + parent::display($tpl); + } +} diff --git a/administrator/com_joomgallery/tmpl/migration/default.php b/administrator/com_joomgallery/tmpl/migration/default.php new file mode 100644 index 00000000..20ffc2ec --- /dev/null +++ b/administrator/com_joomgallery/tmpl/migration/default.php @@ -0,0 +1,82 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +// No direct access +defined('_JEXEC') or die; + +use \Joomla\CMS\HTML\HTMLHelper; +use \Joomla\CMS\Factory; +use \Joomla\CMS\Router\Route; +use \Joomla\CMS\Language\Text; + +// Import CSS +$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); +$wa->useStyle('com_joomgallery.admin') + ->useScript('com_joomgallery.admin') + ->useScript('com_joomgallery.tasklessForm'); +?> + +
+

:

+
+ + scripts as $name => $script) : ?> + openMigrations) && \key_exists($name, $this->openMigrations)) + { + $openMigrations = true; + $openMigrationsIDs = $this->openMigrations[$name]; + } + ?> +
+
+
+
+

+
+
+ <?php echo $name; ?> logo +
+
+ +
+ +

+ +

+ + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/administrator/com_joomgallery/tmpl/migration/step1.php b/administrator/com_joomgallery/tmpl/migration/step1.php new file mode 100644 index 00000000..01cf9ab4 --- /dev/null +++ b/administrator/com_joomgallery/tmpl/migration/step1.php @@ -0,0 +1,79 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +// No direct access +defined('_JEXEC') or die; + +use \Joomla\CMS\HTML\HTMLHelper; +use \Joomla\CMS\Factory; +use \Joomla\CMS\Router\Route; +use \Joomla\CMS\Language\Text; + +// Import CSS +$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); +$wa->useStyle('com_joomgallery.admin') + ->useScript('com_joomgallery.admin'); +?> + +
+ +
+ +
+ +

:

+
+ + error)): ?> + + + + + + +
+ +
+ error)) : ?> + form->getFieldsets() as $key => $fieldset) : ?> +
+
+
+ label); ?> +
+ form->renderFieldset($fieldset->name);; ?> +
+
+
+
+ + + + + + + + +
+
+
\ No newline at end of file diff --git a/administrator/com_joomgallery/tmpl/migration/step2.php b/administrator/com_joomgallery/tmpl/migration/step2.php new file mode 100644 index 00000000..7c81b943 --- /dev/null +++ b/administrator/com_joomgallery/tmpl/migration/step2.php @@ -0,0 +1,167 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +// No direct access +defined('_JEXEC') or die; + +use \Joomla\CMS\HTML\HTMLHelper; +use \Joomla\CMS\Factory; +use \Joomla\CMS\Router\Route; +use \Joomla\CMS\Language\Text; + +// Import CSS +$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); +$wa->useStyle('com_joomgallery.admin') + ->useScript('com_joomgallery.admin'); +?> + +
+ +
+ +
+ +

:

+
+ + error)): ?> + + + + + + +
+ +
+ + error)) : ?> + + precheck as $cat) : ?> +
+
+ title): ?> +
+

title; ?>

+ desc): ?> + desc; ?> + +
+ +
+ + + + + + + + + + + + + checks as $check) : ?> + result) + { + if($check->warning) + { + // Check successful, but marked as warning + $badgeClass = 'warning'; + $badgeText = Text::_('COM_JOOMGALLERY_WARNING'); + } + else + { + // Check successful + $badgeClass = 'success'; + $badgeText = Text::_('COM_JOOMGALLERY_SUCCESSFUL'); + } + } + else + { + // Check failed + $badgeClass = 'danger'; + $badgeText = Text::_('COM_JOOMGALLERY_FAILED'); + } + ?> + + + + + + + + +
title; ?>
colTitle; ?>
+ title; ?>
+ desc; ?> +
+ +
+
+
+
+
+ + + + + + + + + +
+ + true, + 'title' => 'Test Title', + 'footer' => '', + ); + + echo HTMLHelper::_('bootstrap.renderModal', 'help-modal-box', $options, ''); + ?> +
+ + \ No newline at end of file diff --git a/administrator/com_joomgallery/tmpl/migration/step3.php b/administrator/com_joomgallery/tmpl/migration/step3.php new file mode 100644 index 00000000..a2abff75 --- /dev/null +++ b/administrator/com_joomgallery/tmpl/migration/step3.php @@ -0,0 +1,177 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +// No direct access +defined('_JEXEC') or die; + +use \Joomla\CMS\Factory; +use \Joomla\CMS\Router\Route; +use \Joomla\CMS\Language\Text; +use \Joomla\CMS\HTML\HTMLHelper; +use \Joomla\CMS\Layout\FileLayout; +use Joomla\CMS\Form\FormFactoryInterface; + +// Import CSS +$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); +$wa->useStyle('com_joomgallery.admin') + ->useScript('com_joomgallery.admin') + ->useScript('com_joomgallery.migrator'); + +// Add language strings to JavaScript +Text::script('COM_JOOMGALLERY_ERROR_NETWORK_PROBLEM'); +Text::script('ERROR'); +Text::script('WARNING'); +Text::script('INFO'); +Text::script('SUCCESS'); +?> + +
+ +
+ +
+ +

:

+
+ + error)): ?> + + + + + + +
+ + error) && !empty($this->migrateables)) : ?> + + migrateables as $key => $migrateable) : ?> + get('type'); + $total = count($migrateable->get('queue')) + $migrateable->get('failed')->count() + $migrateable->get('successful')->count(); + ?> +
+
+
+
+

+
+
+ : queue); ?> + : successful); ?> + : failed); ?> +
+ + + + + + + + + +
+
progress > 0){echo $migrateable->progress.'%';}; ?>
+
+ +
+
+
+
+
+
+
+
+
+
+ completed; + + if($migrateable->completed) + { + array_push($completed, $type); + } + + $i++; + ?> + + + + migrateables))) && empty(array_diff_key(array_keys($this->migrateables), $completed))) + { + $total_complete = true; + } + ?> +
+ + /> + + + +
+ + get(FormFactoryInterface::class); + $migrepairForm = $formFactory->createForm('migrepairForm', array()); + $source = _JOOM_PATH_ADMIN . '/forms/migrationrepair.xml'; + + if ($migrepairForm->loadFile($source) == false) + { + throw new \RuntimeException('Form::loadForm could not load file'); + } + + // Migration repair modal box + $options = array('modal-dialog-scrollable' => true, + 'title' => Text::_('COM_JOOMGALLERY_MIGRATION_MANUAL'), + 'footer' => '', + ); + $data = array('script' => $this->script->name, 'form' => $migrepairForm); + $layout = new FileLayout('joomgallery.migrepair', null, array('component' => 'com_joomgallery', 'client' => 1)); + $body = $layout->render($data); + + echo HTMLHelper::_('bootstrap.renderModal', 'repair-modal-box', $options, $body); + ?> + + +
\ No newline at end of file diff --git a/administrator/com_joomgallery/tmpl/migration/step4.php b/administrator/com_joomgallery/tmpl/migration/step4.php new file mode 100644 index 00000000..ec2600b4 --- /dev/null +++ b/administrator/com_joomgallery/tmpl/migration/step4.php @@ -0,0 +1,174 @@ + ** +** @copyright 2008 - 2023 JoomGallery::ProjectTeam ** +** @license GNU General Public License version 3 or later ** +*****************************************************************************************/ + +// No direct access +defined('_JEXEC') or die; + +use \Joomla\CMS\HTML\HTMLHelper; +use \Joomla\CMS\Factory; +use \Joomla\CMS\Router\Route; +use \Joomla\CMS\Language\Text; + +HTMLHelper::addIncludePath(JPATH_COMPONENT . '/src/Helper/'); + +// Import CSS +$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); +$wa->useStyle('com_joomgallery.admin') + ->useScript('com_joomgallery.admin') + ->useScript('form.validate') + ->useScript('com_joomgallery.form-edit'); +?> + +
+
+ +
+ +

:

+
+ + error)): ?> + + + + + + +
+ + + error)) : ?> + + postcheck as $cat) : ?> +
+
+ title): ?> +
+

title; ?>

+ desc): ?> + desc; ?> + +
+ +
+ + + + + + + + + + + + + checks as $check) : ?> + result) + { + if($check->warning) + { + // Check successful, but marked as warning + $badgeClass = 'warning'; + $badgeText = Text::_('COM_JOOMGALLERY_WARNING'); + } + else + { + // Check successful + $badgeClass = 'success'; + $badgeText = Text::_('COM_JOOMGALLERY_SUCCESSFUL'); + } + } + else + { + // Check failed + $badgeClass = 'danger'; + $badgeText = Text::_('COM_JOOMGALLERY_FAILED'); + } + ?> + + + + + + + + +
title; ?>
colTitle; ?>
+ title; ?>
+ desc; ?> +
+ +
+
+
+
+
+ + + +
+ + success) : ?> +
+ + sourceDeletion) : ?> +
+
+
+

+ +
+
+
+ + + +
+
+
+

+ + + + + + + + + + + openMigrations as $openMigration) : ?> + + + +
+ + + + + +
\ No newline at end of file diff --git a/joomgallery.xml b/joomgallery.xml index 8807e778..de5d3cee 100644 --- a/joomgallery.xml +++ b/joomgallery.xml @@ -53,6 +53,7 @@ JCATEGORIES COM_JOOMGALLERY_TAGS COM_JOOMGALLERY_CONFIG_SETS + COM_JOOMGALLERY_MIGRATIONS access.xml diff --git a/media/com_joomgallery/css/admin.css b/media/com_joomgallery/css/admin.css index 135f69f0..849011c4 100644 --- a/media/com_joomgallery/css/admin.css +++ b/media/com_joomgallery/css/admin.css @@ -60,12 +60,23 @@ img.jg-controlpanel-logo { .mr { margin-right: 1rem !important; } +.flex-center { + display: flex; + justify-content: center; +} .modal .modal-body { margin: 1rem 2rem; } .modal .modal-body { margin: 1rem 2rem; } +.card.border.active, +.card.border.success { + border-color: var(--success) !important; +} +.card.border.error { + border-color: var(--danger) !important; +} .controls .choices__inner.is-valid { border-color: #457d54; padding-right: calc(1.5em + 1rem); @@ -82,6 +93,12 @@ img.jg-controlpanel-logo { background-position: right calc(0.375em + 0.25rem) center; background-size: calc(0.75em + 0.5rem) calc(0.75em + 0.5rem); } +.log-area { + font-size: 0.85rem; + min-height: 100px; + max-height: 250px; + overflow-y: scroll; +} .jcc-color-box { float: left; width: 15px; @@ -199,3 +216,24 @@ joomla-field-image .jg_minithumb { .uppy-Dashboard-Item-name { white-space: nowrap; } +.jg-migration .card img { + max-width: 120px; +} +.jg-migration .card img { + max-width: 120px; +} +.jg-migration .navigation { + margin: 0 0 2rem; +} +.log-area p { + margin-bottom: 0.5rem; +} +.log-area .color-success { + color: var(--success) +} +.log-area .color-error { + color: var(--danger) +} +.log-area .color-warning { + color: var(--warning) +} diff --git a/media/com_joomgallery/joomla.asset.json b/media/com_joomgallery/joomla.asset.json old mode 100755 new mode 100644 index d5f5a513..4c112312 --- a/media/com_joomgallery/joomla.asset.json +++ b/media/com_joomgallery/joomla.asset.json @@ -112,7 +112,12 @@ { "name": "com_joomgallery.masonry", "type": "script", - "uri": "com_joomgallery/masonry.min.js", + "uri": "com_joomgallery/masonry.min.js" + }, + { + "name": "com_joomgallery.tasklessForm", + "type": "script", + "uri": "com_joomgallery/tasklessForm.js", "attributes": { "type": "module" } @@ -137,6 +142,11 @@ "name": "com_joomgallery.uppy", "type": "style", "uri": "com_joomgallery/uppy.css" + }, + { + "name": "com_joomgallery.migrator", + "type": "script", + "uri": "com_joomgallery/migrator/dist/migrator.js" } ] } diff --git a/media/com_joomgallery/js/form-edit.js b/media/com_joomgallery/js/form-edit.js index 0781e6fd..16af3244 100644 --- a/media/com_joomgallery/js/form-edit.js +++ b/media/com_joomgallery/js/form-edit.js @@ -16,7 +16,15 @@ const submitTask = task => { const form = document.getElementById(formId); const type = document.getElementById(typeId).value; - if (task === type+'.cancel' || document.formvalidator.isValid(form)) { + const btn = document.querySelector(`[${buttonDataSelector}="${task}"]`); + if (task === type+'.cancel') { + submitForm(task, form); + } + if (btn.parentElement.getAttribute('confirm-message')) { + if(confirm(btn.parentElement.getAttribute('confirm-message')) && document.formvalidator.isValid(form)) { + submitForm(task, form); + } + } else if (document.formvalidator.isValid(form)) { submitForm(task, form); } }; diff --git a/media/com_joomgallery/js/migrator/dist/migrator.js b/media/com_joomgallery/js/migrator/dist/migrator.js new file mode 100644 index 00000000..f547e791 --- /dev/null +++ b/media/com_joomgallery/js/migrator/dist/migrator.js @@ -0,0 +1,696 @@ +var Migrator; +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ // The require scope +/******/ var __webpack_require__ = {}; +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +/*!**********************!*\ + !*** ./src/index.js ***! + \**********************/ +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ repairTask: () => (/* binding */ repairTask), +/* harmony export */ stopTask: () => (/* binding */ stopTask), +/* harmony export */ submitTask: () => (/* binding */ submitTask), +/* harmony export */ updateMigrateablesList: () => (/* binding */ updateMigrateablesList) +/* harmony export */ }); +// Selectors used by this script +let typeSelector = 'data-type'; +let formIdTmpl = 'migrationForm'; +let buttonTmpl = 'migrationBtn'; +let step4Btn = 'step4Btn'; +let tryLimit = 3; + +/** + * Storage for migrateables + * @var {Object} migrateablesList + */ +var migrateablesList = {}; + +/** + * Counter of how many times the same migration was tried to perfrom + * @var {Integer} tryCounter + */ +var tryCounter = 0; + +/** + * State. As long as this state is set to true, the migration will be + * continued automatically regarding the pending queue in the migrateablesList. + * @var {Boolean} continueState + */ +var continueState = true; + +/** + * State. Set this state to true to stop automatic execution as soon as the next ajax respond comes back. + * @var {Boolean} forceStop + */ +var forceStop = false; + +/** + * Adds all completed migrateables to list + * + * @param {Object} event Event object + * @param {Object} element DOM element object + */ +let updateMigrateablesList = function() { + let types_inputs = document.getElementsByName('type'); + let types = {}; + + // Add all available migrateables to types object + types_inputs.forEach((type) => { + if(Boolean(type.value)) { + types[type.value] = false; + } + }); + + // Loop through all migrateables + Object.keys(types).forEach(type => { + let formId = formIdTmpl + '-' + type; + let form = document.getElementById(formId); + + let migrateable = atob(form.querySelector('[name="migrateable"]').value); + migrateable = JSON.parse(migrateable); + + if(migrateable['completed']) + { + // Add migrateable in list + migrateablesList[type] = migrateable; + } + }); +} + +/** + * Submit the migration task by pressing the button + * + * @param {Object} event Event object + * @param {Object} element DOM element object + */ +let submitTask = function(event, element) { + event.preventDefault(); + + let type = element.getAttribute(typeSelector); + let formId = formIdTmpl + '-' + type; + let task = element.parentNode.querySelector('[name="task"]').value; + + if(tryCounter == 0) { + startTask(type, element); + } + + tryCounter = tryCounter + 1; + + ajax(formId, task) + .then(res => { + // Handle the successful result here + responseHandler(type, res); + + if(tryCounter >= tryLimit) { + // We reached the limit of tries --> looks like we have a network problem + updateMigrateables(type, {'success': false, 'message': Joomla.JText._('COM_JOOMGALLERY_ERROR_NETWORK_PROBLEM'), 'data': false}); + // Stop automatic execution and update GUI + forceStop = true; + } + + if(continueState && !forceStop) { + // Kick off the next task + submitTask(event, element); + } else { + // Stop automatic task execution and update GUI + finishTask(type, element, formId); + } + }) + .catch(error => { + // Handle any errors here + addLog(error, type, 'error'); + }); +}; + +/** + * Stop the migration task by pressing the button + * + * @param {Object} event Event object + * @param {Object} element DOM element object + */ +let stopTask = function(event, element) { + event.preventDefault(); + + let type = element.getAttribute(typeSelector); + let bar = document.getElementById('progress-'+type); + let startBtn = document.getElementById('migrationBtn-'+type); + let stopBtn = element; + + // Force automatic execution to stop + forceStop = true; + + // Update progress bar + bar.classList.remove('progress-bar-striped'); + bar.classList.remove('progress-bar-animated'); + + // Enable start button + startBtn.classList.remove('disabled'); + startBtn.removeAttribute('disabled'); + + // Disable stop button + stopBtn.classList.add('disabled'); + stopBtn.setAttribute('disabled', 'true'); +} + +/** + * Manually set one record migration to true + * + * @param {Object} event Event object + * @param {Object} element DOM element object + */ +let repairTask = function(event, element) { + event.preventDefault(); + + // Get relevant elements + let type = element.getAttribute(typeSelector); + let mig = document.getElementById('migrationForm-'+type).querySelector('[name="migrateable"]'); + let inputType = document.getElementById('migrepairForm').querySelector('[name="type"]'); + let inputMig = document.getElementById('migrepairForm').querySelector('[name="migrateable"]'); + + // Fill input values + inputType.value = type; + inputMig.value = mig.value; + + // Show modal + let bsmodal = new bootstrap.Modal(document.getElementById('repair-modal-box'), {keyboard: false}); + bsmodal.show(); +} + +/** + * Perform an ajax request in json format + * + * @param {String} formId Id of the form element + * @param {String} task Name of the task + * + * @returns {Object} Result object + * {success: true, status: 200, message: '', messages: {}, data: { { {success, data, continue, error, debug, warning} }} + */ +let ajax = async function(formId, task) { + + // Catch form and data + let formData = new FormData(document.getElementById(formId)); + formData.append('format', 'json'); + + if(task == 'migration.start') { + formData.append('id', getNextMigrationID(formId)); + } + + // Set request parameters + let parameters = { + method: 'POST', + mode: 'same-origin', + cache: 'default', + redirect: 'follow', + referrerPolicy: 'no-referrer-when-downgrade', + body: formData, + }; + + // Set the url + let url = document.getElementById(formId).getAttribute('action'); + + // Perform the fetch request + let response = await fetch(url, parameters); + + // Resolve promise as text string + let txt = await response.text(); + let res = null; + + if (!response.ok) { + // Catch network error + return {success: false, status: response.status, message: response.message, messages: {}, data: {error: txt, data:null}}; + } + + if(txt.startsWith('{"success"')) { + // Response is of type json --> everything fine + res = JSON.parse(txt); + res.status = response.status; + res.data = JSON.parse(res.data); + } else if (txt.includes('Fatal error')) { + // PHP fatal error occurred + res = {success: false, status: response.status, message: response.statusText, messages: {}, data: {error: txt, data:null}}; + } else { + // Response is not of type json --> probably some php warnings/notices + let split = txt.split('\n{"'); + let temp = JSON.parse('{"'+split[1]); + let data = JSON.parse(temp.data); + res = {success: true, status: response.status, message: split[0], messages: temp.messages, data: data}; + } + + // Make sure res.data.data.queue is of type array + if(typeof res.data.data != "undefined" && res.data.data != null && 'queue' in res.data.data) { + if(res.data.data.queue.constructor !== Array) { + res.data.data.queue = Object.values(res.data.data.queue); + } + } + + return res; +} + +/** + * Perform a migration task + * @param {String} formId Id of the form element + * + * @returns {String} Id of the database record to be migrated + */ +let getNextMigrationID = function(formId) { + let type = formId.replace(formIdTmpl + '-', ''); + let form = document.getElementById(formId); + + let migrateable = atob(form.querySelector('[name="migrateable"]').value); + migrateable = JSON.parse(migrateable); + + // Overwrite migrateable in list + migrateablesList[type] = migrateable; + + // Loop through queue + for (let id of migrateable.queue) { + if (!(id in migrateable.successful) && !(id in migrateable.failed)) { + migrateablesList[type]['currentID'] = id; + break; + } + } + + return migrateablesList[type]['currentID']; +} + +/** + * Handle migration response + * + * @param {Object} response The response object in the form of + * {success: true, status: 200, message: '', messages: {}, data: { {success, data, continue, error, debug, warning} }} + * + * @returns void + */ +let responseHandler = function(type, response) { + if(response.success == false) { + // Ajax request failed or server responded with error code + addLog('Error in server response. We will try again. ('+tryCounter+'/'+tryLimit+')', type, 'info'); + addLog(response.message, type, 'error'); + addLog(response.messages, type, 'error'); + addLog(response.data.error, type, 'error'); + + + // Try again... + } + else { + // Ajax request successful + if(!response.data.success) + { + // Migration failed + addLog('[Migrator.js] Migration of '+type+' with id = '+migrateablesList[type]['currentID']+' failed.', type, 'error'); + logMessages(type, response.data); + + // Stop autimatic continuation if requested from backend + if(!response.data.continue || response.data.continue == null || response.data.continue == false) { + console.log('Stop automatic continuation requested from backend'); + continueState = false; + } + + // Update migrateables + updateMigrateables(type, response.data); + } + else + { + // Save record successful + logMessages(type, response.data); + addLog('[Migrator.js] Migration of '+type+' with id = '+migrateablesList[type]['currentID']+' successful.', type, 'success'); + + // Stop autimatic continuation if requested from backend + if(!response.data.continue || response.data.continue == null || response.data.continue == false) { + console.log('Stop automatic continuation requested from backend'); + continueState = false; + } + + // Update migrateables + updateMigrateables(type, response.data); + + // Reset tryCounter + tryCounter = 0; + } + } +} + +/** + * Add a message to the logging output and the console + * + * @param {Mixed} msg One or multiple messages to be added to the log + * @param {String} type The type defining the logging output to use + * @param {String} msgType The type of message (available: error, warning, success, info) + * @param {Boolean} console True to add the message also to the console + * @param {Boolean} newLine True to add the message on a new line + * @param {Integer} marginTop Number of how much margin you want on the top of the message + * + * @returns void + */ +let addLog = function(msg, type, msgType, console=false, newLine=true, marginTop=0) { + if(!Boolean(msg) || msg == null || msg == '') { + // Message is empty. Do nothing + return; + } else if(typeof msg === 'string') { + // Your message is a simple string + let tmp_msg = ''; + + // Test if your string a json string + try { + tmp_msg = JSON.parse(msg); + } catch (e) { + } + + // Convert string to array + if(tmp_msg !== '') { + // remove object properties 'error' and 'code' if existent + if('error' in tmp_msg) { + delete tmp_msg.error; + } + if('code' in tmp_msg) { + delete tmp_msg.code; + } + msg = Object.values(tmp_msg); + } else { + msg = [msg]; + } + } else if(typeof msg === 'object') { + // Your message is an object. Convert to array + msg = Object.values(msg); + } + + // Get logging output element + let logOutput = document.getElementById('logOutput-'+type); + + // Loop through all messages + msg.forEach((message, i) => { + // Print in console + if(console) { + console.log(message); + } + + // Create element + let line = null; + if(newLine) { + line = document.createElement('p'); + } else { + line = document.createElement('span'); + } + + // Top margin to element + marginTop = parseInt(marginTop); + if(marginTop > 0) { + line.classList.add('mt-'+String(marginTop)); + } + + // Add text color + line.classList.add('color-'+msgType); + + // Add message to element + let msgType_txt = msgType.toLocaleUpperCase(); + line.textContent = '['+Joomla.JText._(msgType_txt)+'] '+String(message); + + // Print into logging output + logOutput.appendChild(line); + }); +} + +/** + * Clear the logging output + * + * @param {String} type The type defining the logging output to clear + * + * @returns void + */ +let clearLog = function(type) { + // Get logging output element + let logOutput = document.getElementById('logOutput-'+type); + + // clear + logOutput.innerHTML = ''; +} + +/** + * Output all available messages from the result object + * + * @param {String} type The type defining the content type to be updated + * @param {Object} res The result object in the form of + * {success: bool, data: mixed, continue: bool, error: string|array, debug: string|array, warning: string|array} + * + * @returns void + */ +let logMessages = function(type, res) { + // Available message types: error, debug, warning + let available = ['error', 'debug', 'warning']; + let msgTypes = {'error': 'error', 'debug': 'info', 'warning': 'warning'}; + + available.forEach((value, index) => { + if(!res[value] || !Boolean(res.data) || res.data == null) { + return; + } + + addLog(res[value], type, msgTypes[value]); + }); +} + +/** + * Update migrateable input field, progress bar and badges + * + * @param {String} type The type defining the content type to be updated + * @param {Object} res The result object in the form of + * {success: bool, data: mixed, continue: bool, error: string|array, debug: string|array, warning: string|array} + * + * @returns void + */ +let updateMigrateables = function(type, res) { + let formId = formIdTmpl + '-' + type; + let form = document.getElementById(formId); + + if(!res.success && (!Boolean(res.data) || res.data == null || res.data == '')) { + // Migration failed, but no data available in result + + // Create result data based on input field + let migrateable = atob(form.querySelector('[name="migrateable"]').value); + res.data = JSON.parse(migrateable); + + // See: Joomgallery\Component\Joomgallery\Administrator\Model\MigrationModel::migrate + // Remove migrated primary key from queue + res.data.queue = res.data.queue.filter(function(e) { return e !== migrateablesList[type]['currentID'] }) + + // Add migrated primary key to failed object + res.data.failed[migrateablesList[type]['currentID']] = res.message; + } + + if(!Boolean(res.data.progress) || res.data.progress == null || res.data.progress == '') { + // Update progress if not delivered with result object + let total = res.data.queue.lenght + Object.keys(res.data.successful).length + Object.keys(res.data.failed).length; + let finished = Object.keys(res.data.successful).length + Object.keys(res.data.failed).length; + res.data.progress = Math.round((100 / total) * (finished)); + } + + // Get badges + let queueBadge = document.getElementById('badgeQueue-'+type); + let resBadge = document.getElementById('badgeSuccessful-'+type); + if(!res.success) { + resBadge = document.getElementById('badgeFailed-'+type); + } + + // Update migrateable input field + let field = form.querySelector('[name="migrateable"]'); + field.value = btoa(JSON.stringify(res.data)); + + // Update badges + queueBadge.innerHTML = parseInt(queueBadge.innerHTML) - 1; + resBadge.innerHTML = parseInt(resBadge.innerHTML) + 1; + + // Update progress bar + let bar = document.getElementById('progress-'+type); + bar.setAttribute('aria-valuenow', res.data.progress); + bar.style.width = res.data.progress + '%'; + bar.innerText = res.data.progress + '%'; +} + +/** + * Update GUI to start migration + * + * @param {String} type The type defining the content type to be updated + * @param {DOM Element} button The button beeing pressed to start the task + * + * @returns void + */ +let startTask = function(type, button) { + let bar = document.getElementById('progress-'+type); + let startBtn = button; + let stopBtn = document.getElementById('stopBtn-'+type); + + // Update progress bar + bar.classList.add('progress-bar-striped'); + bar.classList.add('progress-bar-animated'); + + // Disable start button + startBtn.classList.add('disabled'); + startBtn.setAttribute('disabled', 'true'); + + // Enable stop button + stopBtn.classList.remove('disabled'); + stopBtn.removeAttribute('disabled'); + + // Reinitialize variables + tryCounter = 0; + continueState = true; + forceStop = false; +} + +/** + * Update GUI to end migration + * + * @param {String} type The type defining the content type to be updated + * @param {DOM Element} button The button beeing pressed to start the task + * @param {String} formId Id of the form element + * + * @returns void + */ +let finishTask = function(type, button, formId) { + let bar = document.getElementById('progress-'+type); + let startBtn = button; + let stopBtn = document.getElementById('stopBtn-'+type); + let dependency = document.getElementById('dependent_of-'+type); + + // Update migrateablesList + getNextMigrationID(formId); + + // Update progress bar + bar.classList.remove('progress-bar-striped'); + bar.classList.remove('progress-bar-animated'); + + // Enable start button + if(!migrateablesList[type]['completed']) { + // Only enable start button if migration is not finished + startBtn.classList.remove('disabled'); + startBtn.removeAttribute('disabled'); + } + + // Disable stop button + stopBtn.classList.add('disabled'); + stopBtn.setAttribute('disabled', 'true'); + + // If migration is completed + if(migrateablesList[type]['completed']) { + dependency = JSON.parse(dependency.innerHTML); + if(dependency.length > 0) { + // Reload page + location.reload(); + } else { + // Update next start button + enableNextBtn(type, button); + // Update step 4 button + updateStep4Btn(); + } + } +} + +/** + * Enable start button of next migration content type + * + * @param {String} type The type defining the content type to be updated + * @param {DOM Element} button The current start button + * + * @returns void + */ +let enableNextBtn = function(type, button) { + let types_inputs = document.getElementsByName('type'); + let next_type = ''; + + // Find next migration content type + let this_type = false; + for (const type_input of types_inputs) { + if(this_type) { + next_type = type_input.value; + break; + } + if(Boolean(type_input.value) && type_input.value == type) { + this_type = true; + } + } + + if(next_type !== '') { + // Get next button + let nextBtn = document.getElementById(buttonTmpl + '-' + next_type); + + // Enable button + nextBtn.classList.remove('disabled'); + nextBtn.removeAttribute('disabled'); + } +} + +/** + * Update button to go to step 4 + * + * @returns void + */ +let updateStep4Btn = function() { + let types_inputs = document.getElementsByName('type'); + let types = {}; + + // Add all available migrateables to types object + types_inputs.forEach((type) => { + if(Boolean(type.value)) { + types[type.value] = false; + } + }); + + // Check if all migrateables are available and completed + let tot_complete = true; + Object.keys(types).forEach(type => { + if(Boolean(migrateablesList[type])) { + if(!migrateablesList[type]['completed']) + { + // Migrateable not yet completed + tot_complete = false; + } + } + else + { + // Migrateable does not yet exist. Thus not completed + tot_complete = false; + } + }); + + if(tot_complete) { + // Enable step 4 button + document.getElementById(step4Btn).classList.remove('disabled'); + document.getElementById(step4Btn).removeAttribute('disabled'); + } +} +Migrator = __webpack_exports__; +/******/ })() +; +//# sourceMappingURL=migrator.js.map \ No newline at end of file diff --git a/media/com_joomgallery/js/migrator/dist/migrator.js.map b/media/com_joomgallery/js/migrator/dist/migrator.js.map new file mode 100644 index 00000000..ae467e46 --- /dev/null +++ b/media/com_joomgallery/js/migrator/dist/migrator.js.map @@ -0,0 +1 @@ +{"version":3,"file":"migrator.js","mappings":";;;UAAA;UACA;;;;;WCDA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;;;;ACNA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,SAAS;AAClB;AACA;AACA;AACA;AACA;AACA,SAAS,UAAU;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,UAAU;AACnB;AACA;AACA;AACA;AACA;AACA,SAAS,UAAU;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,WAAW,SAAS;AACpB,WAAW,SAAS;AACpB;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;AACA,WAAW,SAAS;AACpB,WAAW,SAAS;AACpB;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kCAAkC,oGAAoG;AACtI;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA,WAAW,SAAS;AACpB,WAAW,SAAS;AACpB;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW,SAAS;AACpB,WAAW,SAAS;AACpB;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kFAAkF,gBAAgB;AAClG;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB;AACA,aAAa,UAAU;AACvB,aAAa,qDAAqD,aAAa;AAC/E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY,gFAAgF,SAAS;AACrG;AACA;AACA,sBAAsB;AACtB;AACA;AACA;AACA;AACA,IAAI;AACJ;AACA,WAAW,mFAAmF,SAAS;AACvG,IAAI;AACJ;AACA,8BAA8B;AAC9B,6BAA6B;AAC7B;AACA,WAAW;AACX;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,UAAU;AACvB;AACA,aAAa,UAAU;AACvB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,UAAU;AACvB,aAAa,qDAAqD,WAAW;AAC7E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB,aAAa,UAAU;AACvB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA,IAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;AACA,YAAY,UAAU;AACtB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY,UAAU;AACtB,YAAY,UAAU;AACtB,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA,kBAAkB;AAClB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;AACA,YAAY,UAAU;AACtB,YAAY,UAAU;AACtB,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,yDAAyD,kDAAkD;AAC3G;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY,aAAa;AACzB,YAAY,aAAa;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY,aAAa;AACzB,YAAY,aAAa;AACzB,YAAY,aAAa;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,YAAY,aAAa;AACzB,YAAY,aAAa;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;AACA;AACA,C","sources":["webpack://Migrator/webpack/bootstrap","webpack://Migrator/webpack/runtime/define property getters","webpack://Migrator/webpack/runtime/hasOwnProperty shorthand","webpack://Migrator/webpack/runtime/make namespace object","webpack://Migrator/./src/index.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","// Selectors used by this script\r\nlet typeSelector = 'data-type';\r\nlet formIdTmpl = 'migrationForm';\r\nlet buttonTmpl = 'migrationBtn';\r\nlet step4Btn = 'step4Btn';\r\nlet tryLimit = 3;\r\n\r\n/**\r\n * Storage for migrateables\r\n * @var {Object} migrateablesList\r\n */\r\nvar migrateablesList = {};\r\n\r\n/**\r\n * Counter of how many times the same migration was tried to perfrom\r\n * @var {Integer} tryCounter\r\n */\r\nvar tryCounter = 0;\r\n\r\n/**\r\n * State. As long as this state is set to true, the migration will be\r\n * continued automatically regarding the pending queue in the migrateablesList.\r\n * @var {Boolean} continueState\r\n */\r\nvar continueState = true;\r\n\r\n/**\r\n * State. Set this state to true to stop automatic execution as soon as the next ajax respond comes back.\r\n * @var {Boolean} forceStop\r\n */\r\nvar forceStop = false;\r\n\r\n/**\r\n * Adds all completed migrateables to list\r\n * \r\n * @param {Object} event Event object\r\n * @param {Object} element DOM element object\r\n */\r\nexport let updateMigrateablesList = function() {\r\n let types_inputs = document.getElementsByName('type');\r\n let types = {};\r\n\r\n // Add all available migrateables to types object\r\n types_inputs.forEach((type) => {\r\n if(Boolean(type.value)) {\r\n types[type.value] = false;\r\n }\r\n });\r\n\r\n // Loop through all migrateables\r\n Object.keys(types).forEach(type => {\r\n let formId = formIdTmpl + '-' + type;\r\n let form = document.getElementById(formId);\r\n\r\n let migrateable = atob(form.querySelector('[name=\"migrateable\"]').value);\r\n migrateable = JSON.parse(migrateable);\r\n\r\n if(migrateable['completed'])\r\n {\r\n // Add migrateable in list\r\n migrateablesList[type] = migrateable;\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * Submit the migration task by pressing the button\r\n * \r\n * @param {Object} event Event object\r\n * @param {Object} element DOM element object\r\n */\r\nexport let submitTask = function(event, element) {\r\n event.preventDefault();\r\n\r\n let type = element.getAttribute(typeSelector);\r\n let formId = formIdTmpl + '-' + type;\r\n let task = element.parentNode.querySelector('[name=\"task\"]').value;\r\n\r\n if(tryCounter == 0) {\r\n startTask(type, element);\r\n } \r\n\r\n tryCounter = tryCounter + 1;\r\n\r\n ajax(formId, task)\r\n .then(res => {\r\n // Handle the successful result here\r\n responseHandler(type, res);\r\n\r\n if(tryCounter >= tryLimit) {\r\n // We reached the limit of tries --> looks like we have a network problem\r\n updateMigrateables(type, {'success': false, 'message': Joomla.JText._('COM_JOOMGALLERY_ERROR_NETWORK_PROBLEM'), 'data': false});\r\n // Stop automatic execution and update GUI\r\n forceStop = true;\r\n }\r\n \r\n if(continueState && !forceStop) {\r\n // Kick off the next task\r\n submitTask(event, element);\r\n } else {\r\n // Stop automatic task execution and update GUI\r\n finishTask(type, element, formId);\r\n }\r\n })\r\n .catch(error => {\r\n // Handle any errors here\r\n addLog(error, type, 'error');\r\n });\r\n};\r\n\r\n/**\r\n * Stop the migration task by pressing the button\r\n * \r\n * @param {Object} event Event object\r\n * @param {Object} element DOM element object\r\n */\r\nexport let stopTask = function(event, element) {\r\n event.preventDefault();\r\n\r\n let type = element.getAttribute(typeSelector);\r\n let bar = document.getElementById('progress-'+type);\r\n let startBtn = document.getElementById('migrationBtn-'+type);\r\n let stopBtn = element;\r\n\r\n // Force automatic execution to stop\r\n forceStop = true;\r\n\r\n // Update progress bar\r\n bar.classList.remove('progress-bar-striped');\r\n bar.classList.remove('progress-bar-animated');\r\n \r\n // Enable start button\r\n startBtn.classList.remove('disabled');\r\n startBtn.removeAttribute('disabled');\r\n\r\n // Disable stop button\r\n stopBtn.classList.add('disabled');\r\n stopBtn.setAttribute('disabled', 'true');\r\n}\r\n\r\n/**\r\n * Manually set one record migration to true\r\n * \r\n * @param {Object} event Event object\r\n * @param {Object} element DOM element object\r\n */\r\nexport let repairTask = function(event, element) {\r\n event.preventDefault();\r\n\r\n // Get relevant elements\r\n let type = element.getAttribute(typeSelector);\r\n let mig = document.getElementById('migrationForm-'+type).querySelector('[name=\"migrateable\"]');\r\n let inputType = document.getElementById('migrepairForm').querySelector('[name=\"type\"]');\r\n let inputMig = document.getElementById('migrepairForm').querySelector('[name=\"migrateable\"]');\r\n\r\n // Fill input values\r\n inputType.value = type;\r\n inputMig.value = mig.value;\r\n\r\n // Show modal\r\n let bsmodal = new bootstrap.Modal(document.getElementById('repair-modal-box'), {keyboard: false});\r\n bsmodal.show();\r\n}\r\n\r\n/**\r\n * Perform an ajax request in json format\r\n * \r\n * @param {String} formId Id of the form element\r\n * @param {String} task Name of the task\r\n * \r\n * @returns {Object} Result object\r\n * {success: true, status: 200, message: '', messages: {}, data: { { {success, data, continue, error, debug, warning} }}\r\n */\r\nlet ajax = async function(formId, task) {\r\n\r\n // Catch form and data\r\n let formData = new FormData(document.getElementById(formId));\r\n formData.append('format', 'json');\r\n\r\n if(task == 'migration.start') {\r\n formData.append('id', getNextMigrationID(formId));\r\n }\r\n\r\n // Set request parameters\r\n let parameters = {\r\n method: 'POST',\r\n mode: 'same-origin',\r\n cache: 'default',\r\n redirect: 'follow',\r\n referrerPolicy: 'no-referrer-when-downgrade',\r\n body: formData,\r\n };\r\n\r\n // Set the url\r\n let url = document.getElementById(formId).getAttribute('action');\r\n\r\n // Perform the fetch request\r\n let response = await fetch(url, parameters);\r\n\r\n // Resolve promise as text string\r\n let txt = await response.text();\r\n let res = null;\r\n\r\n if (!response.ok) {\r\n // Catch network error\r\n return {success: false, status: response.status, message: response.message, messages: {}, data: {error: txt, data:null}};\r\n }\r\n\r\n if(txt.startsWith('{\"success\"')) {\r\n // Response is of type json --> everything fine\r\n res = JSON.parse(txt);\r\n res.status = response.status;\r\n res.data = JSON.parse(res.data);\r\n } else if (txt.includes('Fatal error')) {\r\n // PHP fatal error occurred\r\n res = {success: false, status: response.status, message: response.statusText, messages: {}, data: {error: txt, data:null}};\r\n } else {\r\n // Response is not of type json --> probably some php warnings/notices\r\n let split = txt.split('\\n{\"');\r\n let temp = JSON.parse('{\"'+split[1]);\r\n let data = JSON.parse(temp.data);\r\n res = {success: true, status: response.status, message: split[0], messages: temp.messages, data: data};\r\n }\r\n\r\n // Make sure res.data.data.queue is of type array\r\n if(typeof res.data.data != \"undefined\" && res.data.data != null && 'queue' in res.data.data) {\r\n if(res.data.data.queue.constructor !== Array) {\r\n res.data.data.queue = Object.values(res.data.data.queue);\r\n }\r\n }\r\n\r\n return res;\r\n}\r\n\r\n/**\r\n * Perform a migration task\r\n * @param {String} formId Id of the form element\r\n * \r\n * @returns {String} Id of the database record to be migrated\r\n */\r\nlet getNextMigrationID = function(formId) {\r\n let type = formId.replace(formIdTmpl + '-', '');\r\n let form = document.getElementById(formId);\r\n\r\n let migrateable = atob(form.querySelector('[name=\"migrateable\"]').value);\r\n migrateable = JSON.parse(migrateable);\r\n\r\n // Overwrite migrateable in list\r\n migrateablesList[type] = migrateable;\r\n\r\n // Loop through queue\r\n for (let id of migrateable.queue) {\r\n if (!(id in migrateable.successful) && !(id in migrateable.failed)) {\r\n migrateablesList[type]['currentID'] = id;\r\n break;\r\n }\r\n }\r\n\r\n return migrateablesList[type]['currentID'];\r\n}\r\n\r\n/**\r\n * Handle migration response\r\n * \r\n * @param {Object} response The response object in the form of\r\n * {success: true, status: 200, message: '', messages: {}, data: { {success, data, continue, error, debug, warning} }}\r\n * \r\n * @returns void\r\n */\r\nlet responseHandler = function(type, response) {\r\n if(response.success == false) {\r\n // Ajax request failed or server responded with error code\r\n addLog('Error in server response. We will try again. ('+tryCounter+'/'+tryLimit+')', type, 'info');\r\n addLog(response.message, type, 'error');\r\n addLog(response.messages, type, 'error');\r\n addLog(response.data.error, type, 'error');\r\n \r\n\r\n // Try again...\r\n }\r\n else {\r\n // Ajax request successful\r\n if(!response.data.success)\r\n {\r\n // Migration failed\r\n addLog('[Migrator.js] Migration of '+type+' with id = '+migrateablesList[type]['currentID']+' failed.', type, 'error');\r\n logMessages(type, response.data);\r\n\r\n // Stop autimatic continuation if requested from backend\r\n if(!response.data.continue || response.data.continue == null || response.data.continue == false) {\r\n console.log('Stop automatic continuation requested from backend');\r\n continueState = false;\r\n }\r\n\r\n // Update migrateables\r\n updateMigrateables(type, response.data);\r\n }\r\n else\r\n {\r\n // Save record successful\r\n logMessages(type, response.data);\r\n addLog('[Migrator.js] Migration of '+type+' with id = '+migrateablesList[type]['currentID']+' successful.', type, 'success');\r\n\r\n // Stop autimatic continuation if requested from backend\r\n if(!response.data.continue || response.data.continue == null || response.data.continue == false) {\r\n console.log('Stop automatic continuation requested from backend');\r\n continueState = false;\r\n }\r\n\r\n // Update migrateables\r\n updateMigrateables(type, response.data);\r\n\r\n // Reset tryCounter\r\n tryCounter = 0;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Add a message to the logging output and the console\r\n * \r\n * @param {Mixed} msg One or multiple messages to be added to the log\r\n * @param {String} type The type defining the logging output to use\r\n * @param {String} msgType The type of message (available: error, warning, success, info)\r\n * @param {Boolean} console True to add the message also to the console\r\n * @param {Boolean} newLine True to add the message on a new line\r\n * @param {Integer} marginTop Number of how much margin you want on the top of the message\r\n * \r\n * @returns void\r\n */\r\nlet addLog = function(msg, type, msgType, console=false, newLine=true, marginTop=0) {\r\n if(!Boolean(msg) || msg == null || msg == '') {\r\n // Message is empty. Do nothing\r\n return;\r\n } else if(typeof msg === 'string') {\r\n // Your message is a simple string\r\n let tmp_msg = '';\r\n\r\n // Test if your string a json string\r\n try {\r\n tmp_msg = JSON.parse(msg);\r\n } catch (e) {\r\n }\r\n\r\n // Convert string to array\r\n if(tmp_msg !== '') {\r\n // remove object properties 'error' and 'code' if existent\r\n if('error' in tmp_msg) {\r\n delete tmp_msg.error;\r\n }\r\n if('code' in tmp_msg) {\r\n delete tmp_msg.code;\r\n }\r\n msg = Object.values(tmp_msg);\r\n } else {\r\n msg = [msg];\r\n }\r\n } else if(typeof msg === 'object') {\r\n // Your message is an object. Convert to array\r\n msg = Object.values(msg);\r\n }\r\n\r\n // Get logging output element\r\n let logOutput = document.getElementById('logOutput-'+type);\r\n\r\n // Loop through all messages\r\n msg.forEach((message, i) => {\r\n // Print in console\r\n if(console) {\r\n console.log(message);\r\n }\r\n\r\n // Create element\r\n let line = null;\r\n if(newLine) {\r\n line = document.createElement('p');\r\n } else {\r\n line = document.createElement('span');\r\n }\r\n\r\n // Top margin to element\r\n marginTop = parseInt(marginTop);\r\n if(marginTop > 0) {\r\n line.classList.add('mt-'+String(marginTop));\r\n }\r\n\r\n // Add text color\r\n line.classList.add('color-'+msgType);\r\n \r\n // Add message to element\r\n let msgType_txt = msgType.toLocaleUpperCase();\r\n line.textContent = '['+Joomla.JText._(msgType_txt)+'] '+String(message);\r\n\r\n // Print into logging output\r\n logOutput.appendChild(line);\r\n });\r\n}\r\n\r\n/**\r\n * Clear the logging output\r\n *\r\n * @param {String} type The type defining the logging output to clear\r\n * \r\n * @returns void\r\n */\r\nlet clearLog = function(type) {\r\n // Get logging output element\r\n let logOutput = document.getElementById('logOutput-'+type);\r\n\r\n // clear\r\n logOutput.innerHTML = '';\r\n}\r\n\r\n/**\r\n * Output all available messages from the result object\r\n *\r\n * @param {String} type The type defining the content type to be updated\r\n * @param {Object} res The result object in the form of\r\n * {success: bool, data: mixed, continue: bool, error: string|array, debug: string|array, warning: string|array}\r\n * \r\n * @returns void\r\n */\r\nlet logMessages = function(type, res) {\r\n // Available message types: error, debug, warning\r\n let available = ['error', 'debug', 'warning'];\r\n let msgTypes = {'error': 'error', 'debug': 'info', 'warning': 'warning'};\r\n\r\n available.forEach((value, index) => {\r\n if(!res[value] || !Boolean(res.data) || res.data == null) {\r\n return;\r\n }\r\n\r\n addLog(res[value], type, msgTypes[value]);\r\n });\r\n}\r\n\r\n/**\r\n * Update migrateable input field, progress bar and badges\r\n *\r\n * @param {String} type The type defining the content type to be updated\r\n * @param {Object} res The result object in the form of\r\n * {success: bool, data: mixed, continue: bool, error: string|array, debug: string|array, warning: string|array}\r\n * \r\n * @returns void\r\n */\r\nlet updateMigrateables = function(type, res) {\r\n let formId = formIdTmpl + '-' + type;\r\n let form = document.getElementById(formId);\r\n\r\n if(!res.success && (!Boolean(res.data) || res.data == null || res.data == '')) {\r\n // Migration failed, but no data available in result\r\n\r\n // Create result data based on input field\r\n let migrateable = atob(form.querySelector('[name=\"migrateable\"]').value);\r\n res.data = JSON.parse(migrateable);\r\n\r\n // See: Joomgallery\\Component\\Joomgallery\\Administrator\\Model\\MigrationModel::migrate\r\n // Remove migrated primary key from queue\r\n res.data.queue = res.data.queue.filter(function(e) { return e !== migrateablesList[type]['currentID'] })\r\n\r\n // Add migrated primary key to failed object\r\n res.data.failed[migrateablesList[type]['currentID']] = res.message;\r\n }\r\n\r\n if(!Boolean(res.data.progress) || res.data.progress == null || res.data.progress == '') {\r\n // Update progress if not delivered with result object\r\n let total = res.data.queue.lenght + Object.keys(res.data.successful).length + Object.keys(res.data.failed).length;\r\n let finished = Object.keys(res.data.successful).length + Object.keys(res.data.failed).length;\r\n res.data.progress = Math.round((100 / total) * (finished));\r\n }\r\n\r\n // Get badges\r\n let queueBadge = document.getElementById('badgeQueue-'+type);\r\n let resBadge = document.getElementById('badgeSuccessful-'+type);\r\n if(!res.success) {\r\n resBadge = document.getElementById('badgeFailed-'+type);\r\n }\r\n\r\n // Update migrateable input field\r\n let field = form.querySelector('[name=\"migrateable\"]');\r\n field.value = btoa(JSON.stringify(res.data));\r\n\r\n // Update badges\r\n queueBadge.innerHTML = parseInt(queueBadge.innerHTML) - 1;\r\n resBadge.innerHTML = parseInt(resBadge.innerHTML) + 1;\r\n\r\n // Update progress bar\r\n let bar = document.getElementById('progress-'+type);\r\n bar.setAttribute('aria-valuenow', res.data.progress);\r\n bar.style.width = res.data.progress + '%';\r\n bar.innerText = res.data.progress + '%';\r\n}\r\n\r\n/**\r\n * Update GUI to start migration\r\n *\r\n * @param {String} type The type defining the content type to be updated\r\n * @param {DOM Element} button The button beeing pressed to start the task\r\n * \r\n * @returns void\r\n */\r\nlet startTask = function(type, button) {\r\n let bar = document.getElementById('progress-'+type);\r\n let startBtn = button;\r\n let stopBtn = document.getElementById('stopBtn-'+type);\r\n\r\n // Update progress bar\r\n bar.classList.add('progress-bar-striped');\r\n bar.classList.add('progress-bar-animated');\r\n \r\n // Disable start button\r\n startBtn.classList.add('disabled');\r\n startBtn.setAttribute('disabled', 'true');\r\n\r\n // Enable stop button\r\n stopBtn.classList.remove('disabled');\r\n stopBtn.removeAttribute('disabled');\r\n\r\n // Reinitialize variables\r\n tryCounter = 0;\r\n continueState = true;\r\n forceStop = false;\r\n}\r\n\r\n/**\r\n * Update GUI to end migration\r\n *\r\n * @param {String} type The type defining the content type to be updated\r\n * @param {DOM Element} button The button beeing pressed to start the task\r\n * @param {String} formId Id of the form element\r\n * \r\n * @returns void\r\n */\r\nlet finishTask = function(type, button, formId) {\r\n let bar = document.getElementById('progress-'+type);\r\n let startBtn = button;\r\n let stopBtn = document.getElementById('stopBtn-'+type);\r\n let dependency = document.getElementById('dependent_of-'+type);\r\n\r\n // Update migrateablesList\r\n getNextMigrationID(formId);\r\n\r\n // Update progress bar\r\n bar.classList.remove('progress-bar-striped');\r\n bar.classList.remove('progress-bar-animated');\r\n \r\n // Enable start button\r\n if(!migrateablesList[type]['completed']) {\r\n // Only enable start button if migration is not finished\r\n startBtn.classList.remove('disabled');\r\n startBtn.removeAttribute('disabled');\r\n }\r\n\r\n // Disable stop button\r\n stopBtn.classList.add('disabled');\r\n stopBtn.setAttribute('disabled', 'true');\r\n\r\n // If migration is completed\r\n if(migrateablesList[type]['completed']) {\r\n dependency = JSON.parse(dependency.innerHTML);\r\n if(dependency.length > 0) {\r\n // Reload page\r\n location.reload();\r\n } else {\r\n // Update next start button\r\n enableNextBtn(type, button);\r\n // Update step 4 button\r\n updateStep4Btn();\r\n } \r\n }\r\n}\r\n\r\n/**\r\n * Enable start button of next migration content type\r\n * \r\n * @param {String} type The type defining the content type to be updated\r\n * @param {DOM Element} button The current start button\r\n * \r\n * @returns void\r\n */\r\nlet enableNextBtn = function(type, button) {\r\n let types_inputs = document.getElementsByName('type');\r\n let next_type = '';\r\n\r\n // Find next migration content type\r\n let this_type = false;\r\n for (const type_input of types_inputs) {\r\n if(this_type) {\r\n next_type = type_input.value;\r\n break;\r\n }\r\n if(Boolean(type_input.value) && type_input.value == type) {\r\n this_type = true;\r\n }\r\n }\r\n\r\n if(next_type !== '') {\r\n // Get next button\r\n let nextBtn = document.getElementById(buttonTmpl + '-' + next_type);\r\n\r\n // Enable button\r\n nextBtn.classList.remove('disabled');\r\n nextBtn.removeAttribute('disabled');\r\n }\r\n}\r\n\r\n/**\r\n * Update button to go to step 4\r\n * \r\n * @returns void\r\n */\r\nlet updateStep4Btn = function() {\r\n let types_inputs = document.getElementsByName('type');\r\n let types = {};\r\n\r\n // Add all available migrateables to types object\r\n types_inputs.forEach((type) => {\r\n if(Boolean(type.value)) {\r\n types[type.value] = false;\r\n }\r\n });\r\n\r\n // Check if all migrateables are available and completed\r\n let tot_complete = true;\r\n Object.keys(types).forEach(type => {\r\n if(Boolean(migrateablesList[type])) {\r\n if(!migrateablesList[type]['completed'])\r\n {\r\n // Migrateable not yet completed\r\n tot_complete = false;\r\n }\r\n }\r\n else\r\n {\r\n // Migrateable does not yet exist. Thus not completed\r\n tot_complete = false;\r\n }\r\n });\r\n\r\n if(tot_complete) {\r\n // Enable step 4 button\r\n document.getElementById(step4Btn).classList.remove('disabled');\r\n document.getElementById(step4Btn).removeAttribute('disabled');\r\n }\r\n}"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/media/com_joomgallery/js/migrator/package-lock.json b/media/com_joomgallery/js/migrator/package-lock.json new file mode 100644 index 00000000..3c84565c --- /dev/null +++ b/media/com_joomgallery/js/migrator/package-lock.json @@ -0,0 +1,1361 @@ +{ + "name": "migrator", + "version": "4.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "migrator", + "version": "4.0.0", + "license": "GNU", + "devDependencies": { + "webpack": "^5.80.0", + "webpack-cli": "^5.0.2" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", + "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz", + "integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001564", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", + "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.590", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.590.tgz", + "integrity": "sha512-hohItzsQcG7/FBsviCYMtQwUSWvVF7NVqPOnJCErWsAshsP/CR2LAXdmq276RbESNdhxiAq5/vRo1g2pxGXVww==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + } + } +} diff --git a/media/com_joomgallery/js/migrator/package.json b/media/com_joomgallery/js/migrator/package.json new file mode 100644 index 00000000..f4374b18 --- /dev/null +++ b/media/com_joomgallery/js/migrator/package.json @@ -0,0 +1,18 @@ +{ + "name": "migrator", + "version": "4.0.0", + "description": "JavaScript app to handle migration process", + "main": "index.js", + "scripts": { + "build": "webpack" + }, + "keywords": [], + "author": "JoomGallery::ProjectTeam", + "license": "GNU", + "devDependencies": { + "webpack": "^5.80.0", + "webpack-cli": "^5.0.2" + }, + "dependencies": { + } +} diff --git a/media/com_joomgallery/js/migrator/src/index.js b/media/com_joomgallery/js/migrator/src/index.js new file mode 100644 index 00000000..eb891eb8 --- /dev/null +++ b/media/com_joomgallery/js/migrator/src/index.js @@ -0,0 +1,645 @@ +// Selectors used by this script +let typeSelector = 'data-type'; +let formIdTmpl = 'migrationForm'; +let buttonTmpl = 'migrationBtn'; +let step4Btn = 'step4Btn'; +let tryLimit = 3; + +/** + * Storage for migrateables + * @var {Object} migrateablesList + */ +var migrateablesList = {}; + +/** + * Counter of how many times the same migration was tried to perfrom + * @var {Integer} tryCounter + */ +var tryCounter = 0; + +/** + * State. As long as this state is set to true, the migration will be + * continued automatically regarding the pending queue in the migrateablesList. + * @var {Boolean} continueState + */ +var continueState = true; + +/** + * State. Set this state to true to stop automatic execution as soon as the next ajax respond comes back. + * @var {Boolean} forceStop + */ +var forceStop = false; + +/** + * Adds all completed migrateables to list + * + * @param {Object} event Event object + * @param {Object} element DOM element object + */ +export let updateMigrateablesList = function() { + let types_inputs = document.getElementsByName('type'); + let types = {}; + + // Add all available migrateables to types object + types_inputs.forEach((type) => { + if(Boolean(type.value)) { + types[type.value] = false; + } + }); + + // Loop through all migrateables + Object.keys(types).forEach(type => { + let formId = formIdTmpl + '-' + type; + let form = document.getElementById(formId); + + let migrateable = atob(form.querySelector('[name="migrateable"]').value); + migrateable = JSON.parse(migrateable); + + if(migrateable['completed']) + { + // Add migrateable in list + migrateablesList[type] = migrateable; + } + }); +} + +/** + * Submit the migration task by pressing the button + * + * @param {Object} event Event object + * @param {Object} element DOM element object + */ +export let submitTask = function(event, element) { + event.preventDefault(); + + let type = element.getAttribute(typeSelector); + let formId = formIdTmpl + '-' + type; + let task = element.parentNode.querySelector('[name="task"]').value; + + if(tryCounter == 0) { + startTask(type, element); + } + + tryCounter = tryCounter + 1; + + ajax(formId, task) + .then(res => { + // Handle the successful result here + responseHandler(type, res); + + if(tryCounter >= tryLimit) { + // We reached the limit of tries --> looks like we have a network problem + updateMigrateables(type, {'success': false, 'message': Joomla.JText._('COM_JOOMGALLERY_ERROR_NETWORK_PROBLEM'), 'data': false}); + // Stop automatic execution and update GUI + forceStop = true; + } + + if(continueState && !forceStop) { + // Kick off the next task + submitTask(event, element); + } else { + // Stop automatic task execution and update GUI + finishTask(type, element, formId); + } + }) + .catch(error => { + // Handle any errors here + addLog(error, type, 'error'); + }); +}; + +/** + * Stop the migration task by pressing the button + * + * @param {Object} event Event object + * @param {Object} element DOM element object + */ +export let stopTask = function(event, element) { + event.preventDefault(); + + let type = element.getAttribute(typeSelector); + let bar = document.getElementById('progress-'+type); + let startBtn = document.getElementById('migrationBtn-'+type); + let stopBtn = element; + + // Force automatic execution to stop + forceStop = true; + + // Update progress bar + bar.classList.remove('progress-bar-striped'); + bar.classList.remove('progress-bar-animated'); + + // Enable start button + startBtn.classList.remove('disabled'); + startBtn.removeAttribute('disabled'); + + // Disable stop button + stopBtn.classList.add('disabled'); + stopBtn.setAttribute('disabled', 'true'); +} + +/** + * Manually set one record migration to true + * + * @param {Object} event Event object + * @param {Object} element DOM element object + */ +export let repairTask = function(event, element) { + event.preventDefault(); + + // Get relevant elements + let type = element.getAttribute(typeSelector); + let mig = document.getElementById('migrationForm-'+type).querySelector('[name="migrateable"]'); + let inputType = document.getElementById('migrepairForm').querySelector('[name="type"]'); + let inputMig = document.getElementById('migrepairForm').querySelector('[name="migrateable"]'); + + // Fill input values + inputType.value = type; + inputMig.value = mig.value; + + // Show modal + let bsmodal = new bootstrap.Modal(document.getElementById('repair-modal-box'), {keyboard: false}); + bsmodal.show(); +} + +/** + * Perform an ajax request in json format + * + * @param {String} formId Id of the form element + * @param {String} task Name of the task + * + * @returns {Object} Result object + * {success: true, status: 200, message: '', messages: {}, data: { { {success, data, continue, error, debug, warning} }} + */ +let ajax = async function(formId, task) { + + // Catch form and data + let formData = new FormData(document.getElementById(formId)); + formData.append('format', 'json'); + + if(task == 'migration.start') { + formData.append('id', getNextMigrationID(formId)); + } + + // Set request parameters + let parameters = { + method: 'POST', + mode: 'same-origin', + cache: 'default', + redirect: 'follow', + referrerPolicy: 'no-referrer-when-downgrade', + body: formData, + }; + + // Set the url + let url = document.getElementById(formId).getAttribute('action'); + + // Perform the fetch request + let response = await fetch(url, parameters); + + // Resolve promise as text string + let txt = await response.text(); + let res = null; + + if (!response.ok) { + // Catch network error + return {success: false, status: response.status, message: response.message, messages: {}, data: {error: txt, data:null}}; + } + + if(txt.startsWith('{"success"')) { + // Response is of type json --> everything fine + res = JSON.parse(txt); + res.status = response.status; + res.data = JSON.parse(res.data); + } else if (txt.includes('Fatal error')) { + // PHP fatal error occurred + res = {success: false, status: response.status, message: response.statusText, messages: {}, data: {error: txt, data:null}}; + } else { + // Response is not of type json --> probably some php warnings/notices + let split = txt.split('\n{"'); + let temp = JSON.parse('{"'+split[1]); + let data = JSON.parse(temp.data); + res = {success: true, status: response.status, message: split[0], messages: temp.messages, data: data}; + } + + // Make sure res.data.data.queue is of type array + if(typeof res.data.data != "undefined" && res.data.data != null && 'queue' in res.data.data) { + if(res.data.data.queue.constructor !== Array) { + res.data.data.queue = Object.values(res.data.data.queue); + } + } + + return res; +} + +/** + * Perform a migration task + * @param {String} formId Id of the form element + * + * @returns {String} Id of the database record to be migrated + */ +let getNextMigrationID = function(formId) { + let type = formId.replace(formIdTmpl + '-', ''); + let form = document.getElementById(formId); + + let migrateable = atob(form.querySelector('[name="migrateable"]').value); + migrateable = JSON.parse(migrateable); + + // Overwrite migrateable in list + migrateablesList[type] = migrateable; + + // Loop through queue + for (let id of migrateable.queue) { + if (!(id in migrateable.successful) && !(id in migrateable.failed)) { + migrateablesList[type]['currentID'] = id; + break; + } + } + + return migrateablesList[type]['currentID']; +} + +/** + * Handle migration response + * + * @param {Object} response The response object in the form of + * {success: true, status: 200, message: '', messages: {}, data: { {success, data, continue, error, debug, warning} }} + * + * @returns void + */ +let responseHandler = function(type, response) { + if(response.success == false) { + // Ajax request failed or server responded with error code + addLog('Error in server response. We will try again. ('+tryCounter+'/'+tryLimit+')', type, 'info'); + addLog(response.message, type, 'error'); + addLog(response.messages, type, 'error'); + addLog(response.data.error, type, 'error'); + + + // Try again... + } + else { + // Ajax request successful + if(!response.data.success) + { + // Migration failed + addLog('[Migrator.js] Migration of '+type+' with id = '+migrateablesList[type]['currentID']+' failed.', type, 'error'); + logMessages(type, response.data); + + // Stop autimatic continuation if requested from backend + if(!response.data.continue || response.data.continue == null || response.data.continue == false) { + console.log('Stop automatic continuation requested from backend'); + continueState = false; + } + + // Update migrateables + updateMigrateables(type, response.data); + } + else + { + // Save record successful + logMessages(type, response.data); + addLog('[Migrator.js] Migration of '+type+' with id = '+migrateablesList[type]['currentID']+' successful.', type, 'success'); + + // Stop autimatic continuation if requested from backend + if(!response.data.continue || response.data.continue == null || response.data.continue == false) { + console.log('Stop automatic continuation requested from backend'); + continueState = false; + } + + // Update migrateables + updateMigrateables(type, response.data); + + // Reset tryCounter + tryCounter = 0; + } + } +} + +/** + * Add a message to the logging output and the console + * + * @param {Mixed} msg One or multiple messages to be added to the log + * @param {String} type The type defining the logging output to use + * @param {String} msgType The type of message (available: error, warning, success, info) + * @param {Boolean} console True to add the message also to the console + * @param {Boolean} newLine True to add the message on a new line + * @param {Integer} marginTop Number of how much margin you want on the top of the message + * + * @returns void + */ +let addLog = function(msg, type, msgType, console=false, newLine=true, marginTop=0) { + if(!Boolean(msg) || msg == null || msg == '') { + // Message is empty. Do nothing + return; + } else if(typeof msg === 'string') { + // Your message is a simple string + let tmp_msg = ''; + + // Test if your string a json string + try { + tmp_msg = JSON.parse(msg); + } catch (e) { + } + + // Convert string to array + if(tmp_msg !== '') { + // remove object properties 'error' and 'code' if existent + if('error' in tmp_msg) { + delete tmp_msg.error; + } + if('code' in tmp_msg) { + delete tmp_msg.code; + } + msg = Object.values(tmp_msg); + } else { + msg = [msg]; + } + } else if(typeof msg === 'object') { + // Your message is an object. Convert to array + msg = Object.values(msg); + } + + // Get logging output element + let logOutput = document.getElementById('logOutput-'+type); + + // Loop through all messages + msg.forEach((message, i) => { + // Print in console + if(console) { + console.log(message); + } + + // Create element + let line = null; + if(newLine) { + line = document.createElement('p'); + } else { + line = document.createElement('span'); + } + + // Top margin to element + marginTop = parseInt(marginTop); + if(marginTop > 0) { + line.classList.add('mt-'+String(marginTop)); + } + + // Add text color + line.classList.add('color-'+msgType); + + // Add message to element + let msgType_txt = msgType.toLocaleUpperCase(); + line.textContent = '['+Joomla.JText._(msgType_txt)+'] '+String(message); + + // Print into logging output + logOutput.appendChild(line); + }); +} + +/** + * Clear the logging output + * + * @param {String} type The type defining the logging output to clear + * + * @returns void + */ +let clearLog = function(type) { + // Get logging output element + let logOutput = document.getElementById('logOutput-'+type); + + // clear + logOutput.innerHTML = ''; +} + +/** + * Output all available messages from the result object + * + * @param {String} type The type defining the content type to be updated + * @param {Object} res The result object in the form of + * {success: bool, data: mixed, continue: bool, error: string|array, debug: string|array, warning: string|array} + * + * @returns void + */ +let logMessages = function(type, res) { + // Available message types: error, debug, warning + let available = ['error', 'debug', 'warning']; + let msgTypes = {'error': 'error', 'debug': 'info', 'warning': 'warning'}; + + available.forEach((value, index) => { + if(!res[value] || !Boolean(res.data) || res.data == null) { + return; + } + + addLog(res[value], type, msgTypes[value]); + }); +} + +/** + * Update migrateable input field, progress bar and badges + * + * @param {String} type The type defining the content type to be updated + * @param {Object} res The result object in the form of + * {success: bool, data: mixed, continue: bool, error: string|array, debug: string|array, warning: string|array} + * + * @returns void + */ +let updateMigrateables = function(type, res) { + let formId = formIdTmpl + '-' + type; + let form = document.getElementById(formId); + + if(!res.success && (!Boolean(res.data) || res.data == null || res.data == '')) { + // Migration failed, but no data available in result + + // Create result data based on input field + let migrateable = atob(form.querySelector('[name="migrateable"]').value); + res.data = JSON.parse(migrateable); + + // See: Joomgallery\Component\Joomgallery\Administrator\Model\MigrationModel::migrate + // Remove migrated primary key from queue + res.data.queue = res.data.queue.filter(function(e) { return e !== migrateablesList[type]['currentID'] }) + + // Add migrated primary key to failed object + res.data.failed[migrateablesList[type]['currentID']] = res.message; + } + + if(!Boolean(res.data.progress) || res.data.progress == null || res.data.progress == '') { + // Update progress if not delivered with result object + let total = res.data.queue.lenght + Object.keys(res.data.successful).length + Object.keys(res.data.failed).length; + let finished = Object.keys(res.data.successful).length + Object.keys(res.data.failed).length; + res.data.progress = Math.round((100 / total) * (finished)); + } + + // Get badges + let queueBadge = document.getElementById('badgeQueue-'+type); + let resBadge = document.getElementById('badgeSuccessful-'+type); + if(!res.success) { + resBadge = document.getElementById('badgeFailed-'+type); + } + + // Update migrateable input field + let field = form.querySelector('[name="migrateable"]'); + field.value = btoa(JSON.stringify(res.data)); + + // Update badges + queueBadge.innerHTML = parseInt(queueBadge.innerHTML) - 1; + resBadge.innerHTML = parseInt(resBadge.innerHTML) + 1; + + // Update progress bar + let bar = document.getElementById('progress-'+type); + bar.setAttribute('aria-valuenow', res.data.progress); + bar.style.width = res.data.progress + '%'; + bar.innerText = res.data.progress + '%'; +} + +/** + * Update GUI to start migration + * + * @param {String} type The type defining the content type to be updated + * @param {DOM Element} button The button beeing pressed to start the task + * + * @returns void + */ +let startTask = function(type, button) { + let bar = document.getElementById('progress-'+type); + let startBtn = button; + let stopBtn = document.getElementById('stopBtn-'+type); + + // Update progress bar + bar.classList.add('progress-bar-striped'); + bar.classList.add('progress-bar-animated'); + + // Disable start button + startBtn.classList.add('disabled'); + startBtn.setAttribute('disabled', 'true'); + + // Enable stop button + stopBtn.classList.remove('disabled'); + stopBtn.removeAttribute('disabled'); + + // Reinitialize variables + tryCounter = 0; + continueState = true; + forceStop = false; +} + +/** + * Update GUI to end migration + * + * @param {String} type The type defining the content type to be updated + * @param {DOM Element} button The button beeing pressed to start the task + * @param {String} formId Id of the form element + * + * @returns void + */ +let finishTask = function(type, button, formId) { + let bar = document.getElementById('progress-'+type); + let startBtn = button; + let stopBtn = document.getElementById('stopBtn-'+type); + let dependency = document.getElementById('dependent_of-'+type); + + // Update migrateablesList + getNextMigrationID(formId); + + // Update progress bar + bar.classList.remove('progress-bar-striped'); + bar.classList.remove('progress-bar-animated'); + + // Enable start button + if(!migrateablesList[type]['completed']) { + // Only enable start button if migration is not finished + startBtn.classList.remove('disabled'); + startBtn.removeAttribute('disabled'); + } + + // Disable stop button + stopBtn.classList.add('disabled'); + stopBtn.setAttribute('disabled', 'true'); + + // If migration is completed + if(migrateablesList[type]['completed']) { + dependency = JSON.parse(dependency.innerHTML); + if(dependency.length > 0) { + // Reload page + location.reload(); + } else { + // Update next start button + enableNextBtn(type, button); + // Update step 4 button + updateStep4Btn(); + } + } +} + +/** + * Enable start button of next migration content type + * + * @param {String} type The type defining the content type to be updated + * @param {DOM Element} button The current start button + * + * @returns void + */ +let enableNextBtn = function(type, button) { + let types_inputs = document.getElementsByName('type'); + let next_type = ''; + + // Find next migration content type + let this_type = false; + for (const type_input of types_inputs) { + if(this_type) { + next_type = type_input.value; + break; + } + if(Boolean(type_input.value) && type_input.value == type) { + this_type = true; + } + } + + if(next_type !== '') { + // Get next button + let nextBtn = document.getElementById(buttonTmpl + '-' + next_type); + + // Enable button + nextBtn.classList.remove('disabled'); + nextBtn.removeAttribute('disabled'); + } +} + +/** + * Update button to go to step 4 + * + * @returns void + */ +let updateStep4Btn = function() { + let types_inputs = document.getElementsByName('type'); + let types = {}; + + // Add all available migrateables to types object + types_inputs.forEach((type) => { + if(Boolean(type.value)) { + types[type.value] = false; + } + }); + + // Check if all migrateables are available and completed + let tot_complete = true; + Object.keys(types).forEach(type => { + if(Boolean(migrateablesList[type])) { + if(!migrateablesList[type]['completed']) + { + // Migrateable not yet completed + tot_complete = false; + } + } + else + { + // Migrateable does not yet exist. Thus not completed + tot_complete = false; + } + }); + + if(tot_complete) { + // Enable step 4 button + document.getElementById(step4Btn).classList.remove('disabled'); + document.getElementById(step4Btn).removeAttribute('disabled'); + } +} \ No newline at end of file diff --git a/media/com_joomgallery/js/migrator/webpack.config.js b/media/com_joomgallery/js/migrator/webpack.config.js new file mode 100644 index 00000000..f49c3318 --- /dev/null +++ b/media/com_joomgallery/js/migrator/webpack.config.js @@ -0,0 +1,15 @@ +const path = require('path'); + +module.exports = { + target: 'web', + //mode: 'production', + mode: 'development', + entry: './src/index.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'migrator.js', + library: 'Migrator', + libraryTarget: 'var' + }, + devtool: 'source-map', +} diff --git a/media/com_joomgallery/js/tasklessForm.js b/media/com_joomgallery/js/tasklessForm.js new file mode 100644 index 00000000..6aeaccd1 --- /dev/null +++ b/media/com_joomgallery/js/tasklessForm.js @@ -0,0 +1,42 @@ +/** + * @copyright (C) 2018 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ +((document, submitForm) => { + + // Selectors used by this script + const buttonDataSelector = 'data-submit-task'; + const formId = 'adminForm'; + const containerId = 'formInputContainer'; + + /** + * Submit the task + * @param task + */ + const submitTask = task => { + const form = document.getElementById(formId); + const container = document.getElementById(containerId); + + // add task input element + let input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'task'; + input.value = task; + container.appendChild(input); + + // submit form + form.submit(); + }; + + // Register events + document.addEventListener('DOMContentLoaded', () => { + const buttons = [].slice.call(document.querySelectorAll(`[${buttonDataSelector}]`)); + buttons.forEach(button => { + button.addEventListener('click', e => { + e.preventDefault(); + const task = e.target.getAttribute(buttonDataSelector); + submitTask(task); + }); + }); + }); +})(document, Joomla.submitform); diff --git a/script.php b/script.php index b9df3677..6c750610 100644 --- a/script.php +++ b/script.php @@ -38,6 +38,13 @@ class com_joomgalleryInstallerScript extends InstallerScript */ protected $extension = 'JoomGallery'; + /** + * List of incompatible Joomla versions + * + * @var array + */ + protected $incompatible = array('4.4.0', '4.4.1', '5.0.0', '5.0.1'); + /** * Minimum PHP version required to install the extension * @@ -95,7 +102,16 @@ public function preflight($type, $parent) // Only proceed if Joomla version is correct if(version_compare(JVERSION, '4.0.0', '<')) { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_JOOMLA_COMPATIBILITY', '4.x', '4.x'), 'error'); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_JOOMLA_COMPATIBILITY', '4.x', JVERSION), 'error'); + + return false; + } + + // Only proceed if it is not an incompatible Joomla version + $jversion = explode('-', JVERSION); + if(in_array($jversion[0], $this->incompatible)) + { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_JOOMLA_COMPATIBILITY', '4.x', JVERSION), 'error'); return false; } @@ -103,7 +119,7 @@ public function preflight($type, $parent) // Only proceed if PHP version is correct if(version_compare(PHP_VERSION, $this->minPhp, '<=')) { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_PHP_COMPATIBILITY', '4.x', '7.3', $this->minPhp), 'error'); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_JOOMGALLERY_ERROR_PHP_COMPATIBILITY', '4.x', '7.4', $this->minPhp), 'error'); return false; } @@ -136,7 +152,7 @@ public function preflight($type, $parent) { // save release code information //------------------------------- - if (File::exists(JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR.'joomgallery.xml')) + if(File::exists(JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR.'joomgallery.xml')) { $xml = simplexml_load_file(JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR.'joomgallery.xml'); $this->act_code = $xml->version; @@ -163,11 +179,12 @@ public function preflight($type, $parent) } } - // copy old XML file (JGv1-3) - $xml_path = JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR; + // copy old XML file (JGv1-3) to temp folder + $xml_path = JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR; + $tmp_folder = Factory::getApplication()->get('tmp_path'); if(File::exists($xml_path.'joomgallery.xml')) { - File::copy($xml_path.'joomgallery.xml', $xml_path.'joomgallery_old.xml'); + File::copy($xml_path.'joomgallery.xml', $tmp_folder.DIRECTORY_SEPARATOR.'joomgallery_old.xml'); } // remove old JoomGallery files and folders @@ -417,6 +434,17 @@ function postflight($type, $parent) { $app = Factory::getApplication(); + if($this->fromOldJG) + { + // copy old XML file (JGv1-3) back from temp folder + $xml_path = JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR; + $tmp_folder = Factory::getApplication()->get('tmp_path'); + if(File::exists($tmp_folder.DIRECTORY_SEPARATOR.'joomgallery_old.xml')) + { + File::copy($tmp_folder.DIRECTORY_SEPARATOR.'joomgallery_old.xml', $xml_path.'joomgallery_old.xml'); + } + } + // Create default Category if(!$this->addDefaultCategory()) { @@ -533,10 +561,16 @@ public function addDefaultCategory() $db = Factory::getContainer()->get(DatabaseInterface::class); // Load JoomTableTrait - $trait_path = JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'Table'.DIRECTORY_SEPARATOR.'JoomTableTrait.php'; - $traitClass = '\\Joomgallery\\Component\\Joomgallery\\Administrator\\Table\\JoomTableTrait'; + $joomtabletrait_path = JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'Table'.DIRECTORY_SEPARATOR.'JoomTableTrait.php'; + $joomtabletraitClass = '\\Joomgallery\\Component\\Joomgallery\\Administrator\\Table\\JoomTableTrait'; - require_once $trait_path; + require_once $joomtabletrait_path; + + // Load MigrationTableTrait + $migrationtabletrait_path = JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'Table'.DIRECTORY_SEPARATOR.'MigrationTableTrait.php'; + $migrationtabletraitClass = '\\Joomgallery\\Component\\Joomgallery\\Administrator\\Table\\MigrationTableTrait'; + + require_once $migrationtabletrait_path; // Load CategoryTable $class_path = JPATH_ADMINISTRATOR.DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR.'com_joomgallery'.DIRECTORY_SEPARATOR.'src'.DIRECTORY_SEPARATOR.'Table'.DIRECTORY_SEPARATOR.'CategoryTable.php';