• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanodash / 23133101585

16 Mar 2026 07:45AM UTC coverage: 15.984% (+0.2%) from 15.811%
23133101585

Pull #402

github

web-flow
Merge bd8288c47 into 39c6ac11c
Pull Request #402: Fix unbounded memory growth and resource exhaustion

717 of 5509 branches covered (13.02%)

Branch coverage included in aggregate %.

1809 of 10294 relevant lines covered (17.57%)

2.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

14.03
src/main/java/com/knowledgepixels/nanodash/NanodashSession.java
1
package com.knowledgepixels.nanodash;
2

3
import com.knowledgepixels.nanodash.component.NanopubResults;
4
import com.knowledgepixels.nanodash.component.PublishForm;
5
import com.knowledgepixels.nanodash.domain.User;
6
import com.knowledgepixels.nanodash.page.OrcidLoginPage;
7
import com.knowledgepixels.nanodash.page.ProfilePage;
8
import jakarta.servlet.http.HttpServletRequest;
9
import jakarta.servlet.http.HttpSession;
10
import jakarta.xml.bind.DatatypeConverter;
11
import org.apache.commons.io.FileUtils;
12
import org.apache.wicket.Session;
13
import org.apache.wicket.protocol.http.WebSession;
14
import org.apache.wicket.request.Request;
15
import org.apache.wicket.request.flow.RedirectToUrlException;
16
import org.apache.wicket.request.mapper.parameter.PageParameters;
17
import org.eclipse.rdf4j.model.IRI;
18
import org.eclipse.rdf4j.model.ValueFactory;
19
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
20
import org.nanopub.extra.security.*;
21
import org.nanopub.extra.setting.IntroNanopub;
22
import org.slf4j.Logger;
23
import org.slf4j.LoggerFactory;
24

25
import java.io.File;
26
import java.io.IOException;
27
import java.io.Serializable;
28
import java.nio.charset.StandardCharsets;
29
import java.security.KeyPair;
30
import java.util.Date;
31
import java.util.Collections;
32
import java.util.LinkedHashMap;
33
import java.util.List;
34
import java.util.Map;
35
import java.util.concurrent.ConcurrentHashMap;
36
import java.util.concurrent.ConcurrentMap;
37

38
import org.apache.wicket.markup.html.WebPage;
39
import org.nanopub.Nanopub;
40

41
/**
42
 * Represents a session in the Nanodash application.
43
 */
44
public class NanodashSession extends WebSession {
45

46
    private transient HttpSession httpSession;
47
    private static final Logger logger = LoggerFactory.getLogger(NanodashSession.class);
9✔
48

49
    /**
50
     * Retrieves the current Nanodash session.
51
     *
52
     * @return The current NanodashSession instance.
53
     */
54
    public static NanodashSession get() {
55
        return (NanodashSession) Session.get();
×
56
    }
57

58
    /**
59
     * Constructs a new NanodashSession for the given request.
60
     * Initializes the HTTP session and loads profile information.
61
     *
62
     * @param request The HTTP request.
63
     */
64
    public NanodashSession(Request request) {
65
        super(request);
9✔
66
        httpSession = ((HttpServletRequest) request.getContainerRequest()).getSession();
18✔
67
        bind();
6✔
68
        loadProfileInfo();
6✔
69
    }
3✔
70

71
    private static ValueFactory vf = SimpleValueFactory.getInstance();
9✔
72

73
//        private IntroExtractor introExtractor;
74

75
    private String userDir = System.getProperty("user.home") + "/.nanopub/";
15✔
76
    private NanopubResults.ViewMode nanopubResultsViewMode = NanopubResults.ViewMode.LIST;
9✔
77

78
    private KeyPair keyPair;
79
    private IRI userIri;
80
    private ConcurrentMap<IRI, IntroNanopub> introNps;
81
//        private Boolean isOrcidLinked;
82
//        private String orcidLinkError;
83

84
    private Integer localIntroCount = null;
9✔
85
    private IntroNanopub localIntro = null;
9✔
86

87
    private Date lastTimeIntroPublished = null;
9✔
88

89
    // We should store here some sort of form model and not the forms themselves, but I couldn't figure
90
    // how to do it, so doing it like this for the moment...
91
    private static final int MAX_FORMS = 20;
92
    private final Map<String, PublishForm> formMap = Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) {
57✔
93
        @Override
94
        protected boolean removeEldestEntry(Map.Entry<String, PublishForm> eldest) {
95
            return size() > MAX_FORMS;
×
96
        }
97
    });
98

99
    /**
100
     * Associates a form object with a specific ID.
101
     *
102
     * @param formObjId The ID of the form object.
103
     * @param formObj   The form object to associate.
104
     */
105
    public void setForm(String formObjId, PublishForm formObj) {
106
        formMap.put(formObjId, formObj);
×
107
    }
×
108

109
    /**
110
     * Checks if a form object with the given ID exists.
111
     *
112
     * @param formObjId The ID of the form object.
113
     * @return True if the form object exists, false otherwise.
114
     */
115
    public boolean hasForm(String formObjId) {
116
        return formMap.containsKey(formObjId);
×
117
    }
118

119
    /**
120
     * Retrieves the form object associated with the given ID.
121
     *
122
     * @param formObjId The ID of the form object.
123
     * @return The associated form object, or null if not found.
124
     */
125
    public PublishForm getForm(String formObjId) {
126
        return formMap.get(formObjId);
×
127
    }
128

129
    /**
130
     * Removes the form object associated with the given ID.
131
     *
132
     * @param formObjId The ID of the form object to remove.
133
     */
134
    public void removeForm(String formObjId) {
135
        formMap.remove(formObjId);
×
136
    }
×
137

138
    /**
139
     * Loads profile information for the user.
140
     * Initializes user-related data such as keys and introductions.
141
     */
142
    public void loadProfileInfo() {
143
        localIntroCount = null;
9✔
144
        localIntro = null;
9✔
145
        NanodashPreferences prefs = NanodashPreferences.get();
6✔
146
        if (prefs.isOrcidLoginMode()) {
9!
147
            File usersDir = new File(System.getProperty("user.home") + "/.nanopub/nanodash-users/");
×
148
            if (!usersDir.exists()) usersDir.mkdir();
×
149
        }
150
        if (userIri == null && !prefs.isReadOnlyMode() && !prefs.isOrcidLoginMode()) {
27!
151
            if (getOrcidFile().exists()) {
12!
152
                try {
153
                    String orcid = FileUtils.readFileToString(getOrcidFile(), StandardCharsets.UTF_8).trim();
×
154
                    //String orcid = Files.readString(orcidFile.toPath(), StandardCharsets.UTF_8).trim();
155
                    if (orcid.matches(ProfilePage.ORCID_PATTERN)) {
×
156
                        userIri = vf.createIRI("https://orcid.org/" + orcid);
×
157
                        if (httpSession != null) httpSession.setMaxInactiveInterval(24 * 60 * 60);  // 24h
×
158
                    }
159
                } catch (IOException ex) {
×
160
                    logger.error("Couldn't read ORCID file", ex);
×
161
                }
×
162
            }
163
        }
164
        if (userIri != null && keyPair == null) {
9!
165
            File keyFile = getKeyFile();
×
166
            if (keyFile.exists()) {
×
167
                try {
168
                    keyPair = SignNanopub.loadKey(keyFile.getPath(), SignatureAlgorithm.RSA);
×
169
                } catch (Exception ex) {
×
170
                    logger.error("Couldn't load key pair", ex);
×
171
                }
×
172
            } else {
173
                // Automatically generate new keys
174
                makeKeys();
×
175
            }
176
        }
177
        if (userIri != null && keyPair != null && introNps == null) {
9!
178
            introNps = new ConcurrentHashMap<>(User.getIntroNanopubs(getPubkeyString()));
×
179
        }
180
//                checkOrcidLink();
181
    }
3✔
182

183
    /**
184
     * Checks if the user's profile is complete.
185
     *
186
     * @return True if the profile is complete, false otherwise.
187
     */
188
    public boolean isProfileComplete() {
189
        return userIri != null && keyPair != null && introNps != null;
×
190
    }
191

192
    /**
193
     * Redirects the user to the login page if their profile is incomplete.
194
     *
195
     * @param path       The path to redirect to after login.
196
     * @param parameters The page parameters for the redirect.
197
     */
198
    public void redirectToLoginIfNeeded(String path, PageParameters parameters) {
199
        String loginUrl = getLoginUrl(path, parameters);
×
200
        if (loginUrl == null) return;
×
201
        throw new RedirectToUrlException(loginUrl);
×
202
    }
203

204
    /**
205
     * Retrieves the login URL for the user.
206
     *
207
     * @param path       The path to redirect to after login.
208
     * @param parameters The page parameters for the redirect.
209
     * @return The login URL, or null if the user is already logged in.
210
     */
211
    public String getLoginUrl(String path, PageParameters parameters) {
212
        if (isProfileComplete()) return null;
×
213
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
214
            return OrcidLoginPage.getOrcidLoginUrl(path, parameters);
×
215
        } else {
216
            return ProfilePage.MOUNT_PATH;
×
217
        }
218
    }
219

220
    /**
221
     * Retrieves the public key as a Base64-encoded string.
222
     *
223
     * @return The public key string, or null if the key pair is not set.
224
     */
225
    public String getPubkeyString() {
226
        if (keyPair == null) return null;
×
227
        return DatatypeConverter.printBase64Binary(keyPair.getPublic().getEncoded()).replaceAll("\\s", "");
×
228
    }
229

230
    /**
231
     * Retrieves the public key hash for the user.
232
     *
233
     * @return The SHA-256 hash of the public key, or null if the public key is not set.
234
     */
235
    public String getPubkeyhash() {
236
        String pubkey = getPubkeyString();
×
237
        if (pubkey == null) return null;
×
238
        return Utils.createSha256HexHash(pubkey);
×
239
    }
240

241
    /**
242
     * Checks if the user's public key is approved.
243
     *
244
     * @return True if the public key is approved, false otherwise.
245
     */
246
    public boolean isPubkeyApproved() {
247
        if (keyPair == null || userIri == null) return false;
×
248
        return User.isApprovedPubkeyhashForUser(getPubkeyhash(), userIri);
×
249
    }
250

251
    /**
252
     * Retrieves the user's key pair.
253
     *
254
     * @return The key pair.
255
     */
256
    public KeyPair getKeyPair() {
257
        return keyPair;
×
258
    }
259

260
    /**
261
     * Generates a new key pair for the user.
262
     */
263
    public void makeKeys() {
264
        try {
265
            MakeKeys.make(getKeyFile().getAbsolutePath().replaceFirst("_rsa$", ""), SignatureAlgorithm.RSA);
×
266
            keyPair = SignNanopub.loadKey(getKeyFile().getPath(), SignatureAlgorithm.RSA);
×
267
        } catch (Exception ex) {
×
268
            logger.error("Couldn't create key pair", ex);
×
269
        }
×
270
    }
×
271

272
    /**
273
     * Retrieves the user's IRI.
274
     *
275
     * @return The user's IRI, or null if not set.
276
     */
277
    public IRI getUserIri() {
278
        return userIri;
×
279
    }
280

281
    /**
282
     * Retrieves the user's introduction nanopublications.
283
     *
284
     * @return A list of user's introduction nanopublications.
285
     */
286
    public List<IntroNanopub> getUserIntroNanopubs() {
287
        return User.getIntroNanopubs(userIri);
×
288
    }
289

290
    /**
291
     * Counts the number of local introduction nanopublications.
292
     *
293
     * @return The count of local introduction nanopublications.
294
     */
295
    public int getLocalIntroCount() {
296
        if (localIntroCount == null) {
×
297
            localIntroCount = 0;
×
298
            for (IntroNanopub inp : getUserIntroNanopubs()) {
×
299
                if (isIntroWithLocalKey(inp)) {
×
300
                    localIntroCount++;
×
301
                    localIntro = inp;
×
302
                }
303
            }
×
304
            if (localIntroCount > 1) localIntro = null;
×
305
        }
306
        return localIntroCount;
×
307
    }
308

309
    /**
310
     * Retrieves the local introduction nanopublication.
311
     *
312
     * @return The local introduction nanopublication, or null if not found.
313
     */
314
    public IntroNanopub getLocalIntro() {
315
        getLocalIntroCount();
×
316
        return localIntro;
×
317
    }
318

319
    /**
320
     * Checks if the given introduction nanopublication is associated with the local key.
321
     *
322
     * @param inp The introduction nanopublication.
323
     * @return True if associated with the local key, false otherwise.
324
     */
325
    public boolean isIntroWithLocalKey(IntroNanopub inp) {
326
        IRI location = Utils.getLocation(inp);
×
327
        NanopubSignatureElement el = Utils.getNanopubSignatureElement(inp);
×
328
        String siteUrl = NanodashPreferences.get().getWebsiteUrl();
×
329
        if (location != null && siteUrl != null) {
×
330
            String l = location.stringValue();
×
331
            // TODO: Solve the name change recognition in a better way:
332
            if (!l.equals(siteUrl) && !l.replace("nanobench", "nanodash").equals(siteUrl)) return false;
×
333
        }
334
        if (!getPubkeyString().equals(el.getPublicKeyString())) return false;
×
335
        for (KeyDeclaration kd : inp.getKeyDeclarations()) {
×
336
            if (getPubkeyString().equals(kd.getPublicKeyString())) return true;
×
337
        }
×
338
        return false;
×
339
    }
340

341
    /**
342
     * Sets the user's ORCID identifier.
343
     *
344
     * @param orcid The ORCID identifier.
345
     */
346
    public void setOrcid(String orcid) {
347
        if (!orcid.matches(ProfilePage.ORCID_PATTERN)) {
×
348
            throw new RuntimeException("Illegal ORCID identifier: " + orcid);
×
349
        }
350
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
351
            userDir = System.getProperty("user.home") + "/.nanopub/nanodash-users/" + orcid + "/";
×
352
            File f = new File(userDir);
×
353
            if (!f.exists()) f.mkdir();
×
354
        } else {
×
355
            try {
356
                FileUtils.writeStringToFile(getOrcidFile(), orcid + "\n", StandardCharsets.UTF_8);
×
357
                //                        Files.writeString(orcidFile.toPath(), orcid + "\n");
358
            } catch (IOException ex) {
×
359
                logger.error("Couldn't write ORCID file", ex);
×
360
            }
×
361
        }
362
        userIri = vf.createIRI("https://orcid.org/" + orcid);
×
363
        loadProfileInfo();
×
364
        if (httpSession != null) httpSession.setMaxInactiveInterval(24 * 60 * 60);  // 24h
×
365
    }
×
366

367
    /**
368
     * Logs out the user and invalidates the session.
369
     */
370
    public void logout() {
371
        userIri = null;
×
372
        invalidateNow();
×
373
    }
×
374

375
    /**
376
     * Retrieves the user's introduction nanopublications as a map.
377
     *
378
     * @return A map of introduction nanopublications.
379
     */
380
    public Map<IRI, IntroNanopub> getIntroNanopubs() {
381
        return introNps;
×
382
    }
383

384
//        public void checkOrcidLink() {
385
//                if (isOrcidLinked == null && userIri != null) {
386
//                        orcidLinkError = "";
387
//                        introExtractor = null;
388
//                        try {
389
//                                introExtractor = IntroNanopub.extract(userIri.stringValue(), null);
390
//                                if (introExtractor.getIntroNanopub() == null) {
391
//                                        orcidLinkError = "ORCID account is not linked.";
392
//                                        isOrcidLinked = false;
393
//                                } else {
394
//                                        IntroNanopub inp = IntroNanopub.get(userIri.stringValue(), introExtractor);
395
//                                        if (introNps != null && introNps.containsKey(inp.getNanopub().getUri())) {
396
//                                                // TODO: also check whether introduction contains local key
397
//                                                isOrcidLinked = true;
398
//                                        } else {
399
//                                                isOrcidLinked = false;
400
//                                                orcidLinkError = "Error: ORCID is linked to another introduction nanopublication.";
401
//                                        }
402
//                                }
403
//                        } catch (Exception ex) {
404
//                                logger.error("ORCID check failed");
405
//                                orcidLinkError = "ORCID check failed.";
406
//                        }
407
//                }
408
//        }
409
//
410
//        public void resetOrcidLinked() {
411
//                isOrcidLinked = null;
412
//        }
413
//
414
//        public boolean isOrcidLinked() {
415
//                checkOrcidLink();
416
//                return isOrcidLinked != null && isOrcidLinked == true;
417
//        }
418
//
419
//        public String getOrcidLinkError() {
420
//                return orcidLinkError;
421
//        }
422
//
423
//        public String getOrcidName() {
424
//                if (introExtractor == null || introExtractor.getName() == null) return null;
425
//                if (introExtractor.getName().trim().isEmpty()) return null;
426
//                return introExtractor.getName();
427
//        }
428

429
    /**
430
     * Retrieves the file for storing the user's ORCID identifier.
431
     *
432
     * @return The ORCID file.
433
     */
434
    private File getOrcidFile() {
435
        return new File(userDir + "orcid");
21✔
436
    }
437

438
    /**
439
     * Retrieves the file for storing the user's private key.
440
     *
441
     * @return The key file.
442
     */
443
    public File getKeyFile() {
444
        return new File(userDir + "id_rsa");
×
445
    }
446

447
    /**
448
     * Sets the time when the introduction was last published.
449
     */
450
    public void setIntroPublishedNow() {
451
        lastTimeIntroPublished = new Date();
×
452
    }
×
453

454
    /**
455
     * Checks if the introduction has been published.
456
     *
457
     * @return True if the introduction has been published, false otherwise.
458
     */
459
    public boolean hasIntroPublished() {
460
        return lastTimeIntroPublished != null;
×
461
    }
462

463
    /**
464
     * Calculates the time since the last introduction was published.
465
     *
466
     * @return The time in milliseconds since the last introduction was published, or Long.MAX_VALUE if it has never been published.
467
     */
468
    public long getTimeSinceLastIntroPublished() {
469
        if (lastTimeIntroPublished == null) return Long.MAX_VALUE;
×
470
        return new Date().getTime() - lastTimeIntroPublished.getTime();
×
471
    }
472

473
    /**
474
     * Sets the view mode for displaying nanopublication results.
475
     *
476
     * @param viewMode The desired view mode (e.g., GRID or LIST).
477
     */
478
    public void setNanopubResultsViewMode(NanopubResults.ViewMode viewMode) {
479
        this.nanopubResultsViewMode = viewMode;
×
480
    }
×
481

482
    /**
483
     * Retrieves the current view mode for displaying nanopublication results.
484
     *
485
     * @return The current view mode.
486
     */
487
    public NanopubResults.ViewMode getNanopubResultsViewMode() {
488
        return this.nanopubResultsViewMode;
×
489
    }
490

491
    // --- Preview nanopub support ---
492

493
    public static class PreviewNanopub implements Serializable {
494
        private final Nanopub nanopub;
495
        private final PageParameters pageParams;
496
        private final Class<? extends WebPage> confirmPageClass;
497
        private final boolean consentChecked;
498

499
        public PreviewNanopub(Nanopub nanopub, PageParameters pageParams, Class<? extends WebPage> confirmPageClass, boolean consentChecked) {
×
500
            this.nanopub = nanopub;
×
501
            this.pageParams = pageParams;
×
502
            this.confirmPageClass = confirmPageClass;
×
503
            this.consentChecked = consentChecked;
×
504
        }
×
505

506
        public Nanopub getNanopub() { return nanopub; }
×
507
        public PageParameters getPageParams() { return pageParams; }
×
508
        public Class<? extends WebPage> getConfirmPageClass() { return confirmPageClass; }
×
509
        public boolean isConsentChecked() { return consentChecked; }
×
510
    }
511

512
    private ConcurrentMap<String, PreviewNanopub> previewMap = new ConcurrentHashMap<>();
15✔
513

514
    public void setPreviewNanopub(String id, PreviewNanopub preview) {
515
        previewMap.put(id, preview);
×
516
    }
×
517

518
    public PreviewNanopub getPreviewNanopub(String id) {
519
        return previewMap.get(id);
×
520
    }
521

522
    public PreviewNanopub removePreviewNanopub(String id) {
523
        return previewMap.remove(id);
×
524
    }
525

526
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc