Skip to content

Commit 4c2fcc3

Browse files
committed
Merge remote-tracking branch 'bitbucket4science/dspace-cris-2025_02_x' into task/dspace-cris-2025_02_x/DSC-2256_dspace9
# Conflicts: # package.json # src/app/app-routes.ts # src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/orcid/orcid.component.html # src/app/info/end-user-agreement/end-user-agreement.component.ts # src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.html # src/app/item-page/simple/field-components/specific-field/cc-license/item-page-cc-license-field.component.ts # src/app/item-page/simple/item-page.component.html # src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts # src/assets/i18n/de.json5 # src/assets/i18n/es.json5 # src/assets/i18n/fr.json5 # yarn.lock
2 parents 8185d18 + 76c61e1 commit 4c2fcc3

55 files changed

Lines changed: 810 additions & 102 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bitbucket-pipelines.yml

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ definitions:
105105
- node-2024-02-x
106106
- cypress-2024-02-x
107107
script:
108+
- apt-get update && apt-get install -y curl
108109
- export HASH_COMMIT=${BITBUCKET_COMMIT:0:8}
109110
- echo "Running tests for commit $HASH_COMMIT"
110111
- export DSPACE_REST_HOST=${E2E_RUNNER_HOST}
@@ -120,6 +121,27 @@ definitions:
120121
- export CYPRESS_CACHE_FOLDER=~/.cache/Cypress
121122
- export CHROME_FLAGS="--no-sandbox --disable-dev-shm-usage --disable-gpu"
122123
- export NODE_OPTIONS="--max-old-space-size=4096"
124+
- |
125+
MAX_RETRIES=10
126+
RETRY_COUNT=0
127+
SUCCESS=false
128+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
129+
echo "Pinging REST endpoint... (Attempt $((RETRY_COUNT+1)))"
130+
STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://$DSPACE_REST_HOST$DSPACE_REST_NAMESPACE")
131+
if [ "$STATUS_CODE" -lt 400 ]; then
132+
echo "REST endpoint is up! Status code: $STATUS_CODE"
133+
SUCCESS=true
134+
break
135+
else
136+
echo "REST endpoint not ready (Status code: $STATUS_CODE). Retrying in 60 seconds..."
137+
RETRY_COUNT=$((RETRY_COUNT+1))
138+
sleep 60
139+
fi
140+
done
141+
if [ "$SUCCESS" = "false" ]; then
142+
echo "REST endpoint did not become available after $MAX_RETRIES attempts. Failing the build."
143+
exit 1
144+
fi
123145
- npx cypress install
124146
- yarn serve:ssr &
125147
- echo "Waiting for server to start..."
@@ -253,7 +275,7 @@ definitions:
253275
- export AWS_DEFAULT_REGION=$AWS_REGION
254276
- export CLOUDFRONT_NAME=$(echo "$BITBUCKET_BRANCH" | awk -F'/' '{if(NF==1)val=$1;else if(NF==2)val=$2;else if(NF==3)val=$2;else val=$3;gsub(/_/, "-", val);print tolower(val)}')
255277
- export CLOUDFRONT_DISTRIBUTION_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?Comment=='dev--${CLOUDFRONT_NAME}'].Id" --output text)
256-
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*"
278+
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*" || echo "CloudFront distribution $CLOUDFRONT_DISTRIBUTION_ID not found — skipping invalidation."
257279

258280
- step: &find-and-invalidate-cloudfront-staging
259281
name: Invalidate CloudFront Staging Cache
@@ -264,7 +286,7 @@ definitions:
264286
- export AWS_DEFAULT_REGION=$AWS_REGION
265287
- export CLOUDFRONT_NAME=$(echo "$BITBUCKET_BRANCH" | awk -F'/' '{if(NF==1)val=$1;else if(NF==2)val=$2;else if(NF==3)val=$2;else val=$3;gsub(/_/, "-", val);print tolower(val)}')
266288
- export CLOUDFRONT_DISTRIBUTION_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?Comment=='staging--${CLOUDFRONT_NAME}'].Id" --output text)
267-
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*"
289+
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*" || echo "CloudFront distribution $CLOUDFRONT_DISTRIBUTION_ID not found — skipping invalidation."
268290

269291
- step: &find-and-invalidate-cloudfront-test
270292
name: Invalidate CloudFront Test Cache
@@ -275,7 +297,7 @@ definitions:
275297
- export AWS_DEFAULT_REGION=$AWS_REGION
276298
- export CLOUDFRONT_NAME=$(echo "$BITBUCKET_BRANCH" | awk -F'/' '{if(NF==1)val=$1;else if(NF==2)val=$2;else if(NF==3)val=$2;else val=$3;gsub(/_/, "-", val);print tolower(val)}')
277299
- export CLOUDFRONT_DISTRIBUTION_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?Comment=='test--${CLOUDFRONT_NAME}'].Id" --output text)
278-
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*"
300+
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*" || echo "CloudFront distribution $CLOUDFRONT_DISTRIBUTION_ID not found — skipping invalidation."
279301

280302
pipelines:
281303
custom:
@@ -302,7 +324,7 @@ pipelines:
302324
- step: *angular-build
303325
- parallel: *parallel-run-tests
304326
- step: *build-and-push
305-
'dspace-cris-2024_02_x':
327+
'dspace-cris-2025_02_x':
306328
- step: *preliminary-operation
307329
- step: *angular-build
308330
- parallel: *parallel-run-tests

cypress/e2e/header.cy.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,33 @@ describe('Header', () => {
1414
it('should allow for changing language to German (for example)', () => {
1515
cy.visit('/');
1616

17-
// Click the language switcher (globe) in header
18-
cy.get('button[data-test="lang-switch"]').click();
19-
// Click on the "Deusch" language in dropdown
20-
cy.get('#language-menu-list div[role="option"]').contains('Deutsch').click();
17+
// This test should only run if the language switcher is visible.
18+
// We query for the body first, so the test doesn't fail if the button doesn't exist.
19+
cy.get('body').then($body => {
20+
if ($body.find('button[data-test="lang-switch"]').is(':visible')) {
2121

22-
// HTML "lang" attribute should switch to "de"
23-
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
22+
// Click the language switcher (globe) in header
23+
cy.get('button[data-test="lang-switch"]').click();
24+
// Click on the "Deutsch" language in dropdown
25+
cy.get('#language-menu-list div[role="option"]').contains('Deutsch').click();
2426

25-
// Login menu should now be in German
26-
cy.get('[data-test="login-menu"]').contains('Anmelden');
27+
// HTML "lang" attribute should switch to "de"
28+
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
2729

28-
// Change back to English from language switcher
29-
cy.get('button[data-test="lang-switch"]').click();
30-
cy.get('#language-menu-list div[role="option"]').contains('English').click();
30+
// Login menu should now be in German
31+
cy.get('[data-test="login-menu"]').contains('Anmelden');
3132

32-
// HTML "lang" attribute should switch to "en"
33-
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
33+
// Change back to English from language switcher
34+
cy.get('button[data-test="lang-switch"]').click();
35+
cy.get('#language-menu-list div[role="option"]').contains('English').click();
3436

35-
// Login menu should now be in English
36-
cy.get('[data-test="login-menu"]').contains('Log In');
37+
// HTML "lang" attribute should switch to "en"
38+
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
39+
40+
// Login menu should now be in English
41+
cy.get('[data-test="login-menu"]').contains('Log In');
42+
}
43+
});
3744
});
45+
3846
});

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dspace-angular",
3-
"version": "2025.02.01-SNAPSHOT",
3+
"version": "2025.02.00-SNAPSHOT",
44
"scripts": {
55
"ng": "ng",
66
"config:watch": "nodemon",
@@ -177,6 +177,7 @@
177177
"markdown-it": "^13.0.1",
178178
"mirador": "^3.4.3",
179179
"mirador-dl-plugin": "^0.13.0",
180+
"mirador-imagecropper": "^0.1.9",
180181
"mirador-share-plugin": "^0.16.0",
181182
"morgan": "^1.10.0",
182183
"ng2-file-upload": "7.0.1",

server.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ export function app() {
255255
*/
256256
server.get('/app/client/health', clientHealthCheck);
257257

258+
/**
259+
* Redirecting old manifest
260+
*/
261+
server.get('/json/iiif/**/manifest', redirectManifest);
262+
258263
/**
259264
* Default sending all incoming requests to ngApp() function, after first checking for a cached
260265
* copy of the page (see cacheCheck())
@@ -721,6 +726,39 @@ function healthCheck(req, res) {
721726
});
722727
}
723728

729+
/*
730+
* The callback function to redirect old manifest
731+
*/
732+
function redirectManifest(req, res) {
733+
console.info('Redirecting old manifest');
734+
const url = req.url;
735+
const regex = /json\/iiif\/([^\/]+\/[^\/]+)(?:\/([^\/]+))?\/manifest/;
736+
const match = url.match(regex);
737+
let handle;
738+
let id;
739+
740+
if (match) {
741+
handle = match[1];
742+
const baseUrl = `${environment.rest.baseUrl}/api/pid/find?id=${handle}`;
743+
axios.get(baseUrl)
744+
.then((response) => {
745+
if (response.status === 200) {
746+
const newUrl = `${environment.rest.baseUrl}/iiif/${response.data.id}/manifest`;
747+
console.info('Manifest found, redirect to ', newUrl);
748+
res.redirect(newUrl);
749+
}
750+
})
751+
.catch((error) => {
752+
res.status(error.response.status).send({
753+
error: error.message
754+
});
755+
});
756+
} else {
757+
res.status(422).send({
758+
error: 'Wrong handle'
759+
});
760+
}
761+
}
724762

725763
// Webpack will replace 'require' with '__webpack_require__'
726764
// '__non_webpack_require__' is a proxy to Node 'require'

src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
301301
name: 'email',
302302
validators: {
303303
required: null,
304-
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$',
304+
pattern: '^\\s*[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}\\s*$',
305305
},
306306
required: true,
307307
errorMessages: {
@@ -420,7 +420,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
420420
},
421421
],
422422
},
423-
email: this.email.value,
423+
email: (this.email.value as string)?.trim(),
424424
canLogIn: this.canLogIn.value,
425425
requireCertificate: this.requireCertificate.value,
426426
};

src/app/app-routes.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
4343
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
4444
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
4545
import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
46+
import { RedirectService } from './redirect/redirect.service';
4647
import { viewTrackerResolver } from './statistics/angulartics/dspace/view-tracker.resolver';
4748
import { provideSubmissionState } from './submission/provide-submission-state';
4849
import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
@@ -183,12 +184,12 @@ export const APP_ROUTES: Route[] = [
183184
canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard],
184185
},
185186
{
186-
path: 'standard-login',
187+
path: 'admin-only-login',
187188
loadChildren: () => import('./login-page/login-page-routes').then((m) => m.ROUTES),
188189
data: {
189190
isBackDoor: true,
190191
},
191-
canMatch: [() => !environment.auth.disableStandardLogin],
192+
canMatch: [() => environment.auth.isPasswordLoginEnabledForAdminsOnly],
192193
},
193194
{
194195
path: 'login',
@@ -326,7 +327,7 @@ export const APP_ROUTES: Route[] = [
326327
.then((m) => m.ROUTES),
327328
},
328329
{
329-
path: 'luck-search',
330+
path: 'lucky-search',
330331
loadChildren: () => import('./lucky-search/lucky-search-routes')
331332
.then((m) => m.ROUTES),
332333
},
@@ -350,7 +351,7 @@ export const APP_ROUTES: Route[] = [
350351
loadChildren: () => import('./external-login-email-confirmation-page/external-login-email-confirmation-page-routes')
351352
.then((m) => m.ROUTES),
352353
},
353-
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent, data: { title: PAGE_NOT_FOUND_PATH } },
354+
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent, data: { title: PAGE_NOT_FOUND_PATH }, canActivate: [RedirectService] },
354355
],
355356
},
356357
];

src/app/app.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { AuthInterceptor } from './core/auth/auth.interceptor';
5959
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
6060
import { LocaleInterceptor } from './core/locale/locale.interceptor';
6161
import { LogInterceptor } from './core/log/log.interceptor';
62+
import { schemaModels } from './core/metadata/schema-json-ld/schema-types/provide-schema';
6263
import {
6364
models,
6465
provideCore,
@@ -178,3 +179,4 @@ const metadataRepresentations = METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP;
178179
const startsWithDecoratorMap = STARTS_WITH_DECORATOR_MAP;
179180
const browseByDecoratorMap = BROWSE_BY_DECORATOR_MAP;
180181
const authMethodForDecoratorMap = AUTH_METHOD_FOR_DECORATOR_MAP;
182+
const schemaModelList = schemaModels;

src/app/core/metadata/schema-json-ld/schema-json-ld.service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Inject,
44
Injectable,
55
} from '@angular/core';
6+
import { DomSanitizer } from '@angular/platform-browser';
67

78
import {
89
isEmpty,
@@ -20,7 +21,10 @@ import {
2021
export class SchemaJsonLDService {
2122
static scriptType = 'application/ld+json';
2223

23-
constructor(@Inject(DOCUMENT) private _document: Document) {}
24+
constructor(
25+
@Inject(DOCUMENT) private _document: Document,
26+
protected sanitizer: DomSanitizer,
27+
) {}
2428

2529
removeStructuredData(): void {
2630
const els = [];
@@ -66,7 +70,7 @@ export class SchemaJsonLDService {
6670
}
6771

6872
if (isNotEmpty(constructor)) {
69-
const provider: SchemaType = new constructor();
73+
const provider: SchemaType = new constructor(this.sanitizer);
7074
return provider.getSchema(item);
7175
} else {
7276
return null;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { PersonSchemaType } from './Person/person-schema-type';
2+
import { ProductCreativeWorkSchemaType } from './product/product-creative-work-schema-type';
3+
import { ProductDatasetSchemaType } from './product/product-dataset-schema-type';
4+
import { PublicationBookSchemaType } from './publication/publication-book-schema-type';
5+
import { PublicationChapterSchemaType } from './publication/publication-chapter-schema-type';
6+
import { PublicationCreativeWorkSchemaType } from './publication/publication-creative-work-schema-type';
7+
import { PublicationReportSchemaType } from './publication/publication-report-schema-type';
8+
import { PublicationScholarlyArticleSchemaType } from './publication/publication-scholarly-article-schema-type';
9+
import { PublicationThesisSchemaType } from './publication/publication-thesis-schema-type';
10+
11+
/**
12+
* Declaration needed to make sure all decorator functions are called in time
13+
*/
14+
export const schemaModels = [
15+
PersonSchemaType,
16+
ProductCreativeWorkSchemaType,
17+
ProductDatasetSchemaType,
18+
PublicationBookSchemaType,
19+
PublicationChapterSchemaType,
20+
PublicationCreativeWorkSchemaType,
21+
PublicationReportSchemaType,
22+
PublicationScholarlyArticleSchemaType,
23+
PublicationThesisSchemaType,
24+
];

src/app/core/metadata/schema-json-ld/schema-types/schema-type.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import { SecurityContext } from '@angular/core';
2+
import { DomSanitizer } from '@angular/platform-browser';
13
import isObject from 'lodash/isObject';
24

35
import { isNotEmpty } from '../../../../shared/empty.util';
46
import { Item } from '../../../shared/item.model';
57

68
export abstract class SchemaType {
9+
constructor(protected sanitizer: DomSanitizer) {}
10+
711
protected abstract createSchema(item: Item): Record<string, any>;
812
protected abstract createSchema(item: Item): Record<string, any>;
913

@@ -31,7 +35,35 @@ export abstract class SchemaType {
3135
}
3236
}
3337

38+
protected sanitizeSchema(obj: any): Record<string, any> {
39+
if (Array.isArray(obj)) {
40+
return obj.map(v =>
41+
typeof v === 'string'
42+
? this.sanitizer.sanitize(SecurityContext.HTML, v)
43+
: this.sanitizeSchema(v),
44+
);
45+
}
46+
47+
if (typeof obj === 'object' && obj !== null) {
48+
const sanitized: Record<string, any> = {};
49+
for (const key in obj) {
50+
if (obj.hasOwnProperty(key)) {
51+
const value = obj[key];
52+
sanitized[key] =
53+
typeof value === 'string'
54+
? this.sanitizer.sanitize(SecurityContext.HTML, value)
55+
: this.sanitizeSchema(value);
56+
}
57+
}
58+
return sanitized;
59+
}
60+
61+
return obj;
62+
}
63+
64+
3465
getSchema(item: Item): Record<string, any> {
35-
return SchemaType.removeEmpty(this.createSchema(item));
66+
const sanitizedRaw = this.sanitizeSchema(this.createSchema(item));
67+
return SchemaType.removeEmpty(sanitizedRaw);
3668
}
3769
}

0 commit comments

Comments
 (0)