false, 'type' => '', 'built' => 0, 'error' => '', ); // A plain filesystem path only: no stream wrapper (phar://, a URL), // which is what "a file on this server" can never need. if ( '' === $path || str_contains( $path, '://' ) || ! path_is_absolute( $path ) ) { $result['error'] = __( 'Enter the full path of the database file on this server.', '404ops-activity-log' ); return $result; } if ( ! is_file( $path ) || ! is_readable( $path ) ) { $result['error'] = __( 'No readable file at that path.', '404ops-activity-log' ); return $result; } $size = filesize( $path ); if ( false === $size || $size > self::MAX_BYTES ) { $result['error'] = __( 'That file is too large to be a geolocation database.', '404ops-activity-log' ); return $result; } try { $reader = new Reader( $path ); $metadata = $reader->metadata(); $reader->close(); } catch ( \Throwable $e ) { $result['error'] = __( 'That file is not a MaxMind DB (.mmdb) database.', '404ops-activity-log' ); return $result; } $type = $metadata->database_type; if ( 1 !== preg_match( '/country|city/i', $type ) ) { $result['error'] = sprintf( /* translators: %s: the database's own type name, e.g. GeoLite2-ASN. */ __( 'That database (%s) has no country or city data. Use a Country or City database.', '404ops-activity-log' ), substr( $type, 0, 60 ) ); return $result; } $result['ok'] = true; $result['type'] = substr( $type, 0, 60 ); $result['built'] = (int) $metadata->build_epoch; return $result; } /** * Point geolocation at a database already on the server, leaving the * file where it is. Refused (and nothing changed) unless it inspects * clean. * * @param string $path Absolute path. * @return array{ok: bool, type: string, built: int, error: string} */ public static function use_path( string $path ): array { $inspected = self::inspect( $path ); if ( $inspected['ok'] ) { update_option( self::OPTION_PATH, $path, false ); Geolocator::reset(); } return $inspected; } /** * Install a database file (an upload's temporary file, or a download's * extracted one) as the managed copy, and use it. * * The file is inspected first; on any failure the managed copy and the * option are exactly as they were. * * @param string $source Absolute path of the new database. * @return array{ok: bool, type: string, built: int, error: string} */ public static function install( string $source ): array { $inspected = self::inspect( $source ); if ( ! $inspected['ok'] ) { return $inspected; } if ( ! self::prepare_dir() ) { $inspected['ok'] = false; $inspected['error'] = __( 'Could not create the uploads folder for the database.', '404ops-activity-log' ); return $inspected; } $target = self::managed_path(); $staging = $target . '.new'; // Copy, then rename over: rename() within one directory is atomic, // so a lookup running meanwhile reads the old file or the new one, // never half of either. // phpcs:disable WordPress.WP.AlternativeFunctions -- one local file into a directory this plugin owns; WP_Filesystem may need FTP credentials a cron run cannot supply. if ( ! @copy( $source, $staging ) || ! @rename( $staging, $target ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- the failure is reported below, not raised as a PHP warning. if ( file_exists( $staging ) ) { wp_delete_file( $staging ); } $inspected['ok'] = false; $inspected['error'] = __( 'Could not write the database into the uploads folder.', '404ops-activity-log' ); return $inspected; } // phpcs:enable update_option( self::OPTION_PATH, $target, false ); Geolocator::reset(); return $inspected; } /** * Stop geolocating. The managed copy, if any, is deleted; a file the * owner pointed at elsewhere is theirs and is left alone. */ public static function forget(): void { $managed = self::managed_path(); if ( file_exists( $managed ) ) { wp_delete_file( $managed ); } delete_option( self::OPTION_PATH ); Geolocator::reset(); } /** * Remove the managed directory entirely (uninstall). */ public static function remove_managed_dir(): void { $dir = self::managed_dir(); foreach ( array( self::FILE_NAME, self::FILE_NAME . '.new', 'index.php', '.htaccess' ) as $file ) { if ( file_exists( $dir . '/' . $file ) ) { wp_delete_file( $dir . '/' . $file ); } } if ( is_dir( $dir ) ) { @rmdir( $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- best effort; a directory someone else put files in is left. } } /** * Create the managed directory with its web guards. */ private static function prepare_dir(): bool { $dir = self::managed_dir(); if ( ! wp_mkdir_p( $dir ) ) { return false; } // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- two fixed guard files in a directory this plugin owns. if ( ! file_exists( $dir . '/index.php' ) ) { file_put_contents( $dir . '/index.php', "\n\tRequire all denied\n\n" . "\n\tOrder allow,deny\n\tDeny from all\n\n" ); } // phpcs:enable return is_dir( $dir ) && wp_is_writable( $dir ); } }