@nextcloud/files 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -12,7 +12,8 @@ export { FileType } from './files/fileType';
12
12
  export { File, type IFile } from './files/file';
13
13
  export { Folder, type IFolder } from './files/folder';
14
14
  export { Node, NodeStatus, type INode } from './files/node';
15
- export { isFilenameValid, getUniqueName } from './utils/filename';
15
+ export * from './utils/filename-validation';
16
+ export { getUniqueName } from './utils/filename';
16
17
  export { formatFileSize, parseFileSize } from './utils/fileSize';
17
18
  export { orderBy } from './utils/sorting';
18
19
  export { sortNodes, FilesSortingMode, type FilesSortingOptions } from './utils/fileSorting';
package/dist/index.mjs CHANGED
@@ -1,19 +1,15 @@
1
- import { getCurrentUser, onRequestTokenUpdate, getRequestToken } from "@nextcloud/auth";
2
1
  import { getLoggerBuilder } from "@nextcloud/logger";
2
+ import { getCurrentUser, onRequestTokenUpdate, getRequestToken } from "@nextcloud/auth";
3
3
  import { join, basename, extname, dirname } from "path";
4
4
  import { encodePath } from "@nextcloud/paths";
5
5
  import { generateRemoteUrl } from "@nextcloud/router";
6
- import { createClient, getPatcher } from "webdav";
7
6
  import { CancelablePromise } from "cancelable-promise";
7
+ import { createClient, getPatcher } from "webdav";
8
+ import { isPublicShare, getSharingToken } from "@nextcloud/sharing/public";
9
+ import { getCapabilities } from "@nextcloud/capabilities";
8
10
  import { getCanonicalLocale, getLanguage } from "@nextcloud/l10n";
9
11
  import { TypedEventTarget } from "typescript-event-target";
10
- const getLogger = (user) => {
11
- if (user === null) {
12
- return getLoggerBuilder().setApp("files").build();
13
- }
14
- return getLoggerBuilder().setApp("files").setUid(user.uid).build();
15
- };
16
- const logger = getLogger(getCurrentUser());
12
+ const logger = getLoggerBuilder().setApp("@nextcloud/files").detectUser().build();
17
13
  var NewMenuEntryCategory = /* @__PURE__ */ ((NewMenuEntryCategory2) => {
18
14
  NewMenuEntryCategory2[NewMenuEntryCategory2["UploadFromDevice"] = 0] = "UploadFromDevice";
19
15
  NewMenuEntryCategory2[NewMenuEntryCategory2["CreateNew"] = 1] = "CreateNew";
@@ -256,6 +252,8 @@ const defaultDavProperties = [
256
252
  "d:getcontenttype",
257
253
  "d:getetag",
258
254
  "d:getlastmodified",
255
+ "d:creationdate",
256
+ "d:displayname",
259
257
  "d:quota-available-bytes",
260
258
  "d:resourcetype",
261
259
  "nc:has-preview",
@@ -408,6 +406,11 @@ const davParsePermissions = function(permString = "") {
408
406
  }
409
407
  return permissions;
410
408
  };
409
+ var FileType = /* @__PURE__ */ ((FileType2) => {
410
+ FileType2["Folder"] = "folder";
411
+ FileType2["File"] = "file";
412
+ return FileType2;
413
+ })(FileType || {});
411
414
  const isDavRessource = function(source, davService) {
412
415
  return source.match(davService) !== null;
413
416
  };
@@ -426,6 +429,9 @@ const validateData = (data, davService) => {
426
429
  if (!data.source.startsWith("http")) {
427
430
  throw new Error("Invalid source format, only http(s) is supported");
428
431
  }
432
+ if (data.displayname && typeof data.displayname !== "string") {
433
+ throw new Error("Invalid displayname type");
434
+ }
429
435
  if (data.mtime && !(data.mtime instanceof Date)) {
430
436
  throw new Error("Invalid mtime type");
431
437
  }
@@ -502,7 +508,12 @@ class Node {
502
508
  };
503
509
  constructor(data, davService) {
504
510
  validateData(data, davService || this._knownDavService);
505
- this._data = { ...data, attributes: {} };
511
+ this._data = {
512
+ // TODO: Remove with next major release, this is just for compatibility
513
+ displayname: data.attributes?.displayname,
514
+ ...data,
515
+ attributes: {}
516
+ };
506
517
  this._attributes = new Proxy(this._data.attributes, this.handler);
507
518
  this.update(data.attributes ?? {});
508
519
  if (davService) {
@@ -532,6 +543,21 @@ class Node {
532
543
  get basename() {
533
544
  return basename(this.source);
534
545
  }
546
+ /**
547
+ * The nodes displayname
548
+ * By default the display name and the `basename` are identical,
549
+ * but it is possible to have a different name. This happens
550
+ * on the files app for example for shared folders.
551
+ */
552
+ get displayname() {
553
+ return this._data.displayname || this.basename;
554
+ }
555
+ /**
556
+ * Set the displayname
557
+ */
558
+ set displayname(displayname) {
559
+ this._data.displayname = displayname;
560
+ }
535
561
  /**
536
562
  * Get this object's extension
537
563
  * There is no setter as the source is not meant to be changed manually.
@@ -694,7 +720,11 @@ class Node {
694
720
  */
695
721
  move(destination) {
696
722
  validateData({ ...this._data, source: destination }, this._knownDavService);
723
+ const oldBasename = this.basename;
697
724
  this._data.source = destination;
725
+ if (this.displayname === oldBasename && this.basename !== oldBasename) {
726
+ this.displayname = this.basename;
727
+ }
698
728
  this.updateMtime();
699
729
  }
700
730
  /**
@@ -740,11 +770,6 @@ class Node {
740
770
  }
741
771
  }
742
772
  }
743
- var FileType = /* @__PURE__ */ ((FileType2) => {
744
- FileType2["Folder"] = "folder";
745
- FileType2["File"] = "file";
746
- return FileType2;
747
- })(FileType || {});
748
773
  class File extends Node {
749
774
  get type() {
750
775
  return FileType.File;
@@ -767,8 +792,21 @@ class Folder extends Node {
767
792
  return "httpd/unix-directory";
768
793
  }
769
794
  }
770
- const davRootPath = `/files/${getCurrentUser()?.uid}`;
771
- const davRemoteURL = generateRemoteUrl("dav");
795
+ function davGetRootPath() {
796
+ if (isPublicShare()) {
797
+ return `/files/${getSharingToken()}`;
798
+ }
799
+ return `/files/${getCurrentUser()?.uid}`;
800
+ }
801
+ const davRootPath = davGetRootPath();
802
+ function davGetRemoteURL() {
803
+ const url = generateRemoteUrl("dav");
804
+ if (isPublicShare()) {
805
+ return url.replace("remote.php", "public.php");
806
+ }
807
+ return url;
808
+ }
809
+ const davRemoteURL = davGetRemoteURL();
772
810
  const davGetClient = function(remoteURL = davRemoteURL, headers = {}) {
773
811
  const client = createClient(remoteURL, { headers });
774
812
  function setHeaders(token) {
@@ -817,9 +855,7 @@ const getFavoriteNodes = (davClient, path = "/", davRoot = davRootPath) => {
817
855
  };
818
856
  const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davRemoteURL) {
819
857
  let userId = getCurrentUser()?.uid;
820
- const isPublic = document.querySelector("input#isPublic")?.value;
821
- if (isPublic) {
822
- userId = userId ?? document.querySelector("input#sharingUserId")?.value;
858
+ if (isPublicShare()) {
823
859
  userId = userId ?? "anonymous";
824
860
  } else if (!userId) {
825
861
  throw new Error("No user id found");
@@ -833,6 +869,7 @@ const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davR
833
869
  source: `${remoteURL}${node.filename}`,
834
870
  mtime: new Date(Date.parse(node.lastmod)),
835
871
  mime: node.mime || "application/octet-stream",
872
+ displayname: props.displayname,
836
873
  size: props?.size || Number.parseInt(props.getcontentlength || "0"),
837
874
  // The fileid is set to -1 for failed requests
838
875
  status: id < 0 ? NodeStatus.FAILED : void 0,
@@ -848,16 +885,81 @@ const davResultToNode = function(node, filesRoot = davRootPath, remoteURL = davR
848
885
  delete nodeData.attributes?.props;
849
886
  return node.type === "file" ? new File(nodeData) : new Folder(nodeData);
850
887
  };
851
- const forbiddenCharacters = window._oc_config?.forbidden_filenames_characters ?? ["/", "\\"];
852
- const forbiddenFilenameRegex = window._oc_config?.blacklist_files_regex ? new RegExp(window._oc_config.blacklist_files_regex) : null;
853
- function isFilenameValid(filename) {
854
- if (forbiddenCharacters.some((character) => filename.includes(character))) {
855
- return false;
888
+ var InvalidFilenameErrorReason = /* @__PURE__ */ ((InvalidFilenameErrorReason2) => {
889
+ InvalidFilenameErrorReason2["ReservedName"] = "reserved name";
890
+ InvalidFilenameErrorReason2["Character"] = "character";
891
+ InvalidFilenameErrorReason2["Extension"] = "extension";
892
+ return InvalidFilenameErrorReason2;
893
+ })(InvalidFilenameErrorReason || {});
894
+ class InvalidFilenameError extends Error {
895
+ constructor(options) {
896
+ super(`Invalid ${options.reason} '${options.segment}' in filename '${options.filename}'`, { cause: options });
856
897
  }
857
- if (forbiddenFilenameRegex !== null && filename.match(forbiddenFilenameRegex)) {
858
- return false;
898
+ /**
899
+ * The filename that was validated
900
+ */
901
+ get filename() {
902
+ return this.cause.filename;
903
+ }
904
+ /**
905
+ * Reason why the validation failed
906
+ */
907
+ get reason() {
908
+ return this.cause.reason;
909
+ }
910
+ /**
911
+ * Part of the filename that caused this error
912
+ */
913
+ get segment() {
914
+ return this.cause.segment;
915
+ }
916
+ }
917
+ function validateFilename(filename) {
918
+ const capabilities = getCapabilities().files;
919
+ const forbiddenCharacters = capabilities.forbidden_filename_characters ?? window._oc_config?.forbidden_filenames_characters ?? ["/", "\\"];
920
+ for (const character of forbiddenCharacters) {
921
+ if (filename.includes(character)) {
922
+ throw new InvalidFilenameError({ segment: character, reason: "character", filename });
923
+ }
924
+ }
925
+ filename = filename.toLocaleLowerCase();
926
+ const forbiddenFilenames = capabilities.forbidden_filenames ?? [".htaccess"];
927
+ if (forbiddenFilenames.includes(filename)) {
928
+ throw new InvalidFilenameError({
929
+ filename,
930
+ segment: filename,
931
+ reason: "reserved name"
932
+ /* ReservedName */
933
+ });
934
+ }
935
+ const endOfBasename = filename.indexOf(".", 1);
936
+ const basename2 = filename.substring(0, endOfBasename === -1 ? void 0 : endOfBasename);
937
+ const forbiddenFilenameBasenames = capabilities.forbidden_filename_basenames ?? [];
938
+ if (forbiddenFilenameBasenames.includes(basename2)) {
939
+ throw new InvalidFilenameError({
940
+ filename,
941
+ segment: basename2,
942
+ reason: "reserved name"
943
+ /* ReservedName */
944
+ });
945
+ }
946
+ const forbiddenFilenameExtensions = capabilities.forbidden_filename_extensions ?? [".part", ".filepart"];
947
+ for (const extension of forbiddenFilenameExtensions) {
948
+ if (filename.length > extension.length && filename.endsWith(extension)) {
949
+ throw new InvalidFilenameError({ segment: extension, reason: "extension", filename });
950
+ }
951
+ }
952
+ }
953
+ function isFilenameValid(filename) {
954
+ try {
955
+ validateFilename(filename);
956
+ return true;
957
+ } catch (error) {
958
+ if (error instanceof InvalidFilenameError) {
959
+ return false;
960
+ }
961
+ throw error;
859
962
  }
860
- return true;
861
963
  }
862
964
  function getUniqueName(name, otherNames, options) {
863
965
  const opts = {
@@ -966,10 +1068,10 @@ function sortNodes(nodes, options = {}) {
966
1068
  ...sortingOptions.sortFavoritesFirst ? [(v) => v.attributes?.favorite !== 1] : [],
967
1069
  // 2: Sort folders first if sorting by name
968
1070
  ...sortingOptions.sortFoldersFirst ? [(v) => v.type !== "folder"] : [],
969
- // 3: Use sorting mode if NOT basename (to be able to use displayName too)
1071
+ // 3: Use sorting mode if NOT basename (to be able to use displayname too)
970
1072
  ...sortingOptions.sortingMode !== "basename" ? [(v) => v[sortingOptions.sortingMode]] : [],
971
- // 4: Use displayName if available, fallback to name
972
- (v) => basename2(v.attributes?.displayName || v.basename),
1073
+ // 4: Use displayname if available, fallback to name
1074
+ (v) => basename2(v.attributes?.displayname || v.basename),
973
1075
  // 5: Finally, use basename if all previous sorting methods failed
974
1076
  (v) => v.basename
975
1077
  ];
@@ -2804,6 +2906,8 @@ export {
2804
2906
  FilesSortingMode,
2805
2907
  Folder,
2806
2908
  Header,
2909
+ InvalidFilenameError,
2910
+ InvalidFilenameErrorReason,
2807
2911
  Navigation,
2808
2912
  NewMenuEntryCategory,
2809
2913
  Node,
@@ -2815,6 +2919,8 @@ export {
2815
2919
  davGetDefaultPropfind,
2816
2920
  davGetFavoritesReport,
2817
2921
  davGetRecentSearch,
2922
+ davGetRemoteURL,
2923
+ davGetRootPath,
2818
2924
  davParsePermissions,
2819
2925
  davRemoteURL,
2820
2926
  davResultToNode,
@@ -2837,5 +2943,6 @@ export {
2837
2943
  registerFileAction,
2838
2944
  registerFileListHeaders,
2839
2945
  removeNewFileMenuEntry,
2840
- sortNodes
2946
+ sortNodes,
2947
+ validateFilename
2841
2948
  };