diff --git a/.gitignore b/.gitignore index 7d065aca061..1c049515fa5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# Ignore Greg's Agent Framework local settings and agents. +# This is a symbolic link to the actual directory, +# which is located outside of the repository. +.agents + /.angular/cache /__build__ /__server_build__ @@ -18,6 +23,7 @@ npm-debug.log *.ngfactory.ts *.css.shim.ts *.scss.shim.ts +*.backup .DS_Store @@ -29,7 +35,7 @@ morgan.log yarn-error.log -*.css +#*.css package-lock.json @@ -41,3 +47,6 @@ package-lock.json junit.xml /src/mirador-viewer/config.local.js + +config/config.development.yaml + diff --git a/angular.json b/angular.json index 5e597d4d307..2d5a508d5e5 100644 --- a/angular.json +++ b/angular.json @@ -41,7 +41,8 @@ "aot": true, "assets": [ "src/assets", - "src/robots.txt" + "src/robots.txt", + "src/clockss.txt" ], "styles": [ "src/styles/startup.scss", diff --git a/config/config.example.yml b/config/config.example.yml index ea38303fa36..a09a46b865b 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -75,7 +75,7 @@ cache: anonymousCache: # Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled. # As all pages are cached in server memory, increasing this value will increase memory needs. - # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. + # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. max: 0 # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached # copy is automatically refreshed on the next request. @@ -100,6 +100,11 @@ auth: # If the rest token expires in less than this amount of time, it will be refreshed automatically. # This is independent from the idle warning. timeLeftBeforeTokenRefresh: 120000 # 2 minutes + # When true, show only the username/password login form and hide all non-password authentication methods. + # Set to true only in environments where password login should be the only login option (e.g. demo). + # Default: false (non-password authentication methods such as OIDC/LDAP/X509 remain available, password form hidden). + # Can also be set via environment variable: DSPACE_AUTH_SHOWPASSWORDLOGIN=true + # showPasswordLogin: false # Form settings form: @@ -246,12 +251,15 @@ browseBy: # The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) defaultLowerLimit: 1900 # If true, thumbnail images for items will be added to BOTH search and browse result lists. - showThumbnails: true + showThumbnails: false # The number of entries in a paginated browse results list. # Rounded to the nearest size in the list of selectable sizes on the # settings menu. pageSize: 20 + # Show the Altmetric badge in the simple item page. + showAltmetricBadge: true + communityList: # No. of communities to list per expansion (show more) pageSize: 20 @@ -376,7 +384,7 @@ vocabularies: vocabulary: 'srsc' enabled: true -# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. +# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. comcolSelectionSort: sortField: 'dc.title' - sortDirection: 'ASC' \ No newline at end of file + sortDirection: 'ASC' diff --git a/config/config.yml b/config/config.yml index b5eecd112f0..7e6f41a1c7c 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,5 +1,9 @@ +ui: + host: 0.0.0.0 rest: ssl: true - host: api7.dspace.org + host: backend.production.deepblue-documents.lib.umich.edu port: 443 nameSpace: /server +item: + showAltmetricBadge: true diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index e5f62600e70..6e2a62d0181 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -70,6 +70,7 @@ services: environment: PGDATA: /pgdata image: dspace/dspace-postgres-pgcrypto + platform: linux/amd64 networks: dspacenet: ports: diff --git a/server.ts b/server.ts index 23327c2058e..43958213260 100644 --- a/server.ts +++ b/server.ts @@ -69,6 +69,16 @@ const cookieParser = require('cookie-parser'); const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json')); +// List of paths for SSR; you can use regex for more flexibility if needed +const ssrPaths = [ + '/documents', + '/items/', + '/collections/', + '/communities/', + '/bitstream/', + '/bitstreams/' +]; + // cache of SSR pages for known bots, only enabled in production mode let botCache: LRU; @@ -78,6 +88,15 @@ let anonymousCache: LRU; // extend environment with app config for server extendEnvironmentWithAppConfig(environment, appConfig); + +// Helper function to extract client IP +function getClientIp(req) { + // Extracting IP from X-Forwarded-For if available + const xForwardedFor = req.headers['x-forwarded-for']; + return xForwardedFor ? xForwardedFor.split(',')[0] : req.connection.remoteAddress; +} + + // The Express app is exported so that it can be used by serverless Functions. export function app() { @@ -90,7 +109,9 @@ export function app() { // Tell Express to trust X-FORWARDED-* headers from proxies // See https://expressjs.com/en/guide/behind-proxies.html - server.set('trust proxy', environment.ui.useProxies); + // server.set('trust proxy', environment.ui.useProxies); +server.set('trust proxy', true); + /* * If production mode is enabled in the environment file: @@ -130,6 +151,7 @@ export function app() { server.engine('html', (_, options, callback) => ngExpressEngine({ bootstrap: ServerAppModule, + inlineCriticalCss: false, providers: [ { provide: REQUEST, @@ -159,12 +181,26 @@ export function app() { * Serve the robots.txt ejs template, filling in the origin variable */ server.get('/robots.txt', (req, res) => { + const clientIp = getClientIp(req); + console.log(`Client IP: ${clientIp}`); + res.setHeader('content-type', 'text/plain'); res.render('assets/robots.txt.ejs', { 'origin': req.protocol + '://' + req.headers.host }); }); + server.get('/clockss.txt', (req, res) => { + const clientIp = getClientIp(req); + console.log(`Client IP: ${clientIp}`); + + res.setHeader('content-type', 'text/plain'); + res.render('assets/clockss.txt.ejs', { + 'origin': req.protocol + '://' + req.headers.host + }); + }); + + /* * Set views folder path to directory where template files are stored */ @@ -227,25 +263,84 @@ export function app() { */ router.get('*', cacheCheck, ngApp); + server.use(environment.ui.nameSpace, router); +server.set('trust proxy', true); + return server; } +// Check if the request URL starts with any of the SSR paths +function shouldUseSSR(url) { + // First, check for the browse exclusion patterns + const browseBlacklistRegexes = [ + /^\/communities\/[a-f0-9-]{36}\/browse(\/.*)?$/i, + /^\/collections\/[a-f0-9-]{36}\/browse(\/.*)?$/i, + ]; + + // If the URL contains 'handle' anywhere, return false + if (/\/handle(\/|$)/i.test(url)) { + return false; + } + + // If the URL contains '/2027.42' anywhere, return false + if (/\/2027\.42(\/|$)/i.test(url)) { + return false; + } + + // If the url matches any exclusion, do NOT use SSR + if (browseBlacklistRegexes.some(regex => regex.test(url))) { + return false; + } + + // Otherwise + return ssrPaths.some(path => url.startsWith(path)); +} + + /* * The callback function to serve server side angular */ function ngApp(req, res) { - if (environment.universal.preboot) { - // Render the page to user via SSR (server side rendering) + + console.log('SSR: Original Request= ' + req.url); + + // If URL contains '/assets/', redirect to Deep Blue Repositories + if (req.url.includes('/assets/')) { + res.writeHead(404, { + 'Location': 'https://www.lib.umich.edu/collections/deep-blue-repositories' + }); + res.end(); + return; // Exit after redirecting + + } + + if ( + req.url.includes('/discover') || + req.url.includes('/Mirage2') || + req.url.includes('/feed/') || + req.url.includes('/search-filter') + ) { + res.writeHead(404, { + 'Location': 'https://www.lib.umich.edu/collections/deep-blue-repositories' + }); + res.end(); + return; // Exit after redirecting + } + + if (environment.universal.preboot && req.method === 'GET' && shouldUseSSR(req.url)) { + // Only run SSR for matching paths + console.log('SSR: Using SSR= ' + req.url); serverSideRender(req, res); } else { - // If preboot is disabled, just serve the client - console.log('Universal off, serving for direct client-side rendering (CSR)'); + // For all other paths, serve CSR + console.log('SSR:Universal off or path excluded. Serving direct client-side rendering (CSR)' + req.url); clientSideRender(req, res); } } + /** * Render page content on server side using Angular SSR. By default this page content is * returned to the user. @@ -255,6 +350,22 @@ function ngApp(req, res) { * If false, then only save this rendered content to the in-memory cache (to refresh cache). */ function serverSideRender(req, res, sendToUser: boolean = true) { + + + // Check if 'X-Forwarded-For' is set; if not, set it to the request's IP address. +const xForwardedForHeader = req.headers['x-forwarded-for']; + +console.log("xForwardedForHeader=" + xForwardedForHeader); + +if (!xForwardedForHeader) { + // Consider appending instead if chaining proxies + console.log("req.ip=" + req.ip); + + // Example of appending to support proxy chains: + req.headers['x-forwarded-for'] = req.ip; +} + + // Render the page via SSR (server side rendering) res.render(indexHtml, { req, @@ -535,7 +646,7 @@ function createHttpsServer(keys) { function run() { const port = environment.ui.port || 4000; - const host = environment.ui.host || '/'; + const host = '0.0.0.0'; // Start up the Node server const server = app(); diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index e3a8e2c590f..255cbac0d61 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -5,7 +5,7 @@
- diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts index 62fbb3ee3df..426df9b6309 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts @@ -4,6 +4,8 @@ import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-c import { Collection } from '../../core/shared/collection.model'; import { getCollectionPageRoute } from '../collection-page-routing-paths'; +import { HttpClient} from '@angular/common/http'; + /** * Component that represents the page where a user can edit an existing Collection */ @@ -15,10 +17,11 @@ export class EditCollectionPageComponent extends EditComColPageComponent -

{{ 'communityList.title' | translate }}

+

{{ 'communityList.title' | translate }}

diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index 99e9dbeb0de..7ff1082ef3f 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -96,6 +96,7 @@ export class CommunityListService { private collectionDataService: CollectionDataService, private store: Store ) { + // This is where the 20 communities listed on the community/collection page comes from. this.pageSize = appConfig.communityList.pageSize; } diff --git a/src/app/community-page/edit-community-page/edit-community-page.component.ts b/src/app/community-page/edit-community-page/edit-community-page.component.ts index 54a6ee49442..20e11286309 100644 --- a/src/app/community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/community-page/edit-community-page/edit-community-page.component.ts @@ -4,6 +4,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component'; import { getCommunityPageRoute } from '../community-page-routing-paths'; +import { HttpClient} from '@angular/common/http'; + /** * Component that represents the page where a user can edit an existing Community */ @@ -15,10 +17,11 @@ export class EditCommunityPageComponent extends EditComColPageComponent typeof type === 'string') + .find((type: string) => Object.keys(this.factories).includes(type)) as string; + + let name; + if (hasValue(match)) { + name = this.factories[match](dso); + } + if (isEmpty(name)) { + name = this.factories.Default(dso); + } + + if ( name.endsWith(".pdf") ) + { + return "PDF file"; + } + else if ( (name.endsWith(".txt") ) || (name.endsWith(".asc") ) ) + { + return "TXT file"; + } + else if ( (name.endsWith(".doc") ) || (name.endsWith(".docx") ) ) + { + return "MS Word file"; + } + else if ( (name.endsWith(".pptx") ) || (name.endsWith(".ppt") ) ) + { + return "MS Powerpoint file"; + } + else if ( (name.endsWith(".xlsx") ) || (name.endsWith(".xls") ) ) + { + return "MS Excel file"; + } + else if ( (name.endsWith(".jpeg") ) || (name.endsWith(".jpg") ) ) + { + return "JPEG file"; + } + else if ( (name.endsWith(".tiff") ) || (name.endsWith(".tif") ) ) + { + return "TIFF file"; + } + else if ( (name.endsWith(".gif") ) || (name.endsWith(".png") ) || (name.endsWith(".jp2") ) || (name.endsWith(".pcd") ) ) + { + return "Image file"; + } + else if ( (name.endsWith(".mov") ) || (name.endsWith(".qt") ) ) + { + return "Video file"; + } + else if ( name.endsWith(".zip") ) + { + return "Zip file"; + } + else if ( (name.endsWith(".htm") ) || (name.endsWith(".html") ) ) + { + return "HTML file";; + } + else if ( (name.endsWith(".wav") ) || (name.endsWith(".aiff") ) || + (name.endsWith(".aif") ) || (name.endsWith(".au") ) || + (name.endsWith(".aifc") ) || (name.endsWith(".snd") ) || + (name.endsWith(".ra") ) || (name.endsWith(".ram") ) || + (name.endsWith(".mpa") ) || (name.endsWith(".abs") ) || + (name.endsWith(".mpga") ) || (name.endsWith(".mp3") ) || + (name.endsWith(".ma4") ) ) + { + return "Audio file"; + } + else + { + return "Unidentified file format"; + } + } else { + return ''; + } + } + /** * Gets the Hit highlight * diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index dbca773375a..067227cb430 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -86,6 +86,7 @@ import { ItemType } from './shared/item-relationships/item-type.model'; import { RelationshipType } from './shared/item-relationships/relationship-type.model'; import { Relationship } from './shared/item-relationships/relationship.model'; import { Item } from './shared/item.model'; + import { License } from './shared/license.model'; import { ResourcePolicy } from './resource-policy/models/resource-policy.model'; import { SearchConfigurationService } from './shared/search/search-configuration.service'; @@ -318,6 +319,7 @@ export const models = Bitstream, BitstreamFormat, Item, + ItemRequest, Site, Collection, Community, diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index 89178f8dd21..45dfb8a68de 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -49,7 +49,7 @@ describe('BitstreamDataService', () => { id: '2', shortDescription: 'PNG', description: 'Portable Network Graphics', - supportLevel: BitstreamFormatSupportLevel.Known + supportLevel: BitstreamFormatSupportLevel.AS_IS_KNOWN }); const url = 'fake-bitstream-url'; diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index c3fa84dd6c8..f81cffe77ad 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -149,10 +149,12 @@ export abstract class BaseItemDataService extends IdentifiableDataService * @param item * @param withdrawn */ - public setWithDrawn(item: Item, withdrawn: boolean): Observable> { + +////// + public setWithDrawn(item: Item, withdrawn: boolean, reason: string): Observable> { const patchOperation = { - op: 'replace', path: '/withdrawn', value: withdrawn + op: 'replace', path: '/withdrawn', value: withdrawn, reason: reason } as Operation; this.requestService.removeByHrefSubstring('/discover'); diff --git a/src/app/core/forward-client-ip/forward-client-ip.interceptor.ts b/src/app/core/forward-client-ip/forward-client-ip.interceptor.ts index 2e2e59fa6b8..6b9da58493e 100644 --- a/src/app/core/forward-client-ip/forward-client-ip.interceptor.ts +++ b/src/app/core/forward-client-ip/forward-client-ip.interceptor.ts @@ -18,6 +18,19 @@ export class ForwardClientIpInterceptor implements HttpInterceptor { */ intercept(httpRequest: HttpRequest, next: HttpHandler): Observable> { const clientIp = this.req.get('x-forwarded-for') || this.req.connection.remoteAddress; - return next.handle(httpRequest.clone({ setHeaders: { 'X-Forwarded-For': clientIp } })); +// return next.handle(httpRequest.clone({ setHeaders: { 'X-Forwarded-For': clientIp } })); + +// This came from here: https://github.com/DSpace/dspace-angular/issues/2902 +// UM Change. I was having issues getting the referer, aske Tim about it and +// he pointed me to this. This is not solve the issue, but seems like it would +// be good to have anyway + const headers = { 'X-Forwarded-For': clientIp }; + // if the request has a user-agent retain it + const userAgent = this.req.get('user-agent'); + if (userAgent) { + headers['User-Agent'] = userAgent; + } + + return next.handle(httpRequest.clone({ setHeaders: headers })); } } diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 204c925e6bb..ceea18e4a1a 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -167,6 +167,8 @@ export class MetadataService { this.setCitationDissertationNameTag(); } + this.setRightsTag(); + // this.setCitationJournalTitleTag(); // this.setCitationVolumeTag(); // this.setCitationIssueTag(); @@ -210,6 +212,28 @@ export class MetadataService { this.addMetaTag('citation_title', value); } + private setRightsTag(): void { + const value = this.getMetaTagValue('dc.rights.robot'); + + let rights: string + if ( value === undefined ) + { + rights = "index,nofollow"; + } + else + { + if ( value === "IndexNoFollow" ) + { + rights = "index,nofollow"; + } + if ( value === "NoIndexNoFollow" ) + { + rights = "noindex,nofollow"; + } + } + this.addMetaTag('rights', rights); + } + /** * Add to the */ diff --git a/src/app/core/reload/reload.guard.ts b/src/app/core/reload/reload.guard.ts index 1e99a5687a3..9566aa9ab0d 100644 --- a/src/app/core/reload/reload.guard.ts +++ b/src/app/core/reload/reload.guard.ts @@ -27,7 +27,7 @@ export class ReloadGuard implements CanActivate { : route.queryParams.redirect; return this.router.parseUrl(url); } else { - return this.router.createUrlTree(['home']); + return this.router.createUrlTree(['documents']); } } } diff --git a/src/app/core/shared/bitstream-format-support-level.ts b/src/app/core/shared/bitstream-format-support-level.ts index d92aac77083..c687fc55c58 100644 --- a/src/app/core/shared/bitstream-format-support-level.ts +++ b/src/app/core/shared/bitstream-format-support-level.ts @@ -1,5 +1,6 @@ export enum BitstreamFormatSupportLevel { - Known = 'KNOWN', - Unknown = 'UNKNOWN', - Supported = 'SUPPORTED' + AS_IS_UNKNOWN = 'AS_IS_UNKNOWN', + LIMITED = 'LIMITED', + HIGHEST_LEVEL = 'HIGHEST_LEVEL', + AS_IS_KNOWN = 'AS_IS_KNOWN' } diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index c855325d2d6..fe3256d64e3 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -22,6 +22,14 @@ export class Bitstream extends DSpaceObject implements ChildHALResource { @autoserialize sizeBytes: number; + // UM Change - For hidden file + @autoserialize + formatId: number; + + @autoserialize + sequenceId: number; + // End UM Change + /** * The description of this Bitstream */ diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 20fc275ee28..38ee1d01e15 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -117,6 +117,19 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject @link(BITSTREAM, false, 'thumbnail') thumbnail?: Observable>; + get withdrawReason(): string { + let reason : string; + reason = this.firstMetadataValue('dc.description.withdrawalreason'); + if ( reason ) + { + return reason; + } + else + { + return ''; + } + } + /** * The access status for this Item * Will be undefined unless the access status {@link HALLink} has been resolved. diff --git a/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts b/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts index 177473b7d58..5a1ec912aa2 100644 --- a/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts @@ -29,6 +29,20 @@ export class WorkspaceitemSectionUploadFileObject { value: string; }; + /** not sure this is needed UM */ + /** + * The file format information + */ + format: { + shortDescription: string, + description: string, + mimetype: string, + supportLevel: string, + internal: boolean, + type: string + }; + + /** * The file url */ diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html index 24c3dc5cd7a..3ac8b83f67f 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -1,6 +1,6 @@