datadir = str_replace('//', '/', $parameters['datadir']); // some crazy code uses a local storage on root... if ($this->datadir === '/') { $this->realDataDir = $this->datadir; } else { $realPath = realpath($this->datadir) ?: $this->datadir; $this->realDataDir = rtrim($realPath, '/') . '/'; } if (!str_ends_with($this->datadir, '/')) { $this->datadir .= '/'; } $this->dataDirLength = strlen($this->realDataDir); $this->config = Server::get(IConfig::class); $this->mimeTypeDetector = Server::get(IMimeTypeDetector::class); $this->defUMask = $this->config->getSystemValue('localstorage.umask', 0022); $this->caseInsensitive = $this->config->getSystemValueBool('localstorage.case_insensitive', false); // support Write-Once-Read-Many file systems $this->unlinkOnTruncate = $this->config->getSystemValueBool('localstorage.unlink_on_truncate', false); if (isset($parameters['isExternal']) && $parameters['isExternal'] && !$this->stat('')) { // data dir not accessible or available, can happen when using an external storage of type Local // on an unmounted system mount point throw new StorageNotAvailableException('Local storage path does not exist "' . $this->getSourcePath('') . '"'); } } public function __destruct() { } public function getId(): string { return 'local::' . $this->datadir; } public function mkdir(string $path): bool { $sourcePath = $this->getSourcePath($path); $oldMask = umask($this->defUMask); $result = @mkdir($sourcePath, 0777, true); umask($oldMask); return $result; } public function rmdir(string $path): bool { if (!$this->isDeletable($path)) { return false; } try { $it = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($this->getSourcePath($path)), \RecursiveIteratorIterator::CHILD_FIRST ); /** * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach * This bug is fixed in PHP 5.5.9 or before * See #8376 */ $it->rewind(); while ($it->valid()) { /** * @var \SplFileInfo $file */ $file = $it->current(); clearstatcache(true, $file->getRealPath()); if (in_array($file->getBasename(), ['.', '..'])) { $it->next(); continue; } elseif ($file->isFile() || $file->isLink()) { unlink($file->getPathname()); } elseif ($file->isDir()) { rmdir($file->getPathname()); } $it->next(); } unset($it); // Release iterator and thereby its potential directory lock (e.g. in case of VirtualBox shared folders) clearstatcache(true, $this->getSourcePath($path)); return rmdir($this->getSourcePath($path)); } catch (\UnexpectedValueException $e) { return false; } } public function opendir(string $path) { return opendir($this->getSourcePath($path)); } public function is_dir(string $path): bool { if ($this->caseInsensitive && !$this->file_exists($path)) { return false; } if (str_ends_with($path, '/')) { $path = substr($path, 0, -1); } return is_dir($this->getSourcePath($path)); } public function is_file(string $path): bool { if ($this->caseInsensitive && !$this->file_exists($path)) { return false; } return is_file($this->getSourcePath($path)); } public function stat(string $path): array|false { $fullPath = $this->getSourcePath($path); clearstatcache(true, $fullPath); if (!file_exists($fullPath)) { return false; } $statResult = @stat($fullPath); if (PHP_INT_SIZE === 4 && $statResult && !$this->is_dir($path)) { $filesize = $this->filesize($path); $statResult['size'] = $filesize; $statResult[7] = $filesize; } if (is_array($statResult)) { $statResult['full_path'] = $fullPath; } return $statResult; } public function getMetaData(string $path): ?array { try { $stat = $this->stat($path); } catch (ForbiddenException $e) { return null; } if (!$stat) { return null; } $permissions = Constants::PERMISSION_SHARE; $statPermissions = $stat['mode']; $isDir = ($statPermissions & 0x4000) === 0x4000 && !($statPermissions & 0x8000); if ($statPermissions & 0x0100) { $permissions += Constants::PERMISSION_READ; } if ($statPermissions & 0x0080) { $permissions += Constants::PERMISSION_UPDATE; if ($isDir) { $permissions += Constants::PERMISSION_CREATE; } } if (!($path === '' || $path === '/')) { // deletable depends on the parents unix permissions $parent = dirname($stat['full_path']); if (is_writable($parent)) { $permissions += Constants::PERMISSION_DELETE; } } $data = []; $data['mimetype'] = $isDir ? 'httpd/unix-directory' : $this->mimeTypeDetector->detectPath($path); $data['mtime'] = $stat['mtime']; if ($data['mtime'] === false) { $data['mtime'] = time(); } if ($isDir) { $data['size'] = -1; //unknown } else { $data['size'] = $stat['size']; } $data['etag'] = $this->calculateEtag($path, $stat); $data['storage_mtime'] = $data['mtime']; $data['permissions'] = $permissions; $data['name'] = basename($path); return $data; } public function filetype(string $path): string|false { $filetype = filetype($this->getSourcePath($path)); if ($filetype == 'link') { $filetype = filetype(realpath($this->getSourcePath($path))); } return $filetype; } public function filesize(string $path): int|float|false { if (!$this->is_file($path)) { return 0; } $fullPath = $this->getSourcePath($path); if (PHP_INT_SIZE === 4) { $helper = new \OC\LargeFileHelper; return $helper->getFileSize($fullPath); } return filesize($fullPath); } public function isReadable(string $path): bool { return is_readable($this->getSourcePath($path)); } public function isUpdatable(string $path): bool { return is_writable($this->getSourcePath($path)); } public function file_exists(string $path): bool { if ($this->caseInsensitive) { $fullPath = $this->getSourcePath($path); $parentPath = dirname($fullPath); if (!is_dir($parentPath)) { return false; } $content = scandir($parentPath, SCANDIR_SORT_NONE); return is_array($content) && array_search(basename($fullPath), $content) !== false; } else { return file_exists($this->getSourcePath($path)); } } public function filemtime(string $path): int|false { $fullPath = $this->getSourcePath($path); clearstatcache(true, $fullPath); if (!$this->file_exists($path)) { return false; } if (PHP_INT_SIZE === 4) { $helper = new \OC\LargeFileHelper(); return $helper->getFileMtime($fullPath); } return filemtime($fullPath); } public function touch(string $path, ?int $mtime = null): bool { // sets the modification time of the file to the given value. // If mtime is nil the current time is set. // note that the access time of the file always changes to the current time. if ($this->file_exists($path) && !$this->isUpdatable($path)) { return false; } $oldMask = umask($this->defUMask); if (!is_null($mtime)) { $result = @touch($this->getSourcePath($path), $mtime); } else { $result = @touch($this->getSourcePath($path)); } umask($oldMask); if ($result) { clearstatcache(true, $this->getSourcePath($path)); } return $result; } public function file_get_contents(string $path): string|false { return file_get_contents($this->getSourcePath($path)); } public function file_put_contents(string $path, mixed $data): int|float|false { $oldMask = umask($this->defUMask); if ($this->unlinkOnTruncate) { $this->unlink($path); } $result = file_put_contents($this->getSourcePath($path), $data); umask($oldMask); return $result; } public function unlink(string $path): bool { if ($this->is_dir($path)) { return $this->rmdir($path); } elseif ($this->is_file($path)) { return unlink($this->getSourcePath($path)); } else { return false; } } private function checkTreeForForbiddenItems(string $path): void { $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path)); foreach ($iterator as $file) { /** @var \SplFileInfo $file */ if (Filesystem::isFileBlacklisted($file->getBasename())) { throw new ForbiddenException('Invalid path: ' . $file->getPathname(), false); } } } public function rename(string $source, string $target): bool { $srcParent = dirname($source); $dstParent = dirname($target); if (!$this->isUpdatable($srcParent)) { Server::get(LoggerInterface::class)->error('unable to rename, source directory is not writable : ' . $srcParent, ['app' => 'core']); return false; } if (!$this->isUpdatable($dstParent)) { Server::get(LoggerInterface::class)->error('unable to rename, destination directory is not writable : ' . $dstParent, ['app' => 'core']); return false; } if (!$this->file_exists($source)) { Server::get(LoggerInterface::class)->error('unable to rename, file does not exists : ' . $source, ['app' => 'core']); return false; } if ($this->file_exists($target)) { if ($this->is_dir($target)) { $this->rmdir($target); } elseif ($this->is_file($target)) { $this->unlink($target); } } if ($this->is_dir($source)) { $this->checkTreeForForbiddenItems($this->getSourcePath($source)); } if (@rename($this->getSourcePath($source), $this->getSourcePath($target))) { if ($this->caseInsensitive) { if (mb_strtolower($target) === mb_strtolower($source) && !$this->file_exists($target)) { return false; } } return true; } return $this->copy($source, $target) && $this->unlink($source); } public function copy(string $source, string $target): bool { if ($this->is_dir($source)) { return parent::copy($source, $target); } else { $oldMask = umask($this->defUMask); if ($this->unlinkOnTruncate) { $this->unlink($target); } $result = copy($this->getSourcePath($source), $this->getSourcePath($target)); umask($oldMask); if ($this->caseInsensitive) { if (mb_strtolower($target) === mb_strtolower($source) && !$this->file_exists($target)) { return false; } } return $result; } } public function fopen(string $path, string $mode) { $sourcePath = $this->getSourcePath($path); if (!file_exists($sourcePath) && $mode === 'r') { return false; } $oldMask = umask($this->defUMask); if (($mode === 'w' || $mode === 'w+') && $this->unlinkOnTruncate) { $this->unlink($path); } $result = @fopen($sourcePath, $mode); umask($oldMask); return $result; } public function hash(string $type, string $path, bool $raw = false): string|false { return hash_file($type, $this->getSourcePath($path), $raw); } public function free_space(string $path): int|float|false { $sourcePath = $this->getSourcePath($path); // using !is_dir because $sourcePath might be a part file or // non-existing file, so we'd still want to use the parent dir // in such cases if (!is_dir($sourcePath)) { // disk_free_space doesn't work on files $sourcePath = dirname($sourcePath); } $space = (function_exists('disk_free_space') && is_dir($sourcePath)) ? disk_free_space($sourcePath) : false; if ($space === false || is_null($space)) { return \OCP\Files\FileInfo::SPACE_UNKNOWN; } return Util::numericToNumber($space); } public function search(string $query): array { return $this->searchInDir($query); } public function getLocalFile(string $path): string|false { return $this->getSourcePath($path); } protected function searchInDir(string $query, string $dir = ''): array { $files = []; $physicalDir = $this->getSourcePath($dir); foreach (scandir($physicalDir) as $item) { if (\OC\Files\Filesystem::isIgnoredDir($item)) { continue; } $physicalItem = $physicalDir . '/' . $item; if (strstr(strtolower($item), strtolower($query)) !== false) { $files[] = $dir . '/' . $item; } if (is_dir($physicalItem)) { $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item)); } } return $files; } public function hasUpdated(string $path, int $time): bool { if ($this->file_exists($path)) { return $this->filemtime($path) > $time; } else { return true; } } /** * Get the source path (on disk) of a given path * * @throws ForbiddenException */ public function getSourcePath(string $path): string { if (Filesystem::isFileBlacklisted($path)) { throw new ForbiddenException('Invalid path: ' . $path, false); } $fullPath = $this->datadir . $path; $currentPath = $path; $allowSymlinks = $this->config->getSystemValueBool('localstorage.allowsymlinks', false); if ($allowSymlinks || $currentPath === '') { return $fullPath; } $pathToResolve = $fullPath; $realPath = realpath($pathToResolve); while ($realPath === false) { // for non existing files check the parent directory $currentPath = dirname($currentPath); /** @psalm-suppress TypeDoesNotContainType Let's be extra cautious and still check for empty string */ if ($currentPath === '' || $currentPath === '.') { return $fullPath; } $realPath = realpath($this->datadir . $currentPath); } if ($realPath) { $realPath = $realPath . '/'; } if (substr($realPath, 0, $this->dataDirLength) === $this->realDataDir) { return $fullPath; } Server::get(LoggerInterface::class)->error("Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ['app' => 'core']); throw new ForbiddenException('Following symlinks is not allowed', false); } public function isLocal(): bool { return true; } public function getETag(string $path): string|false { return $this->calculateEtag($path, $this->stat($path)); } private function calculateEtag(string $path, array $stat): string|false { if ($stat['mode'] & 0x4000 && !($stat['mode'] & 0x8000)) { // is_dir & not socket return parent::getETag($path); } else { if ($stat === false) { return md5(''); } $toHash = ''; if (isset($stat['mtime'])) { $toHash .= $stat['mtime']; } if (isset($stat['ino'])) { $toHash .= $stat['ino']; } if (isset($stat['dev'])) { $toHash .= $stat['dev']; } if (isset($stat['size'])) { $toHash .= $stat['size']; } return md5($toHash); } } private function canDoCrossStorageMove(IStorage $sourceStorage): bool { /** @psalm-suppress UndefinedClass,InvalidArgument */ return $sourceStorage->instanceOfStorage(Local::class) // Don't treat ACLStorageWrapper like local storage where copy can be done directly. // Instead, use the slower recursive copying in php from Common::copyFromStorage with // more permissions checks. && !$sourceStorage->instanceOfStorage('OCA\GroupFolders\ACL\ACLStorageWrapper') // Same for access control && !$sourceStorage->instanceOfStorage(\OCA\FilesAccessControl\StorageWrapper::class) // when moving encrypted files we have to handle keys and the target might not be encrypted && !$sourceStorage->instanceOfStorage(Encryption::class); } public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool { if ($this->canDoCrossStorageMove($sourceStorage)) { // resolve any jailed paths while ($sourceStorage->instanceOfStorage(Jail::class)) { /** * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage */ $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath); $sourceStorage = $sourceStorage->getUnjailedStorage(); } /** * @var \OC\Files\Storage\Local $sourceStorage */ $rootStorage = new Local(['datadir' => '/']); return $rootStorage->copy($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath)); } else { return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } } public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { if ($this->canDoCrossStorageMove($sourceStorage)) { // resolve any jailed paths while ($sourceStorage->instanceOfStorage(Jail::class)) { /** * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage */ $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath); $sourceStorage = $sourceStorage->getUnjailedStorage(); } /** * @var \OC\Files\Storage\Local $sourceStorage */ $rootStorage = new Local(['datadir' => '/']); return $rootStorage->rename($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath)); } else { return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } } public function writeStream(string $path, $stream, ?int $size = null): int { /** @var int|false $result We consider here that returned size will never be a float because we write less than 4GB */ $result = $this->file_put_contents($path, $stream); if (is_resource($stream)) { fclose($stream); } if ($result === false) { throw new GenericFileException("Failed write stream to $path"); } else { return $result; } } } l.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
/*
 * jQuery UI Button @VERSION
 *
 * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 *
 * http://docs.jquery.com/UI/Button
 *
 * Depends:
 *	jquery.ui.core.js
 *	jquery.ui.widget.js
 */
(function( $ ) {

var lastActive,
	baseClasses = "ui-button ui-widget ui-state-default ui-corner-all",
	otherClasses = "ui-state-hover ui-state-active " +
		"ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon ui-button-text-only";

$.widget( "ui.button", {
	options: {
		text: true,
		label: null,
		icons: {
			primary: null,
			secondary: null
		}
	},
	_create: function() {
		this._determineButtonType();
		this.hasTitle = !!this.buttonElement.attr( "title" );

		var self = this,
			options = this.options,
			toggleButton = this.type === "checkbox" || this.type === "radio",
			hoverClass = "ui-state-hover" + ( !toggleButton ? " ui-state-active" : "" ),
			focusClass = "ui-state-focus";

		if ( options.label === null ) {
			options.label = this.buttonElement.html();
		}

		this.buttonElement
			.addClass( baseClasses )
			.attr( "role", "button" )
			.bind( "mouseenter.button", function() {
				if ( options.disabled ) {
					return;
				}
				$( this ).addClass( "ui-state-hover" );
				if ( this === lastActive ) {
					$( this ).addClass( "ui-state-active" );
				}
			})
			.bind( "mouseleave.button", function() {
				if ( options.disabled ) {
					return;
				}
				$( this ).removeClass( hoverClass );
			})
			.bind( "focus.button", function() {
				// no need to check disabled, focus won't be triggered anyway
				$( this ).addClass( focusClass );
			})
			.bind( "blur.button", function() {
				$( this ).removeClass( focusClass );
			});

		if ( this.type === "checkbox") {
			this.buttonElement.bind( "click.button", function() {
				if ( options.disabled ) {
					return;
				}
				$( this ).toggleClass( "ui-state-active" );
				self.buttonElement.attr( "aria-pressed", self.element[0].checked );
			});
		} else if ( this.type === "radio") {
			this.buttonElement.bind( "click.button", function() {
				if ( options.disabled ) {
					return;
				}
				$( this ).addClass( "ui-state-active" );
				self.buttonElement.attr( "aria-pressed", true );

				var radio = self.element[ 0 ],
					name = radio.name,
					form = radio.form,
					radios;
				if ( name ) {
					if ( form ) {
						radios = $( form ).find( "[name='" + name + "']" );
					} else {
						radios = $( "[name='" + name + "']", radio.ownerDocument )
							.filter(function() {
								return !this.form;
							});
					}
					radios
						.not( radio )
						.map(function() {
							return $( this ).button( "widget" )[ 0 ];
						})
						.removeClass( "ui-state-active" )
						.attr( "aria-pressed", false );
				}
			});
		} else {
			this.buttonElement
				.bind( "mousedown.button", function() {
					if ( options.disabled ) {
						return;
					}
					$( this ).addClass( "ui-state-active" );
					lastActive = this;
					$( document ).one( "mouseup", function() {
						lastActive = null;
					});
				})
				.bind( "mouseup.button", function() {
					if ( options.disabled ) {
						return;
					}
					$( this ).removeClass( "ui-state-active" );
				})
				.bind( "keydown.button", function(event) {
					if ( event.keyCode == $.ui.keyCode.SPACE || event.keyCode == $.ui.keyCode.ENTER ) {
						$( this ).addClass( "ui-state-active" );
					}
				})
				.bind( "keyup.button", function() {
					$( this ).removeClass( "ui-state-active" );
				});
			if (this.buttonElement.is("a")) {
				this.buttonElement.keyup(function(event) {
					if (event.keyCode == $.ui.keyCode.SPACE) {
						// TODO pass through original event correctly (just as 2nd argument doesn't work)
						$(this).trigger("click");
					}
				});
			}
		}

		// TODO: pull out $.Widget's handling for the disabled option into
		// $.Widget.prototype._setOptionDisabled so it's easy to proxy and can
		// be overridden by individual plugins
		$.Widget.prototype._setOption.call( this, "disabled", options.disabled );
		this._resetButton();
	},

	_determineButtonType: function() {
		this.type = this.element.is( ":checkbox" )
			? "checkbox"
			: this.element.is( ":radio" )
				? "radio"
				: this.element.is( "input" )
					? "input"
					: "button";

		if ( this.type === "checkbox" || this.type === "radio" ) {
			this.buttonElement = $( "[for=" + this.element.attr("id") + "]" );
			this.element.addClass('ui-helper-hidden-accessible');

			var checked = this.element.is( ":checked" );
			if ( checked ) {
				this.buttonElement.addClass( "ui-state-active" );
			}
			this.buttonElement.attr( "aria-pressed", checked );
		} else {
			this.buttonElement = this.element;
		}
	},

	widget: function() {
		return this.buttonElement;
	},

	destroy: function() {
		this.buttonElement
			.removeClass( baseClasses + " " + otherClasses )
			.removeAttr( "role" )
			.removeAttr( "aria-pressed" )
			.html( this.buttonElement.find(".ui-button-text").html() );

		if ( !this.hasTitle ) {
			this.buttonElement.removeAttr( "title" );
		}

		if ( this.type === "checkbox" || this.type === "radio" ) {
			this.element.show();
		}

		$.Widget.prototype.destroy.call( this );
	},

	_setOption: function( key, value ) {
		$.Widget.prototype._setOption.apply( this, arguments );
		this._resetButton();
	},

	_resetButton: function() {
		if ( this.type === "input" ) {
			if ( this.options.label ) {
				this.element.val( this.options.label );
			}
			return;
		}
		var buttonElement = this.buttonElement,
			buttonText = $( "<span></span>" )
				.addClass( "ui-button-text" )
				.html( this.options.label )
				.appendTo( buttonElement.empty() )
				.text();

		var icons = this.options.icons,
			multipleIcons = icons.primary && icons.secondary;
		if ( icons.primary || icons.secondary ) {
			buttonElement.addClass( "ui-button-text-icon" +
				( multipleIcons ? "s" : "" ) );
			if ( icons.primary ) {
				buttonElement.prepend( "<span class='ui-button-icon-primary ui-icon " + icons.primary + "'></span>" );
			}
			if ( icons.secondary ) {
				buttonElement.append( "<span class='ui-button-icon-secondary ui-icon " + icons.secondary + "'></span>" );
			}
			if ( !this.options.text ) {
				buttonElement
					.addClass( multipleIcons ? "ui-button-icons-only" : "ui-button-icon-only" )
					.removeClass( "ui-button-text-icons ui-button-text-icon" );
				if ( !this.hasTitle ) {
					buttonElement.attr( "title", buttonText );
				}
			}
		} else {
			buttonElement.addClass( "ui-button-text-only" );
		}
	}
});

$.widget( "ui.buttonset", {
	_create: function() {
		this.element.addClass( "ui-button-set" );
		this.buttons = this.element.find( ":button, :submit, :reset, :checkbox, :radio, a, :data(button)" )
			.button()
			.map(function() {
				return $( this ).button( "widget" )[ 0 ];
			})
				.removeClass( "ui-corner-all" )
				.filter( ":first" )
					.addClass( "ui-corner-left" )
				.end()
				.filter( ":last" )
					.addClass( "ui-corner-right" )
				.end()
			.end();
	},

	_setOption: function( key, value ) {
		if ( key === "disabled" ) {
			this.buttons.button( "option", key, value );
		}

		$.Widget.prototype._setOption.apply( this, arguments );
	},

	destroy: function() {
		this.element.removeClass( "ui-button-set" );
		this.buttons
			.button( "destroy" )
			.removeClass( "ui-corner-left ui-corner-right" );

		$.Widget.prototype.destroy.call( this );
	}
});

$.fn.log = function() { console.log(this); return this; };
})( jQuery );