nextcloud-notes/lib/Service/NoteUtil.php

191 lines
6.1 KiB
PHP
Raw Normal View History

2020-07-13 08:27:52 +02:00
<?php
declare(strict_types=1);
2019-09-20 22:07:56 +02:00
namespace OCA\Notes\Service;
2020-04-12 19:26:57 +02:00
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
2020-02-06 21:48:28 +01:00
use OCP\IDBConnection;
2019-09-20 22:07:56 +02:00
class NoteUtil {
2020-08-26 15:09:11 +02:00
/** @var Util */
public $util;
2020-02-06 21:48:28 +01:00
private $db;
2019-09-20 22:07:56 +02:00
private $root;
2020-04-12 19:26:57 +02:00
private $tagService;
private $cachedTags;
2019-09-20 22:07:56 +02:00
public function __construct(
2020-08-26 15:09:11 +02:00
Util $util,
2019-09-20 22:07:56 +02:00
IRootFolder $root,
2020-04-12 19:26:57 +02:00
IDBConnection $db,
2020-08-26 15:09:11 +02:00
TagService $tagService
2019-09-20 22:07:56 +02:00
) {
2020-08-26 15:09:11 +02:00
$this->util = $util;
2019-09-20 22:07:56 +02:00
$this->root = $root;
2020-04-12 19:26:57 +02:00
$this->db = $db;
$this->tagService = $tagService;
2019-09-20 22:07:56 +02:00
}
2020-04-12 19:26:57 +02:00
public function getRoot() : IRootFolder {
return $this->root;
2019-09-20 22:07:56 +02:00
}
2020-04-12 19:26:57 +02:00
public function getTagService() : TagService {
return $this->tagService;
2019-09-20 22:07:56 +02:00
}
2020-04-12 19:26:57 +02:00
public function getCategoryFolder(Folder $notesFolder, string $category) {
$path = $notesFolder->getPath();
// sanitise path
$cats = explode('/', $category);
$cats = array_map([$this, 'sanitisePath'], $cats);
$cats = array_filter($cats, function ($str) {
return $str !== '';
});
$path .= '/'.implode('/', $cats);
return $this->getOrCreateFolder($path);
2019-09-20 22:07:56 +02:00
}
/**
* get path of file and the title.txt and check if they are the same
* file. If not the title needs to be renamed
*
* @param Folder $folder a folder to the notes directory
* @param string $title the filename which should be used
* @param string $suffix the suffix (incl. dot) which should be used
* @param int $id the id of the note for which the title should be generated
* used to see if the file itself has the title and not a different file for
* checking for filename collisions
* @return string the resolved filename to prevent overwriting different
* files with the same title
*/
public function generateFileName(Folder $folder, string $title, string $suffix, int $id) : string {
2020-04-12 19:26:57 +02:00
$title = $this->getSafeTitle($title);
$filename = $title . $suffix;
2019-09-20 22:07:56 +02:00
// if file does not exist, that name has not been taken. Similar we don't
// need to handle file collisions if it is the filename did not change
2020-04-12 19:26:57 +02:00
if (!$folder->nodeExists($filename) || $folder->get($filename)->getId() === $id) {
return $filename;
2019-09-20 22:07:56 +02:00
} else {
// increments name (2) to name (3)
$match = preg_match('/\((?P<id>\d+)\)$/u', $title, $matches);
if ($match) {
$newId = ((int) $matches['id']) + 1;
$newTitle = preg_replace(
'/(.*)\s\((\d+)\)$/u',
'$1 (' . $newId . ')',
$title
);
} else {
$newTitle = $title . ' (2)';
}
return $this->generateFileName($folder, $newTitle, $suffix, $id);
}
}
2020-02-27 18:25:30 +01:00
public function getSafeTitle(string $content) : string {
2019-09-20 22:07:56 +02:00
// sanitize: prevent directory traversal, illegal characters and unintended file names
$content = $this->sanitisePath($content);
// generate title from the first line of the content
$splitContent = preg_split("/\R/u", $content, 2);
$title = trim($splitContent[0]);
// using a maximum of 100 chars should be enough
$title = mb_substr($title, 0, 100, "UTF-8");
// ensure that title is not empty
if (empty($title)) {
2020-08-26 15:09:11 +02:00
$title = $this->util->l10n->t('New note');
2019-09-20 22:07:56 +02:00
}
return $title;
}
/** removes characters that are illegal in a file or folder name on some operating systems */
2020-04-12 19:26:57 +02:00
private function sanitisePath(string $str) : string {
2019-09-20 22:07:56 +02:00
// remove characters which are illegal on Windows (includes illegal characters on Unix/Linux)
// prevents also directory traversal by eliminiating slashes
// see also \OC\Files\Storage\Common::verifyPosixPath(...)
$str = str_replace(['*', '|', '/', '\\', ':', '"', '<', '>', '?'], '', $str);
// if mysql doesn't support 4byte UTF-8, then remove those characters
// see \OC\Files\Storage\Common::verifyPath(...)
2020-02-06 21:48:28 +01:00
if (!$this->db->supports4ByteText()) {
2019-09-20 22:07:56 +02:00
$str = preg_replace('%(?:
\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)%xs', '', $str);
}
// prevent file to be hidden
$str = preg_replace("/^[\. ]+/mu", "", $str);
return trim($str);
}
2020-09-20 14:45:49 +02:00
public function stripMarkdown(string $str) : string {
// prepare content: remove markdown characters and empty spaces
$str = preg_replace("/^\s*[*+-]\s+/mu", "", $str); // list item
$str = preg_replace("/^#+\s+(.*?)\s*#*$/mu", "$1", $str); // headline
$str = preg_replace("/^(=+|-+)$/mu", "", $str); // separate line for headline
$str = preg_replace("/(\*+|_+)(.*?)\\1/mu", "$2", $str); // emphasis
return $str;
}
2019-09-20 22:07:56 +02:00
/**
* Finds a folder and creates it if non-existent
* @param string $path path to the folder
* @return Folder
*/
public function getOrCreateFolder(string $path) : Folder {
if ($this->root->nodeExists($path)) {
$folder = $this->root->get($path);
} else {
$folder = $this->root->newFolder($path);
}
if (!($folder instanceof Folder)) {
throw new NotesFolderException($path.' is not a folder');
}
return $folder;
}
/*
* Delete a folder and it's parent(s) if it's/they're empty
2020-02-13 21:48:46 +01:00
* @param Folder $folder folder to delete
2020-04-12 19:26:57 +02:00
* @param Folder $notesFolder root notes folder
2019-09-20 22:07:56 +02:00
*/
2020-04-12 19:26:57 +02:00
public function deleteEmptyFolder(Folder $folder, Folder $notesFolder) : void {
2019-09-20 22:07:56 +02:00
$content = $folder->getDirectoryListing();
$isEmpty = !count($content);
$isNotesFolder = $folder->getPath()===$notesFolder->getPath();
if ($isEmpty && !$isNotesFolder) {
2020-08-26 15:09:11 +02:00
$this->util->logger->debug('Deleting empty category folder '.$folder->getPath());
2019-09-20 22:07:56 +02:00
$parent = $folder->getParent();
$folder->delete();
2020-04-12 19:26:57 +02:00
$this->deleteEmptyFolder($parent, $notesFolder);
2019-09-20 22:07:56 +02:00
}
}
/**
* Checks if there is enough space left on storage. Throws an Exception if storage is not sufficient.
2020-02-13 21:48:46 +01:00
* @param Folder $folder that needs storage
* @param int $requiredBytes amount of storage needed in $folder
* @throws InsufficientStorageException
*/
2020-02-03 21:36:41 +01:00
public function ensureSufficientStorage(Folder $folder, int $requiredBytes) : void {
$availableBytes = $folder->getFreeSpace();
if ($availableBytes >= 0 && $availableBytes < $requiredBytes) {
2020-08-26 15:09:11 +02:00
$this->util->logger->error(
2020-02-13 21:48:46 +01:00
'Insufficient storage in '.$folder->getPath().': '.
'available are '.$availableBytes.'; '.
2020-08-26 15:09:11 +02:00
'required are '.$requiredBytes
2020-02-13 21:48:46 +01:00
);
throw new InsufficientStorageException($requiredBytes.' are required in '.$folder->getPath());
}
}
2019-09-20 22:07:56 +02:00
}