$v ) { $vars[ $k ] = htmlspecialchars_decode( (string) $v, ENT_QUOTES ); } $details = htmlspecialchars_decode( (string) $details, ENT_QUOTES ); $entry = self::catalog( $key ); $this->key = $key; $this->title = self::fill( $entry['title'], $vars ); $this->explanation = self::fill( $entry['message'], $vars ); foreach ( $entry['fix'] as $step ) { $this->fix[] = self::fill( $step, $vars ); } $this->details = (string) $details; parent::__construct( $this->title . ': ' . $this->explanation ); } /** * Create an error. The values are plain data: they are never echoed by PHP directly * and are escaped where they are displayed (esc_html() in PHP, textContent in the admin app). * * @param string $key Catalog key. * @param array $vars Placeholder values. * @param string $details Technical details. * @return Spinix_Error */ public static function make( $key, $vars = array(), $details = '' ) { return new self( $key, $vars, $details ); } /** * Array form sent to the browser. * * @return array */ public function to_array() { return array( 'code' => $this->key, 'title' => $this->title, 'message' => $this->explanation, 'fix' => $this->fix, 'details' => $this->details, ); } /** * Convert any Throwable into the plain-English array form. * * @param Throwable $e Error. * @return array */ public static function describe( $e ) { if ( $e instanceof Spinix_Error ) { return $e->to_array(); } $msg = $e->getMessage(); $err = self::from_php_message( $msg, $e->getFile(), $e->getLine() ); return $err->to_array(); } /** * Turn a PHP error message (fatal, warning, exception) into a Spinix_Error. * * @param string $message PHP message. * @param string $file File. * @param int $line Line. * @return Spinix_Error */ public static function from_php_message( $message, $file = '', $line = 0 ) { $details = trim( $message . ( $file ? ' in ' . $file . ':' . $line : '' ) ); if ( preg_match( '/Allowed memory size of (\d+) bytes exhausted/i', $message, $m ) ) { return new self( 'memory_exhausted', array( 'limit' => size_format( (int) $m[1] ) ), $details ); } if ( preg_match( '/Maximum execution time of (\d+) second/i', $message, $m ) ) { return new self( 'timeout', array( 'seconds' => $m[1] ), $details ); } if ( preg_match( '/open_basedir restriction in effect.*?\(([^)]+)\)/i', $message, $m ) ) { return new self( 'open_basedir', array( 'path' => $m[1] ), $details ); } if ( preg_match( '/No space left on device|Disk quota exceeded/i', $message ) ) { return new self( 'disk_full', array( 'path' => Spinix_Storage::base_dir() ), $details ); } if ( preg_match( '/Permission denied/i', $message ) ) { $path = ''; if ( preg_match( '/\((.*?)\)/', $message, $m ) || preg_match( '/: failed to open stream|Failed to open stream/i', $message ) ) { $path = isset( $m[1] ) ? $m[1] : ''; } return new self( 'permission_denied_path', array( 'path' => $path ? $path : __( 'a file or folder on the server', 'spinix-migration' ) ), $details ); } if ( preg_match( '/Call to undefined function (\w+)/i', $message, $m ) ) { $ext = self::extension_for_function( $m[1] ); if ( $ext ) { return new self( 'php_extension_missing', array( 'ext' => $ext ), $details ); } return new self( 'function_disabled', array( 'function' => $m[1] ), $details ); } if ( preg_match( '/(\w+)\(\) has been disabled for security reasons/i', $message, $m ) ) { return new self( 'function_disabled', array( 'function' => $m[1] ), $details ); } return new self( 'unexpected', array( 'message' => $message ), $details ); } /** * Map a MySQL error to a plain-English error. * * @param int $errno MySQL error number. * @param string $error MySQL error text. * @param string $query The query (trimmed for the report). * @return Spinix_Error */ public static function from_mysql( $errno, $error, $query = '' ) { $errno = (int) $errno; $details = 'MySQL error ' . $errno . ': ' . $error . ( $query ? "\nQuery: " . substr( $query, 0, 300 ) : '' ); switch ( $errno ) { case 1044: case 1142: case 1227: case 1045: $priv = 'CREATE, DROP, ALTER, INSERT, UPDATE, DELETE, INDEX'; if ( preg_match( '/^(\w+)\s+command denied/i', $error, $m ) ) { $priv = strtoupper( $m[1] ); } return new self( 'db_permission', array( 'privilege' => $priv ), $details ); case 1153: case 1301: return new self( 'db_packet', array(), $details ); case 1273: case 1115: return new self( 'db_collation', array( 'error' => $error ), $details ); case 1071: return new self( 'db_key_too_long', array(), $details ); case 2006: case 2013: return new self( 'db_gone_away', array(), $details ); case 1114: case 1021: return new self( 'db_full', array(), $details ); case 1050: return new self( 'db_table_exists', array( 'error' => $error ), $details ); case 1286: return new self( 'db_engine', array( 'error' => $error ), $details ); default: return new self( 'db_generic', array( 'errno' => $errno, 'error' => $error ), $details ); } } /** * Which PHP extension provides a function. * * @param string $fn Function. * @return string */ public static function extension_for_function( $fn ) { $map = array( 'gzdeflate' => 'zlib', 'gzinflate' => 'zlib', 'gzopen' => 'zlib', 'gzcompress' => 'zlib', 'openssl_encrypt' => 'openssl', 'openssl_decrypt' => 'openssl', 'mysqli_connect' => 'mysqli', 'json_encode' => 'json', 'mb_strlen' => 'mbstring', 'hash_pbkdf2' => 'hash', ); $fn = strtolower( $fn ); return isset( $map[ $fn ] ) ? $map[ $fn ] : ''; } /** * Replace {placeholders}. * * @param string $text Text. * @param array $vars Vars. * @return string */ private static function fill( $text, $vars ) { foreach ( $vars as $k => $v ) { $text = str_replace( '{' . $k . '}', (string) $v, $text ); } return $text; } /** * The error catalog. Written for site owners, not developers. * * @param string $key Key. * @return array */ public static function catalog( $key ) { $host = __( 'If you are not comfortable doing this yourself, click "Copy message for my host" and send it to your hosting provider\'s support team. They can fix this in a few minutes.', 'spinix-migration' ); $c = array( 'symlink_unavailable' => array( 'title' => __( 'This server cannot create symbolic links', 'spinix-migration' ), 'message' => __( 'The backup contains {count} symbolic link(s) (shortcut files that point to another file or folder), for example: {example}. To restore them exactly, PHP must be allowed to create symbolic links, but the PHP function "symlink" is disabled or blocked on this server. Nothing has been changed on your website.', 'spinix-migration' ), 'fix' => array( __( '"symlink" is a built-in PHP function, not an extension, so there is nothing to install. It is switched off by the "disable_functions" setting in php.ini.', 'spinix-migration' ), __( 'VPS / dedicated server: open php.ini (run "php --ini" to find it, or check /etc/php//fpm/php.ini), remove "symlink" from the disable_functions line, save, then restart PHP (for example: systemctl restart php8.2-fpm, or restart LiteSpeed/lsphp). On CyberPanel, open "PHP → Edit PHP Configs".', 'spinix-migration' ), __( 'cPanel / Plesk / DirectAdmin: open "MultiPHP INI Editor" or "PHP Settings" and remove "symlink" from disable_functions. If you cannot see that setting, ask your host to enable it.', 'spinix-migration' ), __( 'Then come back here and click "Check again". Spinix will confirm the server is ready before the restore starts.', 'spinix-migration' ), $host, ), ), 'php_extension_missing' => array( 'title' => __( 'A required PHP extension is missing', 'spinix-migration' ), 'message' => __( 'Your server does not have the PHP extension "{ext}" installed or enabled. Spinix needs it for this operation.', 'spinix-migration' ), 'fix' => array( __( 'Open your hosting control panel (cPanel, Plesk, DirectAdmin, etc.) and look for "Select PHP Version" or "PHP Extensions".', 'spinix-migration' ), __( 'Tick the "{ext}" extension and save.', 'spinix-migration' ), $host, ), ), 'dir_not_writable' => array( 'title' => __( 'A folder is not writable', 'spinix-migration' ), 'message' => __( 'Spinix cannot write to this folder: {path}. The web server does not have permission to create or change files there.', 'spinix-migration' ), 'fix' => array( __( 'Using your hosting File Manager or an FTP program, set the folder permissions to 755 (some hosts need 775).', 'spinix-migration' ), __( 'Make sure the folder is owned by the same user that runs your website (the web server / PHP user).', 'spinix-migration' ), $host, ), ), 'dir_create_failed' => array( 'title' => __( 'A folder could not be created', 'spinix-migration' ), 'message' => __( 'Spinix tried to create the folder {path} but the server refused.', 'spinix-migration' ), 'fix' => array( __( 'Check that the parent folder has permission 755 and belongs to the web server user.', 'spinix-migration' ), __( 'Check that your hosting account is not out of disk space or file (inode) quota.', 'spinix-migration' ), $host, ), ), 'permission_denied_path' => array( 'title' => __( 'The server denied access to a file', 'spinix-migration' ), 'message' => __( 'The server refused to let Spinix read or write {path}. This is a file permission problem on the server.', 'spinix-migration' ), 'fix' => array( __( 'Set folders to permission 755 and files to 644 using your File Manager or FTP program.', 'spinix-migration' ), __( 'Make sure files are owned by the user that runs your website.', 'spinix-migration' ), $host, ), ), 'file_not_readable' => array( 'title' => __( 'A file could not be read', 'spinix-migration' ), 'message' => __( 'Spinix could not read {path}. Its permissions do not allow the web server to open it.', 'spinix-migration' ), 'fix' => array( __( 'Set the file permission to 644 using your File Manager or FTP program.', 'spinix-migration' ), $host, ), ), 'file_write_failed' => array( 'title' => __( 'Writing a file failed', 'spinix-migration' ), 'message' => __( 'Spinix could not write to {path}. The disk may be full, or the server is blocking the write.', 'spinix-migration' ), 'fix' => array( __( 'Check the free disk space in your hosting control panel and delete old backups or unused files.', 'spinix-migration' ), __( 'Check that the folder has permission 755.', 'spinix-migration' ), $host, ), ), 'disk_full' => array( 'title' => __( 'Not enough disk space', 'spinix-migration' ), 'message' => __( 'Your server ran out of disk space (or reached its storage quota) while writing to {path}.', 'spinix-migration' ), 'fix' => array( __( 'Delete old backups from the Backups page, or remove unused files and plugins.', 'spinix-migration' ), __( 'Ask your host to increase your storage quota.', 'spinix-migration' ), __( 'Tip: you can exclude large folders (for example media or cache) in the backup options to make the backup smaller.', 'spinix-migration' ), ), ), 'disk_space_low' => array( 'title' => __( 'Not enough free disk space for this operation', 'spinix-migration' ), 'message' => __( 'This operation needs about {needed} of free space, but only {free} is available on the server.', 'spinix-migration' ), 'fix' => array( __( 'Delete old backups from the Backups page, or remove unused files.', 'spinix-migration' ), __( 'Ask your host to increase your storage quota.', 'spinix-migration' ), ), ), 'rename_failed' => array( 'title' => __( 'A file could not be moved into place', 'spinix-migration' ), 'message' => __( 'Spinix could not move {from} to {to}. Usually this means the destination folder is not writable.', 'spinix-migration' ), 'fix' => array( __( 'Set folder permissions to 755 and file permissions to 644, owned by the web server user.', 'spinix-migration' ), $host, ), ), 'open_basedir' => array( 'title' => __( 'The server restricts which folders PHP may use', 'spinix-migration' ), 'message' => __( 'A security setting called "open_basedir" stops PHP from accessing {path}.', 'spinix-migration' ), 'fix' => array( __( 'In your hosting control panel, add your website folder and the system temp folder to the open_basedir setting, or disable it for this site.', 'spinix-migration' ), $host, ), ), 'function_disabled' => array( 'title' => __( 'A PHP function is disabled on this server', 'spinix-migration' ), 'message' => __( 'The PHP function "{function}" is disabled by your hosting provider.', 'spinix-migration' ), 'fix' => array( __( 'Ask your host to remove "{function}" from the "disable_functions" list in php.ini.', 'spinix-migration' ), $host, ), ), 'memory_exhausted' => array( 'title' => __( 'PHP ran out of memory', 'spinix-migration' ), 'message' => __( 'The server stopped the operation because PHP used its whole memory allowance ({limit}).', 'spinix-migration' ), 'fix' => array( __( 'Increase the PHP "memory_limit" to at least 256M in your hosting control panel (look for "PHP Options" or "MultiPHP INI Editor").', 'spinix-migration' ), __( 'Or add this line to wp-config.php above "That\'s all, stop editing": define( \'WP_MEMORY_LIMIT\', \'256M\' );', 'spinix-migration' ), __( 'Then click "Try again". Spinix will continue from where it stopped.', 'spinix-migration' ), ), ), 'timeout' => array( 'title' => __( 'The server stopped the request because it took too long', 'spinix-migration' ), 'message' => __( 'PHP has a time limit of {seconds} seconds per request on this server, and one step took longer than that.', 'spinix-migration' ), 'fix' => array( __( 'Click "Try again". Spinix works in small steps and will continue where it stopped.', 'spinix-migration' ), __( 'If it keeps happening, increase "max_execution_time" to 60 or more in your hosting control panel.', 'spinix-migration' ), ), ), 'db_permission' => array( 'title' => __( 'The database user is missing permissions', 'spinix-migration' ), 'message' => __( 'Your database user is not allowed to run {privilege} commands. Spinix needs these to restore tables.', 'spinix-migration' ), 'fix' => array( __( 'In your hosting control panel open "MySQL Databases", find the database user in wp-config.php and grant it ALL PRIVILEGES on this database.', 'spinix-migration' ), $host, ), ), 'db_packet' => array( 'title' => __( 'A database row is larger than the server allows', 'spinix-migration' ), 'message' => __( 'The MySQL setting "max_allowed_packet" on this server is too small for some of the data in this backup.', 'spinix-migration' ), 'fix' => array( __( 'Ask your host to increase "max_allowed_packet" to at least 64M.', 'spinix-migration' ), $host, ), ), 'db_collation' => array( 'title' => __( 'The database uses a text format this server does not support', 'spinix-migration' ), 'message' => __( 'The backup was made on a newer database server and uses a character set or collation that this server does not know ({error}).', 'spinix-migration' ), 'fix' => array( __( 'Ask your host to upgrade MySQL to 5.7+ or MariaDB 10.3+.', 'spinix-migration' ), $host, ), ), 'db_key_too_long' => array( 'title' => __( 'The database server is too old for this backup', 'spinix-migration' ), 'message' => __( 'This MySQL server cannot create some of the indexes in the backup (index key too long). This happens with MySQL older than 5.7 or MariaDB older than 10.2.', 'spinix-migration' ), 'fix' => array( __( 'Ask your host to upgrade MySQL to 5.7 or newer (or MariaDB 10.3 or newer).', 'spinix-migration' ), $host, ), ), 'db_gone_away' => array( 'title' => __( 'The database server closed the connection', 'spinix-migration' ), 'message' => __( 'MySQL disconnected in the middle of the operation ("MySQL server has gone away"). This is usually caused by a very low timeout or packet size limit on the database server.', 'spinix-migration' ), 'fix' => array( __( 'Click "Try again" — Spinix continues where it stopped.', 'spinix-migration' ), __( 'If it repeats, ask your host to increase "wait_timeout" and "max_allowed_packet" for MySQL.', 'spinix-migration' ), ), ), 'db_full' => array( 'title' => __( 'The database is full', 'spinix-migration' ), 'message' => __( 'The database server has no space left or your database size quota is reached.', 'spinix-migration' ), 'fix' => array( __( 'Ask your host to increase the database size limit, or free up disk space.', 'spinix-migration' ), ), ), 'db_table_exists' => array( 'title' => __( 'A temporary table already exists', 'spinix-migration' ), 'message' => __( 'A leftover table from an earlier attempt is in the way ({error}).', 'spinix-migration' ), 'fix' => array( __( 'Click "Try again". Spinix will clean up and continue.', 'spinix-migration' ), ), ), 'db_engine' => array( 'title' => __( 'A database storage engine is not available', 'spinix-migration' ), 'message' => __( 'The backup uses a MySQL storage engine that this server does not support ({error}).', 'spinix-migration' ), 'fix' => array( $host, ), ), 'db_generic' => array( 'title' => __( 'The database reported an error', 'spinix-migration' ), 'message' => __( 'MySQL returned error {errno}: {error}', 'spinix-migration' ), 'fix' => array( __( 'Click "Try again". If it repeats, send the technical details below to your host.', 'spinix-migration' ), $host, ), ), 'db_not_mysqli' => array( 'title' => __( 'Unsupported database driver', 'spinix-migration' ), 'message' => __( 'This site uses a custom database driver (a "db.php" drop-in) that Spinix cannot work with.', 'spinix-migration' ), 'fix' => array( __( 'Temporarily rename wp-content/db.php, run the operation, then rename it back.', 'spinix-migration' ), ), ), 'archive_invalid' => array( 'title' => __( 'This is not a valid Spinix backup', 'spinix-migration' ), 'message' => __( 'The file "{file}" is not a .spinix backup, or it is damaged.', 'spinix-migration' ), 'fix' => array( __( 'Make sure you selected a file that ends with .spinix and was created by Spinix Migration & Backup.', 'spinix-migration' ), __( 'If you uploaded it with FTP, upload it again in BINARY mode (ASCII mode damages backups).', 'spinix-migration' ), ), ), 'archive_incomplete' => array( 'title' => __( 'This backup file is incomplete', 'spinix-migration' ), 'message' => __( 'The file "{file}" ends before the backup is complete. The download or upload was probably interrupted.', 'spinix-migration' ), 'fix' => array( __( 'Download the backup again from the original site, then upload it again.', 'spinix-migration' ), __( 'If you used FTP, make sure the transfer finished and used BINARY mode.', 'spinix-migration' ), ), ), 'archive_corrupt' => array( 'title' => __( 'The backup file is damaged', 'spinix-migration' ), 'message' => __( 'Part of "{file}" failed its integrity check ({where}). The file was changed or damaged after it was created.', 'spinix-migration' ), 'fix' => array( __( 'Download the backup again from the original site and re-upload it (use BINARY mode in FTP).', 'spinix-migration' ), ), ), 'archive_password' => array( 'title' => __( 'Wrong password', 'spinix-migration' ), 'message' => __( 'This backup is encrypted and the password you entered is not correct.', 'spinix-migration' ), 'fix' => array( __( 'Enter the password that was used when the backup was created. Passwords are case-sensitive.', 'spinix-migration' ), ), ), 'archive_newer' => array( 'title' => __( 'This backup needs a newer version of Spinix', 'spinix-migration' ), 'message' => __( 'The backup was created with a newer Spinix format than this plugin understands.', 'spinix-migration' ), 'fix' => array( __( 'Update Spinix Migration & Backup on this site to the latest version, then try again.', 'spinix-migration' ), ), ), 'job_locked' => array( 'title' => __( 'Another operation is already running', 'spinix-migration' ), 'message' => __( 'Spinix is already running "{job}". Only one backup, export or restore can run at a time.', 'spinix-migration' ), 'fix' => array( __( 'Wait for it to finish, or open the Spinix dashboard to resume or stop it.', 'spinix-migration' ), ), ), 'job_missing' => array( 'title' => __( 'The operation could not be found', 'spinix-migration' ), 'message' => __( 'The operation you are trying to continue no longer exists. It may have been cancelled or cleaned up.', 'spinix-migration' ), 'fix' => array( __( 'Reload the page and start again.', 'spinix-migration' ), ), ), 'not_allowed' => array( 'title' => __( 'You are not allowed to do this', 'spinix-migration' ), 'message' => __( 'Your user account does not have permission, or your login session expired.', 'spinix-migration' ), 'fix' => array( __( 'Reload the page and log in again as an administrator.', 'spinix-migration' ), ), ), 'upload_failed' => array( 'title' => __( 'The upload failed', 'spinix-migration' ), 'message' => __( '{reason}', 'spinix-migration' ), 'fix' => array( __( 'Check your internet connection and try again. Spinix resumes uploads where they stopped.', 'spinix-migration' ), __( 'For very large files you can also upload the .spinix file with FTP into the folder shown on the Backups page.', 'spinix-migration' ), ), ), 'multisite' => array( 'title' => __( 'Multisite is not supported yet', 'spinix-migration' ), 'message' => __( '{reason}', 'spinix-migration' ), 'fix' => array( __( 'Spinix currently supports single WordPress sites. Multisite support is on the roadmap.', 'spinix-migration' ), ), ), 'nothing_selected' => array( 'title' => __( 'Nothing to do', 'spinix-migration' ), 'message' => __( 'You excluded everything, so there is nothing to back up or restore.', 'spinix-migration' ), 'fix' => array( __( 'Select at least one item (database, media, plugins, themes or other files).', 'spinix-migration' ), ), ), 'not_found' => array( 'title' => __( 'File not found', 'spinix-migration' ), 'message' => __( 'The file "{file}" does not exist anymore.', 'spinix-migration' ), 'fix' => array( __( 'Reload the page. If you uploaded it with FTP, check it is inside the Spinix backups folder.', 'spinix-migration' ), ), ), 'invalid_request' => array( 'title' => __( 'Invalid request', 'spinix-migration' ), 'message' => __( '{reason}', 'spinix-migration' ), 'fix' => array( __( 'Reload the page and try again.', 'spinix-migration' ), ), ), 'unexpected' => array( 'title' => __( 'Something unexpected went wrong', 'spinix-migration' ), 'message' => __( 'The server reported: {message}', 'spinix-migration' ), 'fix' => array( __( 'Click "Try again". Spinix continues where it stopped.', 'spinix-migration' ), __( 'If it repeats, open Tools → System Health to check for problems, and send the technical details to your host or to Spinix support.', 'spinix-migration' ), ), ), ); /** * Error catalog (add-ons add their own plain-English errors). */ $c = (array) apply_filters( 'spinix_error_catalog', $c, $host ); return isset( $c[ $key ] ) ? $c[ $key ] : $c['unexpected']; } }