{"record":{"id":"0a17e60f50db4b55","repo":"getgrav/grav","slug":"opening-file-for-writing-failed-on-error-message","errorCode":null,"errorMessage":"Opening file for writing failed on error {$message}","messagePattern":"Opening file for writing failed on error (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Framework/File/AbstractFile.php","lineNumber":198,"sourceCode":"        return is_file($this->filepath) ? (int)filemtime($this->filepath) : time();\n    }\n\n    /**\n     * {@inheritdoc}\n     * @see FileInterface::lock()\n     */\n    public function lock(bool $block = true): bool\n    {\n        if (!$this->handle) {\n            if (!$this->mkdir($this->getPath())) {\n                throw new RuntimeException('Creating directory failed for ' . $this->filepath);\n            }\n            $this->handle = @fopen($this->filepath, 'cb+') ?: null;\n            if (!$this->handle) {\n                $error = error_get_last();\n                $message = $error['message'] ?? 'Unknown error';\n\n                throw new RuntimeException(\"Opening file for writing failed on error {$message}\");\n            }\n        }\n\n        $lock = $block ? LOCK_EX : LOCK_EX | LOCK_NB;\n\n        // Some filesystems do not support file locks, only fail if another process holds the lock.\n        $this->locked = flock($this->handle, $lock, $wouldBlock) || !$wouldBlock;\n\n        return $this->locked;\n    }\n\n    /**\n     * {@inheritdoc}\n     * @see FileInterface::unlock()\n     */\n    public function unlock(): bool\n    {\n        if (!$this->handle) {","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Framework/File/AbstractFile.php#L180-L216","documentation":"After the directory exists, AbstractFile::lock() opens the target with @fopen($filepath, 'cb+') (read/write, create if missing, no truncation); on failure it embeds PHP's last error message (error_get_last()) and throws. Because the directory step already succeeded, this failure is about the file itself: it exists but is not writable by the PHP user, the path is blocked by open_basedir, the 'file' is actually a directory, or a MAC layer (SELinux) denies access.","triggerScenarios":"lock() on a file created earlier by a different user (root CLI run leaving root-owned files); file mode 644 owned by another user while PHP runs as www-data; a directory occupying the intended file path; open_basedir excluding the path; SELinux context denials on the file.","commonSituations":"Mixing bin/grav CLI runs as root with web requests; restoring backups that reset ownership/modes; hardened shared hosting; container images with wrong volume ownership (files baked in as root).","solutions":["Fix the file's ownership/mode: chown www-data:www-data <file> && chmod 664 <file> (and its directory 775).","If the file is a stale/regenerable artifact, remove it so 'c' mode recreates it with correct ownership.","Verify the path is within open_basedir and check SELinux audit logs (restorecon -v on the path).","Run CLI and web PHP as the same user to prevent ownership flips."],"exampleFix":"# before: \"Opening file for writing failed on error fopen(...): failed to open stream: Permission denied\"\nls -l /var/www/grav/user/data/plugin/x.json   # -rw-r--r-- root root\nsudo chown www-data:www-data /var/www/grav/user/data/plugin/x.json\nsudo chmod 664 /var/www/grav/user/data/plugin/x.json\n# after: lock() succeeds","handlingStrategy":"validation","validationCode":"$path = $file->getFilePath();\nif (file_exists($path) ? !is_writable($path) : !is_writable(dirname($path))) {\n    // fopen('cb+') in lock() will fail: fix ownership/mode first\n}","typeGuard":null,"tryCatchPattern":"try {\n    $file->lock();\n} catch (\\RuntimeException $e) {\n    // message embeds PHP's own fopen error; log it verbatim for the admin\n    $log->error($e->getMessage());\n}","preventionTips":["Never run bin/grav as root — files become root-owned and unwritable by the web user.","After restores/migrations, chown -R the install to the PHP user.","Check open_basedir and SELinux contexts when adding new storage paths."],"tags":["php","grav","filesystem","permissions","fopen"],"backgroundTag":"file-permission-denied","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}