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

openmrs / openmrs-core / 29867170267

21 Jul 2026 05:53PM UTC coverage: 64.101% (+0.1%) from 63.994%
29867170267

push

github

ibacher
Require Get Patient Programs privilege for by-uuid program lookups (#6318)

* Require Get Patient Programs privilege for by-uuid program lookups

ProgramWorkflowService.getPatientProgramByUuid and getPatientStateByUuid
had no @Authorized annotation. Because AuthorizationAdvice only enforces
when a method declares a privilege, both fell through with no check at
all, letting any authenticated user read program-enrollment PHI by uuid.
Every other read on the service, including getPatientProgram(Integer),
already requires Get Patient Programs; annotate both by-uuid methods to
match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Require Get Patient Programs privilege for patient program attribute lookup

getPatientProgramAttributeByAttributeName returns patient program
attribute values keyed by patient id, yet lacked an @Authorized check.
Its two sibling attribute readers (getPatientProgramAttributeByUuid and
getPatientProgramByAttributeNameAndValue) already guard on Get Patient
Programs, so this was a missed PHI read of the same class addressed by
GHSA-gqf9-38mg-wgqg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpKNfxm3QjwUBDePEtFm2H

* Add authorization tests pinning the by-uuid program privilege checks

The existing tests for getPatientProgramByUuid, getPatientStateByUuid and
getPatientProgramAttributeByAttributeName run as the privileged superuser
and assert only find/null behavior, so they pass even if the
@Authorized(Get Patient Programs) checks added by this branch are removed.
getPatientProgramAttributeByAttributeName had no test at all.

Add a test per method that logs out (dropping Get Patient Programs) and
asserts APIAuthenticationException is thrown for otherwise-valid arguments,
mirroring AuthorizationAdviceTest and AdministrationServiceTest. These fail
if any of the three annotations is removed, pinning the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cl... (continued)

24273 of 37867 relevant lines covered (64.1%)

0.64 hits per line

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

13.82
/web/src/main/java/org/openmrs/web/filter/update/UpdateFilter.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.web.filter.update;
11

12
import liquibase.changelog.ChangeSet;
13
import liquibase.exception.LockException;
14
import org.apache.commons.collections.CollectionUtils;
15
import org.apache.commons.lang3.StringUtils;
16
import org.openmrs.liquibase.ChangeLogDetective;
17
import org.openmrs.util.DatabaseUpdateException;
18
import org.openmrs.util.DatabaseUpdater;
19
import org.openmrs.liquibase.ChangeSetExecutorCallback;
20
import org.openmrs.util.DatabaseUpdaterLiquibaseProvider;
21
import org.openmrs.util.InputRequiredException;
22
import org.openmrs.liquibase.ChangeLogVersionFinder;
23
import org.openmrs.util.OpenmrsThreadPoolHolder;
24
import org.openmrs.util.OpenmrsUtil;
25
import org.openmrs.util.RoleConstants;
26
import org.openmrs.util.Security;
27
import org.openmrs.web.Listener;
28
import org.openmrs.web.WebDaemon;
29
import org.openmrs.web.filter.StartupFilter;
30
import org.openmrs.web.filter.initialization.InitializationFilter;
31
import org.openmrs.web.filter.util.CustomResourceLoader;
32
import org.openmrs.web.filter.util.ErrorMessageConstants;
33
import org.openmrs.web.filter.util.FilterUtil;
34
import org.slf4j.Logger;
35
import org.slf4j.LoggerFactory;
36
import org.springframework.web.context.ContextLoader;
37

38
import javax.servlet.FilterChain;
39
import javax.servlet.FilterConfig;
40
import javax.servlet.ServletContext;
41
import javax.servlet.ServletContextEvent;
42
import javax.servlet.ServletException;
43
import javax.servlet.ServletRequest;
44
import javax.servlet.ServletResponse;
45
import javax.servlet.http.HttpServletRequest;
46
import javax.servlet.http.HttpServletResponse;
47
import java.io.IOException;
48
import java.sql.Connection;
49
import java.sql.PreparedStatement;
50
import java.sql.ResultSet;
51
import java.sql.SQLException;
52
import java.util.ArrayList;
53
import java.util.Arrays;
54
import java.util.HashMap;
55
import java.util.LinkedList;
56
import java.util.List;
57
import java.util.Locale;
58
import java.util.Map;
59
import java.util.concurrent.Future;
60

61
/**
62
 * This is the second filter that is processed. It is only active when OpenMRS has some liquibase
63
 * updates that need to be run. If updates are needed, this filter/wizard asks for a super user to
64
 * authenticate and review the updates before continuing.
65
 */
66
public class UpdateFilter extends StartupFilter {
1✔
67
        
68
        protected final Logger log = LoggerFactory.getLogger(UpdateFilter.class);
1✔
69
        
70
        /**
71
         * The velocity macro page to redirect to if an error occurs or on initial startup
72
         */
73
        private static final String DEFAULT_PAGE = "maintenance.vm";
74
        
75
        /**
76
         * The page that lists off all the currently unexecuted changes
77
         */
78
        private static final String REVIEW_CHANGES = "reviewchanges.vm";
79
        
80
        private static final String PROGRESS_VM_AJAXREQUEST = "updateProgress.vm.ajaxRequest";
81
        
82
        /**
83
         * Session attribute set once a super user authenticates at the first step and checked on every
84
         * later step. Holding it on the session, rather than on the shared filter instance, stops one
85
         * user's successful authentication from being reused by another user's unauthenticated request.
86
         */
87
        private static final String AUTHENTICATED_SUCCESSFULLY = "updateFilter.authenticatedSuccessfully";
88

89
        /**
90
         * The model object behind this set of screens
91
         */
92
        private UpdateFilterModel updateFilterModel = null;
1✔
93
        
94
        /**
95
         * Variable set as soon as the update is done or verified to not be needed so that future calls
96
         * through this filter are a simple boolean check
97
         */
98
        private static boolean updatesRequired = true;
1✔
99

100
        private UpdateFilterCompletion updateJob;
101
        
102
        /**
103
         * Variable set to true as soon as the update begins and set to false when the process ends. This
104
         * thread should only be accesses through the synchronized method.
105
         */
106
        private static boolean isDatabaseUpdateInProgress = false;
1✔
107
        
108
        /**
109
         * Variable set to true when the db lock is released. It's needed to prevent repeatedly releasing
110
         * this lock by other threads. This var should only be accessed through the synchronized method.
111
         */
112
        private static Boolean lockReleased = false;
1✔
113
        
114
        /**
115
         * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on GET requests
116
         *
117
         * @param httpRequest
118
         * @param httpResponse
119
         */
120
        @Override
121
        protected void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
122
                throws IOException, ServletException {
123
                
124
                Map<String, Object> referenceMap = new HashMap<>();
×
125
                checkLocaleAttributesForFirstTime(httpRequest);
×
126
                // we need to save current user language in references map since it will be used when template
127
                // will be rendered
128
                if (httpRequest.getSession().getAttribute(FilterUtil.LOCALE_ATTRIBUTE) != null) {
×
129
                        referenceMap.put(FilterUtil.LOCALE_ATTRIBUTE,
×
130
                            httpRequest.getSession().getAttribute(FilterUtil.LOCALE_ATTRIBUTE));
×
131
                }
132
                // do step one of the wizard
133
                renderTemplate(DEFAULT_PAGE, referenceMap, httpResponse);
×
134
        }
×
135
        
136
        /**
137
         * Called by {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} on POST requests
138
         */
139
        @Override
140
        protected synchronized void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
141
                throws IOException, ServletException {
142
                
143
                final String updJobStatus = "updateJobStarted";
×
144
                String page = httpRequest.getParameter("page");
×
145
                Map<String, Object> referenceMap = new HashMap<>();
×
146
                if (httpRequest.getSession().getAttribute(FilterUtil.LOCALE_ATTRIBUTE) != null) {
×
147
                        referenceMap.put(FilterUtil.LOCALE_ATTRIBUTE,
×
148
                            httpRequest.getSession().getAttribute(FilterUtil.LOCALE_ATTRIBUTE));
×
149
                }
150
                
151
                // step one
152
                if (DEFAULT_PAGE.equals(page)) {
×
153
                        
154
                        String username = httpRequest.getParameter("username");
×
155
                        String password = httpRequest.getParameter("password");
×
156
                        
157
                        log.debug("Attempting to authenticate user: " + username);
×
158
                        if (authenticateAsSuperUser(username, password)) {
×
159
                                log.debug("Authentication successful.  Redirecting to 'reviewupdates' page.");
×
160
                                // mark this session as authenticated so later steps can verify it
161
                                httpRequest.getSession().setAttribute(AUTHENTICATED_SUCCESSFULLY, Boolean.TRUE);
×
162

163

164
                                //Set variable to tell us whether updates are already in progress
165
                                referenceMap.put("isDatabaseUpdateInProgress", isDatabaseUpdateInProgress);
×
166
                                
167
                                // if another super user has already launched database update
168
                                // allow current super user to review update progress
169
                                if (isDatabaseUpdateInProgress) {
×
170
                                        referenceMap.put(updJobStatus, true);
×
171
                                        httpResponse.setContentType("text/html");
×
172
                                        renderTemplate(REVIEW_CHANGES, referenceMap, httpResponse);
×
173
                                        return;
×
174
                                }
175
                                
176
                                // we will only get here if the db update is NOT running. 
177
                                // so if we find a db lock, we should release it because
178
                                // it was leftover from a previous db update crash
179
                                
180
                                if (!isLockReleased() && DatabaseUpdater.isLocked()) {
×
181
                                        // first we trying to release db lock if it exists
182
                                        try {
183
                                                DatabaseUpdater.releaseDatabaseLock();
×
184
                                                setLockReleased(true);
×
185
                                        }
186
                                        catch (LockException e) {
×
187
                                                // do nothing
188
                                        }
×
189
                                        // if lock was released successfully we need to get unrun changes
190
                                        updateFilterModel.updateChanges();
×
191
                                }
192
                                
193
                                // need to configure velocity tool box for using user's preferred locale
194
                                // so we should store it for further using when configuring velocity tool context
195
                                String localeParameter = FilterUtil.restoreLocale(username);
×
196
                                httpRequest.getSession().setAttribute(FilterUtil.LOCALE_ATTRIBUTE, localeParameter);
×
197
                                referenceMap.put(FilterUtil.LOCALE_ATTRIBUTE, localeParameter);
×
198
                                
199
                                renderTemplate(REVIEW_CHANGES, referenceMap, httpResponse);
×
200
                        } else {
×
201
                                // if not authenticated, show main page again
202
                                try {
203
                                        log.debug("Sleeping for 3 seconds because of a bad username/password");
×
204
                                        Thread.sleep(3000);
×
205
                                }
206
                                catch (InterruptedException e) {
×
207
                                        log.error("Unable to sleep", e);
×
208
                                        throw new ServletException("Got interrupted while trying to sleep thread", e);
×
209
                                }
×
210
                                errors.put(ErrorMessageConstants.UPDATE_ERROR_UNABLE_AUTHENTICATE, null);
×
211
                                renderTemplate(DEFAULT_PAGE, referenceMap, httpResponse);
×
212
                        }
213
                }
×
214
                // step two of wizard in case if there were some warnings
215
                else if (REVIEW_CHANGES.equals(page)) {
×
216

217
                        if (!Boolean.TRUE.equals(httpRequest.getSession().getAttribute(AUTHENTICATED_SUCCESSFULLY))) {
×
218
                                // throw the user back to the main page because they are cheating
219
                                renderTemplate(DEFAULT_PAGE, referenceMap, httpResponse);
×
220
                                return;
×
221
                        }
222
                        
223
                        //if no one has run any required updates
224
                        if (!isDatabaseUpdateInProgress) {
×
225
                                isDatabaseUpdateInProgress = true;
×
226
                                updateJob = new UpdateFilterCompletion();
×
227
                                updateJob.start();
×
228
                                
229
                                // allows current user see progress of running update
230
                                // and also will hide the "Run Updates" button
231
                                
232
                                referenceMap.put(updJobStatus, true);
×
233
                        } else {
234
                                referenceMap.put("isDatabaseUpdateInProgress", true);
×
235
                                // as well we need to allow current user to
236
                                // see progress of already started updates
237
                                // and also will hide the "Run Updates" button
238
                                referenceMap.put(updJobStatus, true);
×
239
                        }
240
                        
241
                        renderTemplate(REVIEW_CHANGES, referenceMap, httpResponse);
×
242
                        
243
                } else if (PROGRESS_VM_AJAXREQUEST.equals(page)) {
×
244
                        
245
                        httpResponse.setContentType("text/json");
×
246
                        httpResponse.setHeader("Cache-Control", "no-cache");
×
247
                        Map<String, Object> result = new HashMap<>();
×
248
                        if (updateJob != null) {
×
249
                                result.put("hasErrors", updateJob.hasErrors());
×
250
                                if (updateJob.hasErrors()) {
×
251
                                        errors.putAll(updateJob.getErrors());
×
252
                                }
253
                                
254
                                if (updateJob.hasWarnings() && updateJob.getExecutingChangesetId() == null) {
×
255
                                        result.put("hasWarnings", updateJob.hasWarnings());
×
256
                                        StringBuilder sb = new StringBuilder("<ul>");
×
257
                                        
258
                                        for (String warning : updateJob.getUpdateWarnings()) {
×
259
                                                sb.append("<li>").append(warning).append("</li>");
×
260
                                        }
×
261
                                        
262
                                        sb.append("</ul>");
×
263
                                        result.put("updateWarnings", sb.toString());
×
264
                                        result.put("updateLogFile",
×
265
                                            StringUtils.replace(
×
266
                                                OpenmrsUtil.getApplicationDataDirectory() + DatabaseUpdater.DATABASE_UPDATES_LOG_FILE, "\\",
×
267
                                                "\\\\"));
268
                                        updateJob.hasUpdateWarnings = false;
×
269
                                        updateJob.getUpdateWarnings().clear();
×
270
                                }
271
                                
272
                                result.put("updatesRequired", updatesRequired());
×
273
                                result.put("message", updateJob.getMessage());
×
274
                                result.put("changesetIds", updateJob.getChangesetIds());
×
275
                                result.put("executingChangesetId", updateJob.getExecutingChangesetId());
×
276
                                
277
                                addLogLinesToResponse(result);
×
278
                        }
279
                        
280
                        String jsonText = toJSONString(result);
×
281
                        httpResponse.getWriter().write(jsonText);
×
282
                }
283
        }
×
284
        
285
        /**
286
         * It sets locale attribute for current session when user is making first GET http request to
287
         * application. It retrieves user locale from request object and checks if this locale is supported
288
         * by application. If not, it tries to load system default locale. If it's not specified it uses
289
         * {@link Locale#ENGLISH} by default
290
         *
291
         * @param httpRequest the http request object
292
         */
293
        public void checkLocaleAttributesForFirstTime(HttpServletRequest httpRequest) {
294
                Locale locale = httpRequest.getLocale();
×
295
                String systemDefaultLocale = FilterUtil.readSystemDefaultLocale(null);
×
296
                if (CustomResourceLoader.getInstance(httpRequest).getAvailablelocales().contains(locale)) {
×
297
                        httpRequest.getSession().setAttribute(FilterUtil.LOCALE_ATTRIBUTE, locale.toString());
×
298
                        log.info("Used client's locale " + locale.toString());
×
299
                } else if (StringUtils.isNotBlank(systemDefaultLocale)) {
×
300
                        httpRequest.getSession().setAttribute(FilterUtil.LOCALE_ATTRIBUTE, systemDefaultLocale);
×
301
                        log.info("Used system default locale " + systemDefaultLocale);
×
302
                } else {
303
                        httpRequest.getSession().setAttribute(FilterUtil.LOCALE_ATTRIBUTE, Locale.ENGLISH.toString());
×
304
                        log.info("Used default locale " + Locale.ENGLISH.toString());
×
305
                }
306
        }
×
307
        
308
        /**
309
         * Look in the users table for a user with this username and password and see if they have a role of
310
         * {@link RoleConstants#SUPERUSER}.
311
         *
312
         * @param usernameOrSystemId user entered username
313
         * @param password user entered password
314
         * @return true if this user has the super user role
315
         * @see #isSuperUser(Connection, Integer) <strong>Should</strong> return false if given invalid
316
         *      credentials <strong>Should</strong> return false if given user is not superuser
317
         *      <strong>Should</strong> return true if given user is superuser <strong>Should</strong> not
318
         *      authorize retired superusers <strong>Should</strong> authenticate with systemId
319
         */
320
        protected boolean authenticateAsSuperUser(String usernameOrSystemId, String password) throws ServletException {
321
                Connection connection = null;
1✔
322
                try {
323
                        connection = DatabaseUpdater.getConnection();
1✔
324
                        
325
                        String select = "select user_id, password, salt from users where (username = ? or system_id = ?) and retired = '0'";
1✔
326
                        PreparedStatement statement = null;
1✔
327
                        try {
328
                                statement = connection.prepareStatement(select);
1✔
329
                                statement.setString(1, usernameOrSystemId);
1✔
330
                                statement.setString(2, usernameOrSystemId);
1✔
331
                                
332
                                if (statement.execute()) {
1✔
333
                                        ResultSet results = null;
1✔
334
                                        try {
335
                                                results = statement.getResultSet();
1✔
336
                                                if (results.next()) {
1✔
337
                                                        Integer userId = results.getInt(1);
1✔
338
                                                        DatabaseUpdater.setAuthenticatedUserId(userId);
1✔
339
                                                        String storedPassword = results.getString(2);
1✔
340
                                                        String salt = results.getString(3);
1✔
341
                                                        String passwordToHash = password + salt;
1✔
342
                                                        return Security.hashMatches(storedPassword, passwordToHash) && isSuperUser(connection, userId);
1✔
343
                                                }
344
                                        }
345
                                        finally {
346
                                                if (results != null) {
1✔
347
                                                        try {
348
                                                                results.close();
1✔
349
                                                        }
350
                                                        catch (Exception resultsCloseEx) {
×
351
                                                                log.error("Failed to quietly close ResultSet", resultsCloseEx);
×
352
                                                        }
1✔
353
                                                }
354
                                        }
355
                                }
356
                        }
357
                        finally {
358
                                if (statement != null) {
1✔
359
                                        try {
360
                                                statement.close();
1✔
361
                                        }
362
                                        catch (Exception statementCloseEx) {
×
363
                                                log.error("Failed to quietly close Statement", statementCloseEx);
×
364
                                        }
1✔
365
                                }
366
                        }
367
                }
368
                catch (Exception connectionEx) {
×
369
                        log.error(
×
370
                            "Error while trying to authenticate as super user. Ignore this if you are upgrading from OpenMRS 1.5 to 1.6",
371
                            connectionEx);
372
                        
373
                        // we may not have upgraded User to have retired instead of voided yet, so if the query above fails, we try
374
                        // again the old way
375
                        if (connection != null) {
×
376
                                String select = "select user_id, password, salt from users where (username = ? or system_id = ?) and voided = '0'";
×
377
                                PreparedStatement statement = null;
×
378
                                try {
379
                                        statement = connection.prepareStatement(select);
×
380
                                        statement.setString(1, usernameOrSystemId);
×
381
                                        statement.setString(2, usernameOrSystemId);
×
382
                                        if (statement.execute()) {
×
383
                                                ResultSet results = null;
×
384
                                                try {
385
                                                        results = statement.getResultSet();
×
386
                                                        if (results.next()) {
×
387
                                                                Integer userId = results.getInt(1);
×
388
                                                                DatabaseUpdater.setAuthenticatedUserId(userId);
×
389
                                                                String storedPassword = results.getString(2);
×
390
                                                                String salt = results.getString(3);
×
391
                                                                String passwordToHash = password + salt;
×
392
                                                                return Security.hashMatches(storedPassword, passwordToHash)
×
393
                                                                        && isSuperUser(connection, userId);
×
394
                                                        }
395
                                                }
396
                                                finally {
397
                                                        if (results != null) {
×
398
                                                                try {
399
                                                                        results.close();
×
400
                                                                }
401
                                                                catch (Exception resultsCloseEx) {
×
402
                                                                        log.error("Failed to quietly close ResultSet", resultsCloseEx);
×
403
                                                                }
×
404
                                                        }
405
                                                }
406
                                        }
407
                                }
408
                                catch (Exception unhandeledEx) {
×
409
                                        log.error("Error while trying to authenticate as super user (voided version)", unhandeledEx);
×
410
                                }
411
                                finally {
412
                                        if (statement != null) {
×
413
                                                try {
414
                                                        statement.close();
×
415
                                                }
416
                                                catch (Exception statementCloseEx) {
×
417
                                                        log.error("Failed to quietly close Statement", statementCloseEx);
×
418
                                                }
×
419
                                        }
420
                                }
421
                        }
422
                }
423
                finally {
424
                        if (connection != null) {
1✔
425
                                try {
426
                                        connection.close();
1✔
427
                                }
428
                                catch (SQLException e) {
×
429
                                        log.debug("Error while closing the database", e);
×
430
                                }
1✔
431
                        }
432
                }
433
                
434
                return false;
1✔
435
        }
436
        
437
        /**
438
         * Checks the given user to see if they have been given the {@link RoleConstants#SUPERUSER}
439
         * role. This method does not look at child roles.
440
         *
441
         * @param connection the java sql connection to use
442
         * @param userId the user id to look at
443
         * @return true if the given user is a super user
444
         * @throws SQLException <strong>Should</strong> return true if given user has superuser role
445
         *             <strong>Should</strong> return false if given user does not have the super user role
446
         */
447
        protected boolean isSuperUser(Connection connection, Integer userId) throws SQLException {
448
                // the 'Administrator' part of this string is necessary because if the database was upgraded
449
                // by OpenMRS 1.6 alpha then System Developer was renamed to that. This has to be here so we
450
                // can roll back that change in 1.6 beta+
451
                String select = "select 1 from user_role where user_id = ? and (role = ? or role = 'Administrator')";
1✔
452
                PreparedStatement statement = connection.prepareStatement(select);
1✔
453
                statement.setInt(1, userId);
1✔
454
                statement.setString(2, RoleConstants.SUPERUSER);
1✔
455
                if (statement.execute()) {
1✔
456
                        ResultSet results = statement.getResultSet();
1✔
457
                        if (results.next()) {
1✔
458
                                return results.getInt(1) == 1;
1✔
459
                        }
460
                }
461
                
462
                return false;
1✔
463
        }
464
        
465
        /**
466
         * `` Do everything to get openmrs going.
467
         *
468
         * @param servletContext the servletContext from the filterconfig
469
         * @see Listener#startOpenmrs(ServletContext)
470
         */
471
        private void startOpenmrs(ServletContext servletContext) throws Exception {
472
                // start spring
473
                // after this point, all errors need to also call: contextLoader.closeWebApplicationContext(event.getServletContext())
474
                // logic copied from org.springframework.web.context.ContextLoaderListener
475
                ContextLoader contextLoader = new ContextLoader();
×
476
                contextLoader.initWebApplicationContext(servletContext);
×
477
                
478
                try {
479
                        WebDaemon.startOpenmrs(servletContext);
×
480
                }
481
                catch (Exception exception) {
×
482
                        contextLoader.closeWebApplicationContext(servletContext);
×
483
                        throw exception;
×
484
                }
×
485
        }
×
486
        
487
        /**
488
         * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
489
         */
490
        @Override
491
        public void init(FilterConfig filterConfig) throws ServletException {
492
                super.init(filterConfig);
×
493
                
494
                log.debug("Initializing the UpdateFilter");
×
495
                
496
                if (!InitializationFilter.initializationRequired()
×
497
                        || (Listener.isSetupNeeded() && Listener.runtimePropertiesFound())) {
×
498
                        updateFilterModel = new UpdateFilterModel();
×
499
                        /*
500
                         * In this case, Listener#runtimePropertiesFound == true and InitializationFilter Wizard is skipped,
501
                         * so no need to reset Context's RuntimeProperties again, because of Listener.contextInitialized has set it.
502
                         */
503
                        try {
504
                                // this pings the DatabaseUpdater.updatesRequired which also
505
                                // considers a db lock to be a 'required update'
506
                                if (updateFilterModel.updateRequired) {
×
507
                                        setUpdatesRequired(true);
×
508
                                } else if (updateFilterModel.changes == null) {
×
509
                                        setUpdatesRequired(false);
×
510
                                } else {
511
                                        log.debug("Setting updates required to {} because of the size of unrun changes", (!updateFilterModel.changes.isEmpty()));
×
512
                                        setUpdatesRequired(!updateFilterModel.changes.isEmpty());
×
513
                                }
514
                        }
515
                        catch (Exception e) {
×
516
                                throw new ServletException("Unable to determine if updates are required", e);
×
517
                        }
×
518
                } else {
519
                        /*
520
                         * The initialization wizard will update the database to the latest version, so the user will not need any updates here.
521
                         * See end of InitializationFilter#InitializationCompletion
522
                         */
523
                        log.debug(
×
524
                            "Setting updates required to false because the user doesn't have any runtime properties yet or database is empty");
525
                        setUpdatesRequired(false);
×
526
                }
527
        }
×
528
        
529
        /**
530
         * @see org.openmrs.web.filter.StartupFilter#getUpdateFilterModel()
531
         */
532
        @Override
533
        protected Object getUpdateFilterModel() {
534
                // this object was initialized in the #init(FilterConfig) method
535
                return updateFilterModel;
×
536
        }
537
        
538
        /**
539
         * @see org.openmrs.web.filter.StartupFilter#skipFilter(HttpServletRequest)
540
         */
541
        @Override
542
        public boolean skipFilter(HttpServletRequest httpRequest) {
543
                return !PROGRESS_VM_AJAXREQUEST.equals(httpRequest.getParameter("page")) && !updatesRequired();
×
544
        }
545
        
546
        /**
547
         * Used by the Listener to know if this filter wants to do its magic
548
         *
549
         * @return true if updates have been determined to be required
550
         * @see #init(FilterConfig)
551
         * @see Listener#isSetupNeeded()
552
         * @see Listener#contextInitialized(ServletContextEvent)
553
         */
554
        public static synchronized boolean updatesRequired() {
555
                return updatesRequired;
×
556
        }
557
        
558
        /**
559
         * @param updatesRequired the updatesRequired to set
560
         */
561
        public static synchronized void setUpdatesRequired(boolean updatesRequired) {
562
                UpdateFilter.updatesRequired = updatesRequired;
×
563
        }
×
564
        
565
        /**
566
         * Indicates if database lock was released. It will also used to prevent releasing existing lock of
567
         * liquibasechangeloglock table by another user, when he also tries to run database update when
568
         * another user is currently running it
569
         */
570
        public static Boolean isLockReleased() {
571
                return lockReleased;
×
572
        }
573
        
574
        public static synchronized void setLockReleased(Boolean lockReleased) {
575
                UpdateFilter.lockReleased = lockReleased;
×
576
        }
×
577
        
578
        /**
579
         * @see org.openmrs.web.filter.StartupFilter#getTemplatePrefix()
580
         */
581
        @Override
582
        protected String getTemplatePrefix() {
583
                return "org/openmrs/web/filter/update/";
×
584
        }
585
        
586
        /**
587
         * This class controls the final steps and is used by the ajax calls to know what updates have been
588
         * executed. TODO: Break this out into a separate (non-inner) class
589
         */
590
        private class UpdateFilterCompletion {
591

592
                private Runnable r;
593

594
                private String executingChangesetId = null;
×
595
                
596
                private List<String> changesetIds = new ArrayList<>();
×
597
                
598
                private Map<String, Object[]> errors = new HashMap<>();
×
599
                
600
                private String message = null;
×
601
                
602
                private boolean erroneous = false;
×
603
                
604
                private boolean hasUpdateWarnings = false;
×
605
                
606
                private List<String> updateWarnings = new LinkedList<>();
×
607
                
608
                public synchronized void reportError(String error, Object... params) {
609
                        Map<String, Object[]> reportedErrors = new HashMap<>();
×
610
                        reportedErrors.put(error, params);
×
611
                        reportErrors(reportedErrors);
×
612
                }
×
613
                
614
                public synchronized void reportErrors(Map<String, Object[]> errs) {
615
                        errors.putAll(errs);
×
616
                        erroneous = true;
×
617
                }
×
618
                
619
                public synchronized boolean hasErrors() {
620
                        return erroneous;
×
621
                }
622
                
623
                public synchronized Map<String, Object[]> getErrors() {
624
                        return errors;
×
625
                }
626
                
627
                /**
628
                 * Start the completion stage. This fires up the thread to do all the work.
629
                 */
630
                public void start() {
631
                        setUpdatesRequired(true);
×
632
                        OpenmrsThreadPoolHolder.threadExecutor.submit(r);
×
633
                }
×
634
                
635
                public synchronized void setMessage(String message) {
636
                        this.message = message;
×
637
                }
×
638
                
639
                public synchronized String getMessage() {
640
                        return message;
×
641
                }
642
                
643
                public synchronized void addChangesetId(String changesetid) {
644
                        this.changesetIds.add(changesetid);
×
645
                        this.executingChangesetId = changesetid;
×
646
                }
×
647
                
648
                public synchronized List<String> getChangesetIds() {
649
                        return changesetIds;
×
650
                }
651
                
652
                public synchronized String getExecutingChangesetId() {
653
                        return executingChangesetId;
×
654
                }
655
                
656
                /**
657
                 * @return the database updater Warnings
658
                 */
659
                public synchronized List<String> getUpdateWarnings() {
660
                        return updateWarnings;
×
661
                }
662
                
663
                public synchronized boolean hasWarnings() {
664
                        return hasUpdateWarnings;
×
665
                }
666
                
667
                public synchronized void reportWarnings(List<String> warnings) {
668
                        updateWarnings.addAll(warnings);
×
669
                        hasUpdateWarnings = true;
×
670
                }
×
671
                
672
                /**
673
                 * This class does all the work of creating the desired database, user, updates, etc
674
                 */
675
                public UpdateFilterCompletion() {
×
676
                         r = new Runnable() {
×
677
                                
678
                                /**
679
                                 * TODO split this up into multiple testable methods
680
                                 *
681
                                 * @see java.lang.Runnable#run()
682
                                 */
683
                                @Override
684
                                public void run() {
685
                                        try {
686
                                                /**
687
                                                 * A callback class that prints out info about liquibase changesets
688
                                                 */
689
                                                class PrintingChangeSetExecutorCallback implements ChangeSetExecutorCallback {
690
                                                        
691
                                                        private String message;
692
                                                        
693
                                                        public PrintingChangeSetExecutorCallback(String message) {
×
694
                                                                this.message = message;
×
695
                                                        }
×
696
                                                        
697
                                                        /**
698
                                                         * @see ChangeSetExecutorCallback#executing(liquibase.changelog.ChangeSet, int)
699
                                                         */
700
                                                        @Override
701
                                                        public void executing(ChangeSet changeSet, int numChangeSetsToRun) {
702
                                                                addChangesetId(changeSet.getId());
×
703
                                                                setMessage(message);
×
704
                                                        }
×
705
                                                        
706
                                                }
707

708
                                                
709
                                                try {
710
                                                        if (DatabaseUpdater.updatesRequired()) {
×
711
                                                                setMessage("Updating the database to the latest version");
×
712

713
                                                                ChangeLogDetective changeLogDetective = ChangeLogDetective.getInstance();
×
714
                                                                ChangeLogVersionFinder changeLogVersionFinder = new ChangeLogVersionFinder();
×
715

716
                                                                List<String> changelogs = new ArrayList<>();
×
717
                                                                List<String> warnings = new ArrayList<>();
×
718

719
                                                                String version = changeLogDetective.getInitialLiquibaseSnapshotVersion(DatabaseUpdater.CONTEXT,
×
720
                                                                        new DatabaseUpdaterLiquibaseProvider());
721

722
                                                                log.debug(
×
723
                                                                        "updating the database with versions of liquibase-update-to-latest files greater than '{}'",
724
                                                                        version);
725

726
                                                                changelogs.addAll(changeLogVersionFinder
×
727
                                                                        .getUpdateFileNames(changeLogVersionFinder.getUpdateVersionsGreaterThan(version)));
×
728

729
                                                                log.debug("found applicable Liquibase update change logs: {}", changelogs);
×
730

731
                                                                for (String changelog : changelogs) {
×
732
                                                                        log.debug("applying Liquibase changelog '{}'", changelog);
×
733

734
                                                                        List<String> currentWarnings = DatabaseUpdater.executeChangelog(changelog,
×
735
                                                                                new PrintingChangeSetExecutorCallback("executing Liquibase changelog :" + changelog));
736

737
                                                                        if (currentWarnings != null) {
×
738
                                                                                warnings.addAll(currentWarnings);
×
739
                                                                        }
740
                                                                }
×
741
                                                                executingChangesetId = null; // clear out the last changeset
×
742

743
                                                                if (CollectionUtils.isNotEmpty(warnings)) {
×
744
                                                                        reportWarnings(warnings);
×
745
                                                                }
746
                                                        }
747
                                                }
748
                                                catch (InputRequiredException inputRequired) {
×
749
                                                        // the user would be stepped through the questions returned here.
750
                                                        log.error("Not implemented", inputRequired);
×
751
                                                        updateFilterModel.updateChanges();
×
752
                                                        reportError(ErrorMessageConstants.UPDATE_ERROR_INPUT_NOT_IMPLEMENTED,
×
753
                                                            inputRequired.getMessage());
×
754
                                                        return;
×
755
                                                }
756
                                                catch (DatabaseUpdateException e) {
×
757
                                                        log.error("Unable to update the database", e);
×
758
                                                        Map<String, Object[]> databaseUpdateErrors = new HashMap<>();
×
759
                                                        databaseUpdateErrors.put(ErrorMessageConstants.UPDATE_ERROR_UNABLE, null);
×
760
                                                        for (String errorMessage : Arrays.asList(e.getMessage().split("\n"))) {
×
761
                                                                databaseUpdateErrors.put(errorMessage, null);
×
762
                                                        }
×
763
                                                        updateFilterModel.updateChanges();
×
764
                                                        reportErrors(databaseUpdateErrors);
×
765
                                                        return;
×
766
                                                }
767
                                                catch (Exception e) {
×
768
                                                        log.error("Unable to update the database", e);
×
769
                                                        return;
×
770
                                                }
×
771
                                                
772
                                                setMessage("Starting OpenMRS");
×
773
                                                try {
774
                                                        startOpenmrs(filterConfig.getServletContext());
×
775
                                                }
776
                                                catch (Exception e) {
×
777
                                                        log.error("Unable to complete the startup.", e);
×
778
                                                        reportError(ErrorMessageConstants.UPDATE_ERROR_COMPLETE_STARTUP, e.getMessage());
×
779
                                                        return;
×
780
                                                }
×
781
                                                
782
                                                // set this so that the wizard isn't run again on next page load
783
                                                setUpdatesRequired(false);
×
784
                                        }
785
                                        finally {
786
                                                if (!hasErrors()) {
×
787
                                                        setUpdatesRequired(false);
×
788
                                                }
789
                                                //reset to let other user's make requests after updates are run
790
                                                isDatabaseUpdateInProgress = false;
×
791
                                        }
792
                                }
×
793
                        };
794
                }
×
795
        }
796
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc