plugin = $plugin; $this->settings = $settings; $this->twig = $twig; $this->logData = ''; } protected function log(string $message): void { // todo enable when testing upgrades (manually) //$this->logData .= $message . "\r\n"; } protected function isVersionLower(string $version, string $targetVersion): bool { // empty means the very first run, no data is available, nothing to fix if ('' === $version) { return false; } $currentVersion = explode('.', $version); $targetVersion = explode('.', $targetVersion); // versions are broken if (3 !== count($currentVersion) || 3 !== count($targetVersion)) { return false; } //// convert to int foreach ($currentVersion as &$part) { $part = (int)$part; } foreach ($targetVersion as &$part) { $part = (int)$part; } //// compare // major if ($currentVersion[0] > $targetVersion[0]) { return false; } elseif ($currentVersion[0] < $targetVersion[0]) { return true; } // minor if ($currentVersion[1] > $targetVersion[1]) { return false; } elseif ($currentVersion[1] < $targetVersion[1]) { return true; } // patch if ($currentVersion[2] >= $targetVersion[2]) { return false; } return true; } protected function moveViewAndCardMetaToPostContentJson(): void { $queryArgs = [ 'post_type' => [ViewsCpt::NAME, CardsCpt::NAME,], 'post_status' => ['publish', 'draft', 'trash',], 'posts_per_page' => -1, ]; $query = new WP_Query($queryArgs); /** * @var WP_Post[] $myPosts */ $myPosts = $query->get_posts(); global $wpdb; foreach ($myPosts as $myPost) { $postId = $myPost->ID; $data = ViewsCpt::NAME === $myPost->post_type ? $this->viewsDataStorage->get($myPost->post_name) : $this->cardsDataStorage->get($myPost->post_name); $data->load($myPost->ID); if (ViewsCpt::NAME === $myPost->post_type) { $this->viewsDataStorage->save($data); } else { $this->cardsDataStorage->save($data); } $wpdb->delete($wpdb->prefix . 'postmeta', [ 'post_id' => $postId, ]); } } protected function moveOptionsToSettings(): void { $license = get_option(Options::PREFIX . 'license', ''); $license = is_string($license) || is_numeric($license) ? (string)$license : ''; $licenceExpiration = get_option(Options::PREFIX . 'license_expiration', ''); $licenceExpiration = is_string($licenceExpiration) ? $licenceExpiration : ''; $demoImport = get_option(Options::PREFIX . 'demo_import', []); $demoImport = is_array($demoImport) ? $demoImport : []; $this->settings->setLicense($license); $this->settings->setLicenseExpiration($licenceExpiration); $this->settings->setDemoImport($demoImport); $this->settings->save(); //// delete_option(Options::PREFIX . 'license'); delete_option(Options::PREFIX . 'license_expiration'); delete_option(Options::PREFIX . 'demo_import'); } // it was for 1.5.10, when versions weren't available protected function firstRun(): bool { // skip upgrading as hook won't be fired and data is not available if (!$this->plugin->isAcfPluginAvailable()) { return false; } add_action('acf/init', function () { $this->moveViewAndCardMetaToPostContentJson(); $this->moveOptionsToSettings(); }); return true; } protected function fixMultipleSlashesInPostContentJson(): void { global $wpdb; // don't use 'get_post($id)->post_content' / 'wp_update_post()' // to avoid the kses issue https://core.trac.wordpress.org/ticket/38715 $myPosts = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_type IN (%s,%s) AND post_content != ''", ViewsCpt::NAME, CardsCpt::NAME ) ); // direct $wpdb queries return strings for int columns, wrap into get_post to get right types /** * @var WP_Post[] $myPosts */ $myPosts = array_map(function ($myPost) { return get_post($myPost->ID); }, $myPosts); foreach ($myPosts as $myPost) { $content = str_replace('\\\\\\', '\\', $myPost->post_content); $wpdb->update($wpdb->posts, ['post_content' => $content], ['ID' => $myPost->ID]); } } protected function replacePostIdentifiers(): void { global $wpdb; $queryForThumbnail = "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, '\$post\$|_thumbnail_id', '\$post\$|_post_thumbnail') WHERE post_type = 'acf_views'"; $queryForThumbnailLink = "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, '\$post\$|_thumbnail_id_link', '\$post\$|_post_thumbnail_link') WHERE post_type = 'acf_views'"; $res1 = $wpdb->get_results($queryForThumbnail); $res2 = $wpdb->get_results($queryForThumbnailLink); $this->log('replacePostIdentifiers, Basic.' . print_r($res1, true) . print_r($res2, true)); } /** * @return WP_Post[] */ protected function getAllViews(): array { $queryArgs = [ 'post_type' => ViewsCpt::NAME, 'posts_per_page' => -1, 'post_status' => ['publish', 'draft', 'trash',], ]; $query = new WP_Query($queryArgs); /** * @var WP_Post[] */ return $query->posts; } /** * @return WP_Post[] */ protected function getAllCards(): array { $queryArgs = [ 'post_type' => CardsCpt::NAME, 'posts_per_page' => -1, 'post_status' => ['publish', 'draft', 'trash',], ]; $query = new WP_Query($queryArgs); /** * @var WP_Post[] */ return $query->posts; } protected function triggerSaveForAllViews(): int { $posts = $this->getAllViews(); foreach ($posts as $post) { $this->acfViewsSaveActions->performSaveActions($post->ID); } return count($posts); } protected function triggerSaveForAllCards(): int { $posts = $this->getAllCards(); foreach ($posts as $post) { $this->acfCardsSaveActions->performSaveActions($post->ID); } return count($posts); } protected function replaceViewIdToUniqueIdInView(ViewData $acfViewData): bool { $isChanged = false; foreach ($acfViewData->items as $item) { $oldId = $item->field->acfViewId; if ('' === $oldId) { continue; } $uniqueId = get_post((int)$oldId)->post_name ?? ''; $isChanged = true; $item->field->acfViewId = $uniqueId; } return $isChanged; } /** * @throws Exception */ protected function disableWebComponentsForExistingViewsAndCards(): void { $cptDataItems = array_merge($this->getAllViews(), $this->getAllCards()); foreach ($cptDataItems as $cptDataItem) { $cptData = ViewsCpt::NAME === $cptDataItem->post_type ? $this->viewsDataStorage->get($cptDataItem->post_name) : $this->cardsDataStorage->get($cptDataItem->post_name); $cptData->isWithoutWebComponent = true; if (ViewsCpt::NAME === $cptDataItem->post_type) { $this->viewsDataStorage->save($cptData); } else { $this->cardsDataStorage->save($cptData); } } } protected function setupLightBoxSimpleFromOldCheckbox(): void { $views = $this->getAllViews(); foreach ($views as $view) { $viewData = $this->viewsDataStorage->get($view->post_name); $isWithChange = false; foreach ($viewData->items as $item) { foreach ($item->repeaterFields as $repeaterField) { if (!$repeaterField->galleryWithLightBox) { continue; } $repeaterField->lightboxType = 'simple'; $isWithChange = true; } if ($item->field->galleryWithLightBox) { $item->field->lightboxType = 'simple'; $isWithChange = true; } } if (!$isWithChange) { continue; } $this->viewsDataStorage->save($viewData); } } protected function replacePostCommentsAndMenuLinkFieldsToSeparate(): void { $views = $this->getAllViews(); $oldCommentsKey = FieldData::createFieldKey(PostFields::GROUP_NAME, '_post_comments'); $newCommentsKey = FieldData::createFieldKey(CommentItemFields::GROUP_NAME, CommentItemFields::FIELD_LIST); $oldMenuLinkKey = FieldData::createFieldKey(MenuFields::GROUP_NAME, '_menu_link'); $newMenuLinkKey = FieldData::createFieldKey(MenuItemFields::GROUP_NAME, MenuItemFields::FIELD_LINK); foreach ($views as $view) { $viewData = $this->viewsDataStorage->get($view->post_name); $isWithChange = false; foreach ($viewData->items as $item) { $newKey = ''; $newGroup = ''; switch ($item->field->key) { case $oldCommentsKey: $newKey = $newCommentsKey; $newGroup = CommentItemFields::GROUP_NAME; break; case $oldMenuLinkKey: $newKey = $newMenuLinkKey; $newGroup = MenuItemFields::GROUP_NAME; break; } if ('' === $newKey) { continue; } $isWithChange = true; $item->field->key = $newKey; $item->group = $newGroup; } if (!$isWithChange) { continue; } $this->viewsDataStorage->save($viewData); } } protected function enableNameBackCompatibilityCheckboxForViewsWithGutenberg(): void { $views = $this->getAllViews(); foreach ($views as $view) { $viewData = $this->viewsDataStorage->get($view->post_name); if (!$viewData->isHasGutenbergBlock) { continue; } $viewData->isGutenbergBlockWithDigitalId = true; $this->viewsDataStorage->save($viewData); } } protected function upgradeGlobalItems(string $previousVersion): void { // NOTE: do not call methods directly (only via init or other hooks) // some plugins, like WPFastestCache can use global functions, which won't be defined yet if ('1.6.0' === $previousVersion) { $this->fixMultipleSlashesInPostContentJson(); } // this early call is necessary, as now the storage expects uniqueId, not postId. // so without it even code made for older versions upgrade (like 1.7.0) won't work if ($this->isVersionLower($previousVersion, '2.2.0')) { add_action('acf/init', [$this, 'recreatePostSlugs',]); } if ($this->isVersionLower($previousVersion, '1.7.0')) { add_action('acf/init', [$this, 'recreatePostSlugs',]); add_action('acf/init', [$this, 'updateMarkupIdentifiers']); } // twig markup if ($this->isVersionLower($previousVersion, '2.0.0')) { $this->log('Upgrading to 2.0.0, Basic'); $this->replacePostIdentifiers(); // trigger save to refresh the markup preview add_action('acf/init', function () { $viewsCount = $this->triggerSaveForAllViews(); $cardsCount = $this->triggerSaveForAllCards(); $this->log('triggered save for all Views, Basic.' . $viewsCount); $this->log('triggered save for all Cards, Basic.' . $cardsCount); }); } if ($this->isVersionLower($previousVersion, '2.1.0')) { add_action('acf/init', [$this, 'enableWithCommonClassesAndUnnecessaryWrappersForAllViews']); } if ($this->isVersionLower($previousVersion, '2.2.0')) { add_action('acf/init', [$this, 'replaceViewIdToUniqueIdInCards',]); add_action('acf/init', [$this, 'replaceViewIdToUniqueIdInViewRelationships',]); } if ($this->isVersionLower($previousVersion, '2.2.2')) { add_action('acf/init', [$this, 'setDigitalIdForMarkupFlagForViewsAndCards']); } if ($this->isVersionLower($previousVersion, '2.2.3')) { // related Views/Cards in post_content_filtered appeared, filled during the save action add_action('acf/init', function () { $this->triggerSaveForAllViews(); $this->triggerSaveForAllCards(); }); } if ($this->isVersionLower($previousVersion, '2.3.0')) { add_action('init', function () { $this->twig->createTemplatesDir(); }); } if ($this->isVersionLower($previousVersion, '2.4.0')) { add_action('acf/init', function () { $this->disableWebComponentsForExistingViewsAndCards(); // add acf-views-masonry CSS to the Views' CSS $this->triggerSaveForAllViews(); $this->setupLightBoxSimpleFromOldCheckbox(); }); } if ($this->isVersionLower($previousVersion, '2.4.2')) { add_action('acf/init', function () { $this->replacePostCommentsAndMenuLinkFieldsToSeparate(); }); } if ($this->isVersionLower($previousVersion, '2.4.5')) { add_action('acf/init', function () { $this->enableNameBackCompatibilityCheckboxForViewsWithGutenberg(); }); } if ($this->isVersionLower($previousVersion, '3.0.0')) { // theme is loaded since this hook add_action('acf/init', function () { $this->fillUniqueIdAndPostTitleInJson(); $this->removeOldThemeLabelsFolder(); $this->putNewDefaultInfoExistingEmptyPhpVariableField(); $this->putNewDefaultInfoExistingEmptyQueryArgsField(); $this->replaceGutenbergCheckboxWithSelect(); }); } // NOTE: when you add new upgrade, you should use 'after_setup_theme' hook if the acf plugin isn't available // (as ACF isn't a direct dependency now). // You shouldn't use 'after_setup_theme' if acf is available, as this hooks is fired before 'acf/init', so it'll // miss upgrades for the previous versions } public function setDependencies( ViewsDataStorage $viewsDataStorage, CardsDataStorage $cardsDataStorage, ViewsCptSaveActions $acfViewsSaveActions, CardsCptSaveActions $acfCardsSaveActions ): void { $this->viewsDataStorage = $viewsDataStorage; $this->cardsDataStorage = $cardsDataStorage; $this->acfViewsSaveActions = $acfViewsSaveActions; $this->acfCardsSaveActions = $acfCardsSaveActions; } /** * @throws Exception */ public function setDigitalIdForMarkupFlagForViewsAndCards(): void { $queryArgs = [ 'post_type' => [ViewsCpt::NAME, CardsCpt::NAME,], 'post_status' => ['publish', 'draft', 'trash',], 'posts_per_page' => -1, ]; $query = new WP_Query($queryArgs); /** * @var WP_Post[] $posts */ $posts = $query->get_posts(); foreach ($posts as $post) { $cptData = ViewsCpt::NAME === $post->post_type ? $this->viewsDataStorage->get($post->post_name) : $this->cardsDataStorage->get($post->post_name); $cptData->isMarkupWithDigitalId = true; if (ViewsCpt::NAME === $post->post_type) { $this->viewsDataStorage->save($cptData); } else { $this->cardsDataStorage->save($cptData); } } } public function recreatePostSlugs(): void { $queryArgs = [ 'post_type' => [ViewsCpt::NAME, CardsCpt::NAME,], 'post_status' => ['publish', 'draft', 'trash',], 'posts_per_page' => -1, ]; $query = new WP_Query($queryArgs); /** * @var WP_Post[] $posts */ $posts = $query->get_posts(); foreach ($posts as $post) { $prefix = ViewsCpt::NAME === $post->post_type ? ViewData::UNIQUE_ID_PREFIX : CardData::UNIQUE_ID_PREFIX; $postName = uniqid($prefix); wp_update_post([ 'ID' => $post->ID, 'post_name' => $postName, ]); // to make sure ids are unique (uniqid based on the time) usleep(1); } } public function replaceViewIdToUniqueIdInCards(): void { $query = new WP_Query([ 'post_type' => CardsCpt::NAME, 'post_status' => ['publish', 'draft', 'trash',], 'posts_per_page' => -1, ]); /** * @var WP_Post[] $acfCards */ $acfCards = $query->get_posts(); foreach ($acfCards as $acfCard) { $acfCardData = $this->cardsDataStorage->get($acfCard->post_name); $oldViewId = $acfCardData->acfViewId; if ('' === $oldViewId) { continue; } $acfCardData->acfViewId = get_post((int)$oldViewId)->post_name ?? ''; $this->cardsDataStorage->save($acfCardData); } } public function replaceViewIdToUniqueIdInViewRelationships(): void { $query = new WP_Query([ 'post_type' => ViewsCpt::NAME, 'post_status' => ['publish', 'draft', 'trash',], 'posts_per_page' => -1, ]); /** * @var WP_Post[] $acfViews */ $acfViews = $query->get_posts(); foreach ($acfViews as $acfView) { $acfViewData = $this->viewsDataStorage->get($acfView->post_name); if (!$this->replaceViewIdToUniqueIdInView($acfViewData)) { continue; } $this->viewsDataStorage->save($acfViewData); } } /** * @throws Exception */ public function enableWithCommonClassesAndUnnecessaryWrappersForAllViews(): void { $queryArgs = [ 'post_type' => ViewsCpt::NAME, 'post_status' => ['publish', 'draft', 'trash',], 'posts_per_page' => -1, ]; $query = new WP_Query($queryArgs); /** * @var WP_Post[] $posts */ $posts = $query->posts; foreach ($posts as $post) { $acfViewData = $this->viewsDataStorage->get($post->post_name); $acfViewData->isWithCommonClasses = true; $acfViewData->isWithUnnecessaryWrappers = true; $this->acfViewsSaveActions->performSaveActions($post->ID); } } /** * @throws Exception */ public function updateMarkupIdentifiers(): void { $queryArgs = [ 'post_type' => ViewsCpt::NAME, 'post_status' => ['publish', 'draft', 'trash',], 'posts_per_page' => -1, ]; $query = new WP_Query($queryArgs); /** * @var WP_Post[] $posts */ $posts = $query->posts; foreach ($posts as $post) { $acfViewData = $this->viewsDataStorage->get($post->post_name); // replace identifiers for Views without Custom Markup if ('' === trim($acfViewData->customMarkup) && '' !== $acfViewData->cssCode) { foreach ($acfViewData->items as $item) { $oldClass = '.' . $item->field->id; $newClass = '.acf-view__' . $item->field->id; $acfViewData->cssCode = str_replace($oldClass . ' ', $newClass . ' ', $acfViewData->cssCode); $acfViewData->cssCode = str_replace($oldClass . '{', $newClass . '{', $acfViewData->cssCode); $acfViewData->cssCode = str_replace($oldClass . ',', $newClass . ',', $acfViewData->cssCode); foreach ($item->repeaterFields as $repeaterField) { $oldClass = '.' . $repeaterField->id; $newClass = '.acf-view__' . $repeaterField->id; $acfViewData->cssCode = str_replace($oldClass . ' ', $newClass . ' ', $acfViewData->cssCode); $acfViewData->cssCode = str_replace($oldClass . '{', $newClass . '{', $acfViewData->cssCode); $acfViewData->cssCode = str_replace($oldClass . ',', $newClass . ',', $acfViewData->cssCode); } } // don't call the 'saveToPostContent()' method, as it'll be called in the 'performSaveActions()' method } // update markup field for all $this->acfViewsSaveActions->performSaveActions($post->ID); } } public function removeOldThemeLabelsFolder(): void { $labelsDir = get_stylesheet_directory() . '/acf-views-labels'; if (false === is_dir($labelsDir)) { return; } $this->viewsDataStorage->getFileSystem()->removeFolderRecursively($labelsDir); } public function putNewDefaultInfoExistingEmptyPhpVariableField(): void { $viewPosts = $this->getAllViews(); foreach ($viewPosts as $viewPost) { $viewData = $this->viewsDataStorage->get($viewPost->post_name); if ('' !== trim($viewData->phpVariables)) { continue; } $viewData->phpVariables = ' */ public function getVariables(): array { return [ // "custom_variable" => get_post_meta($this->objectId, "your_field", true), // "another_var" => $this->customArguments["another"] ?? "", ]; } /** * @return array */ public function getVariablesForValidation(): array { // it\'s better to return dummy data here [ "another_var" => "dummy string", ] return $this->getVariables(); } }; '; $this->viewsDataStorage->save($viewData); } } public function putNewDefaultInfoExistingEmptyQueryArgsField(): void { $cardPosts = $this->getAllCards(); foreach ($cardPosts as $cardPost) { $cardData = $this->cardsDataStorage->get($cardPost->post_name); if ('' !== trim($cardData->extraQueryArguments)) { continue; } $cardData->extraQueryArguments = ' */ public function getVariables(): array { return [ // "another_var" => $this->customArguments["another"] ?? "", ]; } /** * @return array */ public function getVariablesForValidation(): array { // it\'s better to return dummy data here [ "another_var" => "dummy string", ] return $this->getVariables(); } public function getQueryArguments(): array { // https://developer.wordpress.org/reference/classes/wp_query/#parameters return [ // "author" => get_current_user_id(), // "post_parent" => $this->customArguments["post_parent"] ?? 0, ]; } }; '; $this->cardsDataStorage->save($cardData); } } public function replaceGutenbergCheckboxWithSelect(): void { $viewPosts = $this->getAllViews(); foreach ($viewPosts as $viewPost) { $viewData = $this->viewsDataStorage->get($viewPost->post_name); if (false === $viewData->isHasGutenbergBlock) { continue; } $viewData->gutenbergBlockVendor = 'acf'; $this->viewsDataStorage->save($viewData); } } public function fillUniqueIdAndPostTitleInJson(): void { $views = $this->getAllViews(); $cards = $this->getAllCards(); $cptPosts = array_merge($views, $cards); foreach ($cptPosts as $cptPost) { $cptData = ViewsCpt::NAME === $cptPost->post_type ? $this->viewsDataStorage->get($cptPost->post_name) : $this->cardsDataStorage->get($cptPost->post_name); // todo should it be removed? $cptData->loadFromPostContent($cptPost->ID); $cptData->uniqueId = $cptPost->post_name; $cptData->title = $cptPost->post_title; if (ViewsCpt::NAME === $cptPost->post_type) { $this->viewsDataStorage->save($cptData); } else { $this->cardsDataStorage->save($cptData); } } } public function upgradeImportedItem(string $previousVersion, CptData $cptData): void { // to be filled in the future } public function performUpgrade(): void { // all versions since 1.6.0 has a version $previousVersion = $this->settings->getVersion(); // empty means the very first run, no data is available, nothing to fix if ('' === $previousVersion) { return; } $this->upgradeGlobalItems($previousVersion); $this->settings->setVersion($this->plugin->getVersion()); $this->settings->save(); } public function setHooks(CurrentScreen $currentScreen): void { // don't use 'upgrader_process_complete' hook, as user can update the plugin manually by FTP $dbVersion = $this->settings->getVersion(); $codeVersion = $this->plugin->getVersion(); // run upgrade if version in the DB is different from the code version if ($dbVersion !== $codeVersion) { // only at this hook can be sure that other plugin's functions are available add_action('plugins_loaded', [$this, 'performUpgrade']); /*add_action('admin_init', function () { file_put_contents(__DIR__ . '/log.txt', $this->logData); });*/ } } }