336 строки
9.4 KiB
PHP
336 строки
9.4 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2018 Arthur Schiwon <blizzz@arthur-schiwon.de>
|
|
*
|
|
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
|
|
*
|
|
* @license GNU AGPL version 3 or any later version
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace OCA\WorkflowScript;
|
|
|
|
use OC\Files\Filesystem;
|
|
use OC\Files\View;
|
|
use OC\User\NoUserException;
|
|
use OCA\Files_Sharing\SharedStorage;
|
|
use OCA\GroupFolders\Mount\GroupFolderStorage;
|
|
use OCA\WorkflowEngine\Entity\File;
|
|
use OCA\WorkflowScript\AppInfo\Application;
|
|
use OCA\WorkflowScript\BackgroundJobs\Launcher;
|
|
use OCA\WorkflowScript\Exception\PlaceholderNotSubstituted;
|
|
use OCP\BackgroundJob\IJobList;
|
|
use OCP\EventDispatcher\Event;
|
|
use OCP\EventDispatcher\GenericEvent;
|
|
use OCP\Files\Folder;
|
|
use OCP\Files\InvalidPathException;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\Files\Node;
|
|
use OCP\Files\NotFoundException;
|
|
use OCP\Files\NotPermittedException;
|
|
use OCP\IConfig;
|
|
use OCP\IL10N;
|
|
use OCP\IUser;
|
|
use OCP\IUserSession;
|
|
use OCP\SystemTag\MapperEvent;
|
|
use OCP\WorkflowEngine\IManager;
|
|
use OCP\WorkflowEngine\IRuleMatcher;
|
|
use OCP\WorkflowEngine\ISpecificOperation;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\EventDispatcher\GenericEvent as LegacyGenericEvent;
|
|
|
|
class Operation implements ISpecificOperation {
|
|
|
|
/** @var IManager */
|
|
private $workflowEngineManager;
|
|
/** @var IJobList */
|
|
private $jobList;
|
|
/** @var IL10N */
|
|
private $l;
|
|
/** @var IUserSession */
|
|
private $session;
|
|
/** @var IRootFolder */
|
|
private $rootFolder;
|
|
/** @var IConfig */
|
|
private $config;
|
|
/** @var LoggerInterface */
|
|
private $logger;
|
|
|
|
public function __construct(
|
|
IManager $workflowEngineManager,
|
|
IJobList $jobList,
|
|
IL10N $l,
|
|
IUserSession $session,
|
|
IRootFolder $rootFolder,
|
|
IConfig $config,
|
|
LoggerInterface $logger
|
|
) {
|
|
$this->workflowEngineManager = $workflowEngineManager;
|
|
$this->jobList = $jobList;
|
|
$this->l = $l;
|
|
$this->session = $session;
|
|
$this->rootFolder = $rootFolder;
|
|
$this->config = $config;
|
|
$this->logger = $logger;
|
|
}
|
|
|
|
/**
|
|
* @throws PlaceholderNotSubstituted
|
|
*/
|
|
protected function buildCommand(string $template, Node $node, string $event, array $extra = []) {
|
|
$command = $template;
|
|
|
|
if (strpos($command, '%e')) {
|
|
$command = str_replace('%e', escapeshellarg($event), $command);
|
|
}
|
|
|
|
if (strpos($command, '%n')) {
|
|
// Nextcloud relative-path
|
|
$ncRelPath = $this->replacePlaceholderN($node);
|
|
$command = str_replace('%n', escapeshellarg($ncRelPath), $command);
|
|
unset($ncRelPath);
|
|
}
|
|
|
|
if (strpos($command, '%f')) {
|
|
try {
|
|
$view = new View();
|
|
if ($node instanceof Folder) {
|
|
$fullPath = $view->getLocalFolder($node->getPath());
|
|
} else {
|
|
$fullPath = $view->getLocalFile($node->getPath());
|
|
}
|
|
if ($fullPath === null) {
|
|
throw new \InvalidArgumentException();
|
|
}
|
|
$command = str_replace('%f', escapeshellarg($fullPath), $command);
|
|
} catch (\Exception $e) {
|
|
throw new \InvalidArgumentException('Could not determine full path');
|
|
}
|
|
}
|
|
|
|
if (strpos($command, '%i')) {
|
|
$nodeID = -1;
|
|
try {
|
|
$nodeID = $node->getId();
|
|
} catch (InvalidPathException $e) {
|
|
} catch (NotFoundException $e) {
|
|
}
|
|
$command = str_replace('%i', escapeshellarg($nodeID), $command);
|
|
}
|
|
|
|
if (strpos($command, '%a')) {
|
|
$user = $this->session->getUser();
|
|
$userID = '';
|
|
if ($user instanceof IUser) {
|
|
$userID = $user->getUID();
|
|
}
|
|
$command = str_replace('%a', escapeshellarg($userID), $command);
|
|
}
|
|
|
|
if (strpos($command, '%o')) {
|
|
$user = $node->getOwner();
|
|
$userID = '';
|
|
if ($user instanceof IUser) {
|
|
$userID = $user->getUID();
|
|
}
|
|
$command = str_replace('%o', escapeshellarg($userID), $command);
|
|
}
|
|
|
|
if (strpos($command, '%x')) {
|
|
if (!isset($extra['oldFilePath'])) {
|
|
$extra['oldFilePath'] = '';
|
|
}
|
|
$command = str_replace('%x', escapeshellarg($extra['oldFilePath']), $command);
|
|
}
|
|
|
|
return $command;
|
|
}
|
|
|
|
/**
|
|
* @throws PlaceholderNotSubstituted
|
|
*/
|
|
protected function replacePlaceholderN(Node $node): string {
|
|
$owner = $node->getOwner();
|
|
try {
|
|
$nodeID = $node->getId();
|
|
$storage = $node->getStorage();
|
|
} catch (NotFoundException | InvalidPathException $e) {
|
|
$context = [
|
|
'app' => 'workflow_script',
|
|
'exception' => $e,
|
|
'node' => $node,
|
|
];
|
|
$message = 'Could not get if of node {node}';
|
|
if(isset($nodeID)) {
|
|
$message = 'Could not find storage for file ID {fid}, node: {node}';
|
|
$context['fid'] = $nodeID;
|
|
}
|
|
|
|
$this->logger->warning($message, $context);
|
|
throw new PlaceholderNotSubstituted('n', $e);
|
|
}
|
|
|
|
if(isset($storage) && $storage->instanceOfStorage(GroupFolderStorage::class)) {
|
|
// group folders are always located within $DATADIR/__groupfolders/
|
|
$absPath = $storage->getLocalFile($node->getPath());
|
|
$pos = strpos($absPath, '/__groupfolders/');
|
|
// if the string cannot be found, the fallback is absolute path
|
|
// it should never happen #famousLastWords
|
|
if($pos === false) {
|
|
$this->logger->warning(
|
|
'Groupfolder path does not contain __groupfolders. File ID: {fid}, Node path: {path}, absolute path: {abspath}',
|
|
[
|
|
'app' => 'workflow_script',
|
|
'fid' => $nodeID,
|
|
'path' => $node->getPath(),
|
|
'abspath' => $absPath,
|
|
]
|
|
);
|
|
}
|
|
$ncRelPath = substr($absPath, (int)$pos);
|
|
} elseif (isset($storage) && $storage->instanceOfStorage(SharedStorage::class)) {
|
|
try {
|
|
$folder = $this->rootFolder->getUserFolder($owner->getUID());
|
|
} catch (NotPermittedException | NoUserException $e) {
|
|
throw new PlaceholderNotSubstituted('n', $e);
|
|
}
|
|
$nodes = $folder->getById($nodeID);
|
|
if(empty($nodes)) {
|
|
throw new PlaceholderNotSubstituted('n');
|
|
}
|
|
$newNode = array_shift($nodes);
|
|
$ncRelPath = $newNode->getPath();
|
|
} else {
|
|
$ncRelPath = $node->getPath();
|
|
if (strpos($node->getPath(), $owner->getUID()) !== 0) {
|
|
$nodes = $this->rootFolder->getById($nodeID);
|
|
foreach ($nodes as $testNode) {
|
|
if (strpos($node->getPath(), $owner->getUID()) === 0) {
|
|
$ncRelPath = $testNode;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$ncRelPath = ltrim($ncRelPath, '/');
|
|
|
|
return $ncRelPath;
|
|
}
|
|
|
|
/**
|
|
* @throws \UnexpectedValueException
|
|
* @since 9.1
|
|
*/
|
|
public function validateOperation(string $name, array $checks, string $operation): void {
|
|
if (empty($operation)) {
|
|
throw new \UnexpectedValueException($this->l->t('Please provide a script name'));
|
|
}
|
|
|
|
$scriptName = explode(' ', $operation, 2)[0];
|
|
if (!$this->isScriptValid($scriptName)) {
|
|
throw new \UnexpectedValueException($this->l->t('The script does not seem to be executable'));
|
|
}
|
|
}
|
|
|
|
protected function isScriptValid(string $scriptName) {
|
|
$which = shell_exec('command -v ' . escapeshellarg($scriptName));
|
|
if (!empty($which)) {
|
|
return true;
|
|
}
|
|
|
|
return is_executable($scriptName);
|
|
}
|
|
|
|
public function getDisplayName(): string {
|
|
return $this->l->t('Run script');
|
|
}
|
|
|
|
public function getDescription(): string {
|
|
return $this->l->t('Pass files to external scripts for processing outside of Nextcloud');
|
|
}
|
|
|
|
public function getIcon(): string {
|
|
return \OC::$server->getURLGenerator()->imagePath('workflow_script', 'app.svg');
|
|
}
|
|
|
|
public function isAvailableForScope(int $scope): bool {
|
|
return $scope === IManager::SCOPE_ADMIN;
|
|
}
|
|
|
|
public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void {
|
|
if (!$event instanceof GenericEvent
|
|
&& !$event instanceof LegacyGenericEvent
|
|
&& !$event instanceof MapperEvent) {
|
|
return;
|
|
}
|
|
try {
|
|
$extra = [];
|
|
if ($eventName === '\OCP\Files::postRename') {
|
|
/** @var Node $oldNode */
|
|
list($oldNode, $node) = $event->getSubject();
|
|
$extra = ['oldFilePath' => $oldNode->getPath()];
|
|
} else if ($event instanceof MapperEvent) {
|
|
if ($event->getObjectType() !== 'files') {
|
|
return;
|
|
}
|
|
$nodes = $this->rootFolder->getById($event->getObjectId());
|
|
if (!isset($nodes[0])) {
|
|
return;
|
|
}
|
|
$node = $nodes[0];
|
|
unset($nodes);
|
|
} else {
|
|
$node = $event->getSubject();
|
|
}
|
|
/** @var Node $node */
|
|
|
|
// '', admin, 'files', 'path/to/file.txt'
|
|
list(, , $folder,) = explode('/', $node->getPath(), 4);
|
|
if ($folder !== 'files' || $node instanceof Folder) {
|
|
return;
|
|
}
|
|
|
|
$matches = $ruleMatcher->getFlows(false);
|
|
foreach ($matches as $match) {
|
|
try {
|
|
$command = $this->buildCommand($match['operation'], $node, $eventName, $extra);
|
|
} catch (PlaceholderNotSubstituted $e) {
|
|
$this->logger->warning(
|
|
'Could not substitute {placeholder} in {command} with node {node}',
|
|
[
|
|
'app' => 'workflow_script',
|
|
'placeholder' => $e->getPlaceholder(),
|
|
'command' => $match['operation'],
|
|
'node' => $node,
|
|
'exception' => $e,
|
|
]
|
|
);
|
|
}
|
|
$args = ['command' => $command];
|
|
if (strpos($command, '%f')) {
|
|
$args['path'] = $node->getPath();
|
|
}
|
|
$this->jobList->add(Launcher::class, $args);
|
|
}
|
|
} catch (NotFoundException $e) {
|
|
}
|
|
}
|
|
|
|
public function getEntityId(): string {
|
|
return File::class;
|
|
}
|
|
}
|