Already exists? if ($skipExistenceCheck === null && \is_dir($folder)) { return \true; } if (\wp_mkdir_p($folder)) { // Create the anonymous files require_once ABSPATH . 'wp-admin/includes/file.php'; // As we create the folder in `wp-content`, we could have wrong permission for the created // anonymous folder due to `wp_mkdir_p()` as it inherits the `chmod` from the parent folder UtilsUtils::runDirectFilesystem(function ($fs) use($hashFolderPath, $folder) { /** * WP_Filesystem_Direct. * * @var WP_Filesystem_Direct */ $fs = $fs; $fs->chmod($hashFolderPath, \constant('FS_CHMOD_DIR')); $fs->chmod($folder, \constant('FS_CHMOD_DIR')); }); $filesToCopy = \array_filter(\list_files($this->getFolder(), 1), function ($file) { $extension = \pathinfo($file, \PATHINFO_EXTENSION); return \in_array($extension, self::COPY_EXTENSIONS, \true); }); foreach ($filesToCopy as $fileToCopy) { $filename = $folder . self::generateFilename($hash, $fileToCopy); \file_put_contents($filename, Utils::readFileAndCorrectSourceMap($fileToCopy)); UtilsUtils::runDirectFilesystem(function ($fs) use($filename) { /** * WP_Filesystem_Direct. * * @var WP_Filesystem_Direct */ $fs = $fs; $fs->chmod($filename, \constant('FS_CHMOD_FILE')); }); } return \true; } return \false; } /** * Generate a new hash for the current served JS file. */ protected function updateHash() { global $wpdb; $hash = \md5(\wp_generate_uuid4()); \update_option($this->getOptionNamePrefix() . self::OPTION_NAME_SERVE_HASH_SUFFIX, $hash, \true); \update_option($this->getOptionNamePrefix() . self::OPTION_NAME_SERVE_NEXT_HASH_SUFFIX, \time() + self::GENERATE_NEXT_HASH, \true); // Save in history $table_name = $this->getTableName(); $wpdb->insert($table_name, ['serve_hash' => $hash, 'created' => \current_time('mysql')]); // Read all deleted hashes so `DeliverAnonymousAsset` can delete it $sql = "SELECT serve_hash FROM {$table_name} WHERE id NOT IN (SELECT id FROM (SELECT id FROM {$table_name} ORDER BY id DESC LIMIT " . self::MAX_SEO_REDIRECTS . ') foo);'; // phpcs:disable WordPress.DB.PreparedSQL $deletedHashes = $wpdb->get_col($sql); // phpcs:enable WordPress.DB.PreparedSQL // Only hold x SEO redirects (https://stackoverflow.com/a/578926/5506547) $sql = "DELETE FROM {$table_name} WHERE id NOT IN (SELECT id FROM (SELECT id FROM {$table_name} ORDER BY id DESC LIMIT " . self::MAX_SEO_REDIRECTS . ') foo);'; // phpcs:disable WordPress.DB.PreparedSQL $wpdb->query($sql); // phpcs:enable WordPress.DB.PreparedSQL // Unlink the old folder $this->purgeHashes($this->getContentDir(), $deletedHashes); /** * The JavaScript and CSS files, which were previously anonymous, have been rotated and their hashes have been updated. * * @see https://devowl.io/knowledge-base/real-cookie-banner-javascript-files-in-wp-content/ * @hook DevOwl/DeliverAnonymousAsset/Update/$optionNamePrefix * @param {string[]} $deletedHashes * @param {AnonymousAssetBuilder} $builder */ \do_action('DevOwl/DeliverAnonymousAsset/Update/' . $this->getOptionNamePrefix(), $deletedHashes, $this); return $hash; } /** * Make sure the database table is created. * * @param Activator $activator */ public function dbDelta($activator) { $charset_collate = $activator->getCharsetCollate(); $table_name = $this->getTableName(); $sql = "CREATE TABLE {$table_name} (\n id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,\n serve_hash char(32) NOT NULL,\n created datetime NOT NULL,\n PRIMARY KEY (id)\n ) {$charset_collate};"; \dbDelta($sql); // Force to update our assets cause updates can lead to new JavaScript files $this->forceRecreation(); } /** * Force recreation of asset files. */ public function forceRecreation() { \update_option($this->getOptionNamePrefix() . self::OPTION_NAME_SERVE_NEXT_HASH_SUFFIX, 0); } /** * Getter. */ public function getTableName() { global $wpdb; return empty($this->table_name) ? $wpdb->prefix . self::TABLE_NAME : $this->table_name; } /** * Getter. */ public function getFolder() { return $this->folder; } /** * Getter. */ public function getContentDir() { if ($this->contentDir === null) { $this->contentDir = Utils::getContentDir(); } return $this->contentDir; } /** * Getter. * * @codeCoverageIgnore */ public function getOptionNamePrefix() { return $this->optionNamePrefix; } /** * Generate the filename for a given original filename. * * @param string $hash The hash to use * @param string $originalFilenameOrPath */ public static function generateFilename($hash, $originalFilenameOrPath) { $basename = \basename($originalFilenameOrPath); $extension = \pathinfo($basename, \PATHINFO_EXTENSION); return UtilsUtils::simpleHash($hash . $basename) . '.' . $extension; } /** * Hashes got rotated and we can delete old folders from the filesystem. * * @param string $contentDir * @param string[] $hashes */ public static function purgeHashes($contentDir, $hashes) { if ($contentDir !== \false) { UtilsUtils::runDirectFilesystem(function ($fs) use($hashes, $contentDir) { foreach ($hashes as $deletedHash) { $fs->rmdir($contentDir . $deletedHash, \true); } }); } } /** * Remove the files from filesystem. Use this function in your `uninstall.php`. * * @param string $table_name */ public static function uninstall($table_name) { global $wpdb; $contentDir = Utils::getContentDir(); if (!$contentDir) { return; } $sql = "SELECT serve_hash FROM {$table_name}"; // phpcs:disable WordPress.DB.PreparedSQL $hashes = $wpdb->get_col($sql); // phpcs:enable WordPress.DB.PreparedSQL self::purgeHashes($contentDir, $hashes); } }