checkDuplicateDoiRegistrationAgencies(); } catch (Throwable $e) { if ($fallbackVersion = $this->setFallbackVersion()) { $this->_installer->log("A pre-flight check failed. The software was successfully upgraded to {$fallbackVersion} but could not be upgraded further (to " . $this->_installer->newVersion->getVersionString() . '). Check and correct the error, then try again.'); } throw $e; } } protected function getContextTable(): string { return 'journals'; } protected function getContextKeyField(): string { return 'journal_id'; } protected function getContextSettingsTable(): string { return 'journal_settings'; } protected function buildOrphanedEntityProcessor(): void { parent::buildOrphanedEntityProcessor(); $this->addTableProcessor('issues', function (): int { $affectedRows = 0; // Depends directly on ~2 entities: doi_id->dois.doi_id(not found in previous version) journal_id->journals.journal_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('issues', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); return $affectedRows; }); // Shared processor (there's another handler for this table at pkp-lib) $this->addTableProcessor('publications', function (): int { $affectedRows = 0; // Depends directly on ~4 entities: primary_contact_id->authors.author_id doi_id->dois.doi_id(not found in previous version) section_id->sections.section_id submission_id->submissions.submission_id // Custom field (not found in at least one of the softwares) // Attempts to recover the field publications.section_id before discarding the entry $rows = DB::table('publications AS p') ->leftJoin('sections AS s', 's.section_id', '=', 'p.section_id') ->join('submissions AS sub', 'sub.submission_id', '=', 'p.submission_id') ->whereNull('s.section_id') ->select('p.submission_id', 'p.publication_id', 'p.section_id') ->selectSub( fn (Builder $q) => $q ->from('sections AS s') ->where('s.is_inactive', '=', 0) ->whereColumn('s.journal_id', '=', 'sub.context_id') ->selectRaw('MIN(s.section_id)'), 'new_section_id' ) ->get(); foreach ($rows as $row) { $this->_installer->log("The publication ID ({$row->publication_id}) for the submission ID {$row->submission_id} is assigned to an invalid section ID \"{$row->section_id}\", its section will be updated to {$row->new_section_id}"); $affectedRows += DB::table('publications')->where('publication_id', '=', $row->publication_id)->update(['section_id' => $row->new_section_id]); } $affectedRows += $this->deleteOptionalReference('publications', 'section_id', 'sections', 'section_id'); // Remaining cleanups are inherited return $affectedRows; }); $this->addTableProcessor('publication_galleys', function (): int { $affectedRows = 0; // Depends directly on ~3 entities: doi_id->dois.doi_id(not found in previous version) publication_id->publications.publication_id submission_file_id->submission_files.submission_file_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('publication_galleys', 'publication_id', 'publications', 'publication_id'); // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteOptionalReference('publication_galleys', 'submission_file_id', 'submission_files', 'submission_file_id'); return $affectedRows; }); $this->addTableProcessor('issue_galleys', function (): int { $affectedRows = 0; // Depends directly on ~2 entities: file_id->issue_files.file_id issue_id->issues.issue_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('issue_galleys', 'issue_id', 'issues', 'issue_id'); // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('issue_galleys', 'file_id', 'issue_files', 'file_id'); return $affectedRows; }); $this->addTableProcessor('sections', function (): int { $affectedRows = 0; // Depends directly on ~2 entities: journal_id->journals.journal_id review_form_id->review_forms.review_form_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('sections', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); // Custom field (not found in at least one of the softwares) $affectedRows += $this->cleanOptionalReference('sections', 'review_form_id', 'review_forms', 'review_form_id'); return $affectedRows; }); $this->addTableProcessor('subscription_types', function (): int { $affectedRows = 0; // Depends directly on ~1 entities: journal_id->journals.journal_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('subscription_types', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); return $affectedRows; }); $this->addTableProcessor('issue_files', function (): int { $affectedRows = 0; // Depends directly on ~1 entities: issue_id->issues.issue_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('issue_files', 'issue_id', 'issues', 'issue_id'); return $affectedRows; }); $this->addTableProcessor('subscriptions', function (): int { $affectedRows = 0; // Depends directly on ~3 entities: journal_id->journals.journal_id type_id->subscription_types.type_id user_id->users.user_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('subscriptions', 'user_id', 'users', 'user_id'); // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('subscriptions', 'type_id', 'subscription_types', 'type_id'); // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('subscriptions', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); return $affectedRows; }); $this->addTableProcessor('completed_payments', function (): int { $affectedRows = 0; // Depends directly on ~2 entities: context_id->journals.journal_id user_id->users.user_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('completed_payments', 'context_id', $this->getContextTable(), $this->getContextKeyField()); // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteOptionalReference('completed_payments', 'user_id', 'users', 'user_id'); return $affectedRows; }); $this->addTableProcessor('custom_issue_orders', function (): int { $affectedRows = 0; // Depends directly on ~2 entities: issue_id->issues.issue_id journal_id->journals.journal_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('custom_issue_orders', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('custom_issue_orders', 'issue_id', 'issues', 'issue_id'); return $affectedRows; }); $this->addTableProcessor('custom_section_orders', function (): int { $affectedRows = 0; // Depends directly on ~2 entities: issue_id->issues.issue_id section_id->sections.section_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('custom_section_orders', 'section_id', 'sections', 'section_id'); // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('custom_section_orders', 'issue_id', 'issues', 'issue_id'); return $affectedRows; }); $this->addTableProcessor('institutional_subscriptions', function (): int { $affectedRows = 0; // Depends directly on ~2 entities: institution_id->institutions.institution_id(not found in previous version) subscription_id->subscriptions.subscription_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('institutional_subscriptions', 'subscription_id', 'subscriptions', 'subscription_id'); return $affectedRows; }); $this->addTableProcessor('issue_galley_settings', function (): int { $affectedRows = 0; // Depends directly on ~1 entities: galley_id->issue_galleys.galley_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('issue_galley_settings', 'galley_id', 'issue_galleys', 'galley_id'); return $affectedRows; }); $this->addTableProcessor('issue_settings', function (): int { $affectedRows = 0; // Depends directly on ~1 entities: issue_id->issues.issue_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('issue_settings', 'issue_id', 'issues', 'issue_id'); return $affectedRows; }); $this->addTableProcessor('publication_galley_settings', function (): int { $affectedRows = 0; // Depends directly on ~1 entities: galley_id->publication_galleys.galley_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('publication_galley_settings', 'galley_id', 'publication_galleys', 'galley_id'); return $affectedRows; }); $this->addTableProcessor('section_settings', function (): int { $affectedRows = 0; // Depends directly on ~1 entities: section_id->sections.section_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('section_settings', 'section_id', 'sections', 'section_id'); return $affectedRows; }); $this->addTableProcessor('subscription_type_settings', function (): int { $affectedRows = 0; // Depends directly on ~1 entities: type_id->subscription_types.type_id // Custom field (not found in at least one of the softwares) $affectedRows += $this->deleteRequiredReference('subscription_type_settings', 'type_id', 'subscription_types', 'type_id'); return $affectedRows; }); // Support for the issueId setting $this->addTableProcessor('publication_settings', function (): int { $affectedRows = 0; $rows = DB::table('publications AS p') ->join('publication_settings AS ps', 'ps.publication_id', '=', 'p.publication_id') ->leftJoin('issues AS i', DB::raw('CAST(i.issue_id AS CHAR(20))'), '=', 'ps.setting_value') ->where('ps.setting_name', 'issueId') ->whereNull('i.issue_id') ->get(['p.submission_id', 'p.publication_id', 'ps.setting_value']); foreach ($rows as $row) { $this->_installer->log("The publication ID ({$row->publication_id}) for the submission ID {$row->submission_id} is assigned to an invalid issue ID \"{$row->setting_value}\", its value will be updated to NULL"); $affectedRows += DB::table('publication_settings') ->where('publication_id', '=', $row->publication_id) ->where('setting_name', 'issueId') ->where('setting_value', $row->setting_value) ->delete(); } return $affectedRows; }); } protected function getEntityRelationships(): array { return [ $this->getContextTable() => ['submissions', 'issues', 'user_groups', 'sections', 'categories', 'subscription_types', 'navigation_menu_items', 'genres', 'filters', 'announcement_types', 'subscriptions', 'notifications', 'navigation_menus', 'library_files', 'email_templates', 'user_group_stage', 'subeditor_submission_group', 'plugin_settings', 'notification_subscription_settings', $this->getContextSettingsTable(), 'custom_issue_orders', 'completed_payments'], 'users' => ['submission_files', 'review_assignments', 'subscriptions', 'notifications', 'event_log', 'email_log', 'user_user_groups', 'user_settings', 'user_interests', 'temporary_files', 'submission_comments', 'subeditor_submission_group', 'stage_assignments', 'sessions', 'query_participants', 'notification_subscription_settings', 'notes', 'email_log_users', 'edit_decisions', 'completed_payments', 'access_keys'], 'submissions' => ['submission_files', 'publications', 'review_rounds', 'review_assignments', 'submission_search_objects', 'library_files', 'submission_settings', 'submission_comments', 'stage_assignments', 'review_round_files', 'edit_decisions'], 'submission_files' => ['submission_files', 'publication_galleys', 'submission_file_settings', 'submission_file_revisions', 'review_round_files', 'review_files'], // publication_settings dependency added manually 'issues' => [$this->getContextTable(), 'issue_galleys', 'issue_files', 'issue_settings', 'custom_section_orders', 'custom_issue_orders', 'publication_settings'], 'user_groups' => ['authors', 'user_user_groups', 'user_group_stage', 'user_group_settings', 'subeditor_submission_group', 'stage_assignments'], 'publications' => ['submissions', 'publication_galleys', 'authors', 'citations', 'publication_settings', 'publication_categories'], 'publication_galleys' => ['publication_galley_settings'], 'review_forms' => ['sections', 'review_form_elements', 'review_assignments', 'review_form_settings'], 'categories' => ['categories', 'publication_categories', 'category_settings'], 'issue_galleys' => ['issue_galley_settings'], 'sections' => ['publications', 'section_settings', 'custom_section_orders'], 'review_rounds' => ['review_assignments', 'review_round_files', 'edit_decisions'], 'navigation_menu_item_assignments' => ['navigation_menu_item_assignments', 'navigation_menu_item_assignment_settings'], 'authors' => ['publications', 'author_settings'], 'controlled_vocab_entries' => ['user_interests', 'controlled_vocab_entry_settings'], 'data_object_tombstones' => ['data_object_tombstone_settings', 'data_object_tombstone_oai_set_objects'], 'files' => ['submission_files', 'submission_file_revisions'], 'filters' => ['filters', 'filter_settings'], 'genres' => ['submission_files', 'genre_settings'], 'announcement_types' => ['announcements', 'announcement_type_settings'], 'navigation_menu_items' => ['navigation_menu_item_assignments', 'navigation_menu_item_settings'], 'review_assignments' => ['review_form_responses', 'review_files'], 'review_form_elements' => ['review_form_responses', 'review_form_element_settings'], 'subscription_types' => ['subscriptions', 'subscription_type_settings'], 'announcements' => ['announcement_settings'], 'queries' => ['query_participants'], 'navigation_menus' => ['navigation_menu_item_assignments'], 'notifications' => ['notification_settings'], 'filter_groups' => ['filters'], 'event_log' => ['event_log_settings'], 'email_templates' => ['email_templates_settings'], 'static_pages' => ['static_page_settings'], 'email_log' => ['email_log_users'], 'submission_search_keyword_list' => ['submission_search_object_keywords'], 'submission_search_objects' => ['submission_search_object_keywords'], 'controlled_vocabs' => ['controlled_vocab_entries'], 'library_files' => ['library_file_settings'], 'subscriptions' => ['institutional_subscriptions'], 'citations' => ['citation_settings'], 'issue_files' => ['issue_galleys'] ]; } protected function dropForeignKeys(): void { parent::dropForeignKeys(); if (DB::getDoctrineSchemaManager()->introspectTable('publication_galleys')->hasForeignKey('publication_galleys_submission_file_id_foreign')) { Schema::table('publication_galleys', fn (Blueprint $table) => $table->dropForeign('publication_galleys_submission_file_id_foreign')); } } /** * Checks if DOIs have been marked registered with more than one registration agency. * * @throws Exception */ protected function checkDuplicateDoiRegistrationAgencies(): void { $agencies = ['crossref::status', 'datacite::status', 'medra::status']; $submissionIds = DB::table('submission_settings') ->whereIn('setting_name', $agencies) ->groupBy('submission_id') ->havingRaw('COUNT(submission_id) > 1') ->select(['submission_id']) ->get(); $galleyIds = DB::table('publication_galley_settings') ->whereIn('setting_name', $agencies) ->groupBy('galley_id') ->havingRaw('COUNT(galley_id) > 1') ->select(['galley_id']) ->get(); $issueIds = DB::table('issue_settings') ->whereIn('setting_name', $agencies) ->groupBy('issue_id') ->havingRaw('COUNT(issue_id) > 1') ->select(['issue_id']) ->get(); if ($submissionIds->count() > 0 || $galleyIds->count() > 0 || $issueIds->count() > 0) { throw new Exception('Some DOIs have been registered with multiple registration agencies. Resolve duplicates before continuing by running `php tools/resolveAgencyDuplicates.php`.'); } } }