/* * Copyright 2000-2014 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.ui; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.logging.Logger; import org.jsoup.nodes.Element; import com.vaadin.data.Property; import com.vaadin.data.Validator; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.validator.DateRangeValidator; import com.vaadin.event.FieldEvents; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; import com.vaadin.event.FieldEvents.FocusEvent; import com.vaadin.event.FieldEvents.FocusListener; import com.vaadin.server.PaintException; import com.vaadin.server.PaintTarget; import com.vaadin.shared.ui.datefield.DateFieldConstants; import com.vaadin.shared.ui.datefield.Resolution; import com.vaadin.shared.ui.datefield.TextualDateFieldState; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; /** *

* A date editor component that can be bound to any {@link Property} that is * compatible with java.util.Date. *

*

* Since DateField extends AbstractField it implements * the {@link com.vaadin.data.Buffered}interface. *

*

* A DateField is in write-through mode by default, so * {@link com.vaadin.ui.AbstractField#setWriteThrough(boolean)}must be called to * enable buffering. *

* * @author Vaadin Ltd. * @since 3.0 */ @SuppressWarnings("serial") public class DateField extends AbstractField implements FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, LegacyComponent { /** * Resolution identifier: seconds. * * @deprecated As of 7.0, use {@link Resolution#SECOND} */ @Deprecated public static final Resolution RESOLUTION_SEC = Resolution.SECOND; /** * Resolution identifier: minutes. * * @deprecated As of 7.0, use {@link Resolution#MINUTE} */ @Deprecated public static final Resolution RESOLUTION_MIN = Resolution.MINUTE; /** * Resolution identifier: hours. * * @deprecated As of 7.0, use {@link Resolution#HOUR} */ @Deprecated public static final Resolution RESOLUTION_HOUR = Resolution.HOUR; /** * Resolution identifier: days. * * @deprecated As of 7.0, use {@link Resolution#DAY} */ @Deprecated public static final Resolution RESOLUTION_DAY = Resolution.DAY; /** * Resolution identifier: months. * * @deprecated As of 7.0, use {@link Resolution#MONTH} */ @Deprecated public static final Resolution RESOLUTION_MONTH = Resolution.MONTH; /** * Resolution identifier: years. * * @deprecated As of 7.0, use {@link Resolution#YEAR} */ @Deprecated public static final Resolution RESOLUTION_YEAR = Resolution.YEAR; /** * Specified smallest modifiable unit for the date field. */ private Resolution resolution = Resolution.DAY; /** * The internal calendar to be used in java.utl.Date conversions. */ private transient Calendar calendar; /** * Overridden format string */ private String dateFormat; private boolean lenient = false; private String dateString = null; /** * Was the last entered string parsable? If this flag is false, datefields * internal validator does not pass. */ private boolean uiHasValidDateString = true; /** * Determines if week numbers are shown in the date selector. */ private boolean showISOWeekNumbers = false; private String currentParseErrorMessage; private String defaultParseErrorMessage = "Date format not recognized"; private TimeZone timeZone = null; private static Map variableNameForResolution = new HashMap(); private String dateOutOfRangeMessage = "Date is out of allowed range"; private DateRangeValidator currentRangeValidator; /** * Determines whether the ValueChangeEvent should be fired. Used to prevent * firing the event when UI has invalid string until uiHasValidDateString * flag is set */ private boolean preventValueChangeEvent = false; static { variableNameForResolution.put(Resolution.SECOND, "sec"); variableNameForResolution.put(Resolution.MINUTE, "min"); variableNameForResolution.put(Resolution.HOUR, "hour"); variableNameForResolution.put(Resolution.DAY, "day"); variableNameForResolution.put(Resolution.MONTH, "month"); variableNameForResolution.put(Resolution.YEAR, "year"); } /* Constructors */ /** * Constructs an empty DateField with no caption. */ public DateField() { } /** * Constructs an empty DateField with caption. * * @param caption * the caption of the datefield. */ public DateField(String caption) { setCaption(caption); } /** * Constructs a new DateField that's bound to the specified * Property and has the given caption String. * * @param caption * the caption String for the editor. * @param dataSource * the Property to be edited with this editor. */ public DateField(String caption, Property dataSource) { this(dataSource); setCaption(caption); } /** * Constructs a new DateField that's bound to the specified * Property and has no caption. * * @param dataSource * the Property to be edited with this editor. */ public DateField(Property dataSource) throws IllegalArgumentException { if (!Date.class.isAssignableFrom(dataSource.getType())) { throw new IllegalArgumentException("Can't use " + dataSource.getType().getName() + " typed property as datasource"); } setPropertyDataSource(dataSource); } /** * Constructs a new DateField with the given caption and * initial text contents. The editor constructed this way will not be bound * to a Property unless * {@link com.vaadin.data.Property.Viewer#setPropertyDataSource(Property)} * is called to bind it. * * @param caption * the caption String for the editor. * @param value * the Date value. */ public DateField(String caption, Date value) { setValue(value); setCaption(caption); } /* Component basic features */ /* * Paints this component. Don't add a JavaDoc comment here, we use the * default documentation from implemented interface. */ @Override public void paintContent(PaintTarget target) throws PaintException { // Adds the locale as attribute final Locale l = getLocale(); if (l != null) { target.addAttribute("locale", l.toString()); } if (getDateFormat() != null) { target.addAttribute("format", dateFormat); } if (!isLenient()) { target.addAttribute("strict", true); } target.addAttribute(DateFieldConstants.ATTR_WEEK_NUMBERS, isShowISOWeekNumbers()); target.addAttribute("parsable", uiHasValidDateString); /* * TODO communicate back the invalid date string? E.g. returning back to * app or refresh. */ // Gets the calendar final Calendar calendar = getCalendar(); final Date currentDate = getValue(); // Only paint variables for the resolution and up, e.g. Resolution DAY // paints DAY,MONTH,YEAR for (Resolution res : Resolution .getResolutionsHigherOrEqualTo(resolution)) { int value = -1; if (currentDate != null) { value = calendar.get(res.getCalendarField()); if (res == Resolution.MONTH) { // Calendar month is zero based value++; } } target.addVariable(this, variableNameForResolution.get(res), value); } } @Override protected boolean shouldHideErrors() { return super.shouldHideErrors() && uiHasValidDateString; } @Override protected TextualDateFieldState getState() { return (TextualDateFieldState) super.getState(); } @Override protected TextualDateFieldState getState(boolean markAsDirty) { return (TextualDateFieldState) super.getState(markAsDirty); } /** * Sets the start range for this component. If the value is set before this * date (taking the resolution into account), the component will not * validate. If startDate is set to null, any * value before endDate will be accepted by the range * * @param startDate * - the allowed range's start date */ public void setRangeStart(Date startDate) { if (startDate != null && getState().rangeEnd != null && startDate.after(getState().rangeEnd)) { throw new IllegalStateException( "startDate cannot be later than endDate"); } // Create a defensive copy against issues when using java.sql.Date (and // also against mutable Date). getState().rangeStart = startDate != null ? new Date( startDate.getTime()) : null; updateRangeValidator(); } /** * Sets the current error message if the range validation fails. * * @param dateOutOfRangeMessage * - Localizable message which is shown when value (the date) is * set outside allowed range */ public void setDateOutOfRangeMessage(String dateOutOfRangeMessage) { this.dateOutOfRangeMessage = dateOutOfRangeMessage; updateRangeValidator(); } /** * Gets the end range for a certain resolution. The range is inclusive, so * if rangeEnd is set to zero milliseconds past year n and resolution is set * to YEAR, any date in year n will be accepted. Resolutions lower than DAY * will be interpreted on a DAY level. That is, everything below DATE is * cleared * * @param forResolution * - the range conforms to the resolution * @return */ private Date getRangeEnd(Resolution forResolution) { // We need to set the correct resolution for the dates, // otherwise the range validator will complain Date rangeEnd = getState(false).rangeEnd; if (rangeEnd == null) { return null; } Calendar endCal = Calendar.getInstance(); endCal.setTime(rangeEnd); if (forResolution == Resolution.YEAR) { // Adding one year (minresolution) and clearing the rest. endCal.set(endCal.get(Calendar.YEAR) + 1, 0, 1, 0, 0, 0); } else if (forResolution == Resolution.MONTH) { // Adding one month (minresolution) and clearing the rest. endCal.set(endCal.get(Calendar.YEAR), endCal.get(Calendar.MONTH) + 1, 1, 0, 0, 0); } else { endCal.set(endCal.get(Calendar.YEAR), endCal.get(Calendar.MONTH), endCal.get(Calendar.DATE) + 1, 0, 0, 0); } // removing one millisecond will now get the endDate to return to // current resolution's set time span (year or month) endCal.set(Calendar.MILLISECOND, -1); return endCal.getTime(); } /** * Gets the start range for a certain resolution. The range is inclusive, so * if rangeStart is set to one millisecond before year n and * resolution is set to YEAR, any date in year n - 1 will be accepted. * Lowest supported resolution is DAY. * * @param forResolution * - the range conforms to the resolution * @return */ private Date getRangeStart(Resolution forResolution) { if (getState(false).rangeStart == null) { return null; } Calendar startCal = Calendar.getInstance(); startCal.setTime(getState(false).rangeStart); if (forResolution == Resolution.YEAR) { startCal.set(startCal.get(Calendar.YEAR), 0, 1, 0, 0, 0); } else if (forResolution == Resolution.MONTH) { startCal.set(startCal.get(Calendar.YEAR), startCal.get(Calendar.MONTH), 1, 0, 0, 0); } else { startCal.set(startCal.get(Calendar.YEAR), startCal.get(Calendar.MONTH), startCal.get(Calendar.DATE), 0, 0, 0); } startCal.set(Calendar.MILLISECOND, 0); return startCal.getTime(); } private void updateRangeValidator() { if (currentRangeValidator != null) { removeValidator(currentRangeValidator); currentRangeValidator = null; } if (getRangeStart() != null || getRangeEnd() != null) { currentRangeValidator = new DateRangeValidator( dateOutOfRangeMessage, getRangeStart(resolution), getRangeEnd(resolution), null); addValidator(currentRangeValidator); } } /** * Sets the end range for this component. If the value is set after this * date (taking the resolution into account), the component will not * validate. If endDate is set to null, any value * after startDate will be accepted by the range. * * @param endDate * - the allowed range's end date (inclusive, based on the * current resolution) */ public void setRangeEnd(Date endDate) { if (endDate != null && getState().rangeStart != null && getState().rangeStart.after(endDate)) { throw new IllegalStateException( "endDate cannot be earlier than startDate"); } // Create a defensive copy against issues when using java.sql.Date (and // also against mutable Date). getState().rangeEnd = endDate != null ? new Date(endDate.getTime()) : null; updateRangeValidator(); } /** * Returns the precise rangeStart used. * * @param startDate * */ public Date getRangeStart() { return getState(false).rangeStart; } /** * Returns the precise rangeEnd used. * * @param startDate */ public Date getRangeEnd() { return getState(false).rangeEnd; } /* * Invoked when a variable of the component changes. Don't add a JavaDoc * comment here, we use the default documentation from implemented * interface. */ @Override public void changeVariables(Object source, Map variables) { if (!isReadOnly() && (variables.containsKey("year") || variables.containsKey("month") || variables.containsKey("day") || variables.containsKey("hour") || variables.containsKey("min") || variables.containsKey("sec") || variables.containsKey("msec") || variables .containsKey("dateString"))) { // Old and new dates final Date oldDate = getValue(); Date newDate = null; // this enables analyzing invalid input on the server final String newDateString = (String) variables.get("dateString"); dateString = newDateString; // Gets the new date in parts boolean hasChanges = false; Map calendarFieldChanges = new HashMap(); for (Resolution r : Resolution .getResolutionsHigherOrEqualTo(resolution)) { // Only handle what the client is allowed to send. The same // resolutions that are painted String variableName = variableNameForResolution.get(r); if (variables.containsKey(variableName)) { Integer value = (Integer) variables.get(variableName); if (r == Resolution.MONTH) { // Calendar MONTH is zero based value--; } if (value >= 0) { hasChanges = true; calendarFieldChanges.put(r, value); } } } // If no new variable values were received, use the previous value if (!hasChanges) { newDate = null; } else { // Clone the calendar for date operation final Calendar cal = getCalendar(); // Update the value based on the received info // Must set in this order to avoid invalid dates (or wrong // dates if lenient is true) in calendar for (int r = Resolution.YEAR.ordinal(); r >= 0; r--) { Resolution res = Resolution.values()[r]; if (calendarFieldChanges.containsKey(res)) { // Field resolution should be included. Others are // skipped so that client can not make unexpected // changes (e.g. day change even though resolution is // year). Integer newValue = calendarFieldChanges.get(res); cal.set(res.getCalendarField(), newValue); } } newDate = cal.getTime(); } if (newDate == null && dateString != null && !"".equals(dateString)) { try { Date parsedDate = handleUnparsableDateString(dateString); setValue(parsedDate, true); /* * Ensure the value is sent to the client if the value is * set to the same as the previous (#4304). Does not repaint * if handleUnparsableDateString throws an exception. In * this case the invalid text remains in the DateField. */ markAsDirty(); } catch (Converter.ConversionException e) { /* * Datefield now contains some text that could't be parsed * into date. ValueChangeEvent is fired after the value is * changed and the flags are set */ if (oldDate != null) { /* * Set the logic value to null without firing the * ValueChangeEvent */ preventValueChangeEvent = true; try { setValue(null); } finally { preventValueChangeEvent = false; } /* * Reset the dateString (overridden to null by setValue) */ dateString = newDateString; } /* * Saves the localized message of parse error. This can be * overridden in handleUnparsableDateString. The message * will later be used to show a validation error. */ currentParseErrorMessage = e.getLocalizedMessage(); /* * The value of the DateField should be null if an invalid * value has been given. Not using setValue() since we do * not want to cause the client side value to change. */ uiHasValidDateString = false; /* * If value was changed fire the ValueChangeEvent */ if (oldDate != null) { fireValueChange(false); } /* * Because of our custom implementation of isValid(), that * also checks the parsingSucceeded flag, we must also * notify the form (if this is used in one) that the * validity of this field has changed. * * Normally fields validity doesn't change without value * change and form depends on this implementation detail. */ notifyFormOfValidityChange(); markAsDirty(); } } else if (newDate != oldDate && (newDate == null || !newDate.equals(oldDate))) { setValue(newDate, true); // Don't require a repaint, client // updates itself } else if (!uiHasValidDateString) { // oldDate == // newDate == null // Empty value set, previously contained unparsable date string, // clear related internal fields setValue(null); } } if (variables.containsKey(FocusEvent.EVENT_ID)) { fireEvent(new FocusEvent(this)); } if (variables.containsKey(BlurEvent.EVENT_ID)) { fireEvent(new BlurEvent(this)); } } /* * only fires the event if preventValueChangeEvent flag is false */ @Override protected void fireValueChange(boolean repaintIsNotNeeded) { if (!preventValueChangeEvent) { super.fireValueChange(repaintIsNotNeeded); } } /** * This method is called to handle a non-empty date string from the client * if the client could not parse it as a Date. * * By default, a Converter.ConversionException is thrown, and the current * value is not modified. * * This can be overridden to handle conversions, to return null (equivalent * to empty input), to throw an exception or to fire an event. * * @param dateString * @return parsed Date * @throws Converter.ConversionException * to keep the old value and indicate an error */ protected Date handleUnparsableDateString(String dateString) throws Converter.ConversionException { currentParseErrorMessage = null; throw new Converter.ConversionException(getParseErrorMessage()); } /* Property features */ /* * Gets the edited property's type. Don't add a JavaDoc comment here, we use * the default documentation from implemented interface. */ @Override public Class getType() { return Date.class; } /* * (non-Javadoc) * * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object, boolean) */ @Override protected void setValue(Date newValue, boolean repaintIsNotNeeded) throws Property.ReadOnlyException { /* * First handle special case when the client side component have a date * string but value is null (e.g. unparsable date string typed in by the * user). No value changes should happen, but we need to do some * internal housekeeping. */ if (newValue == null && !uiHasValidDateString) { /* * Side-effects of setInternalValue clears possible previous strings * and flags about invalid input. */ setInternalValue(null); /* * Due to DateField's special implementation of isValid(), * datefields validity may change although the logical value does * not change. This is an issue for Form which expects that validity * of Fields cannot change unless actual value changes. * * So we check if this field is inside a form and the form has * registered this as a field. In this case we repaint the form. * Without this hacky solution the form might not be able to clean * validation errors etc. We could avoid this by firing an extra * value change event, but feels like at least as bad solution as * this. */ notifyFormOfValidityChange(); markAsDirty(); return; } super.setValue(newValue, repaintIsNotNeeded); } /** * Detects if this field is used in a Form (logically) and if so, notifies * it (by repainting it) that the validity of this field might have changed. */ private void notifyFormOfValidityChange() { Component parenOfDateField = getParent(); boolean formFound = false; while (parenOfDateField != null || formFound) { if (parenOfDateField instanceof Form) { Form f = (Form) parenOfDateField; Collection visibleItemProperties = f.getItemPropertyIds(); for (Object fieldId : visibleItemProperties) { Field field = f.getField(fieldId); if (equals(field)) { /* * this datefield is logically in a form. Do the same * thing as form does in its value change listener that * it registers to all fields. */ f.markAsDirty(); formFound = true; break; } } } if (formFound) { break; } parenOfDateField = parenOfDateField.getParent(); } } @Override protected void setInternalValue(Date newValue) { // Also set the internal dateString if (newValue != null) { dateString = newValue.toString(); } else { dateString = null; } if (!uiHasValidDateString) { // clear component error and parsing flag setComponentError(null); uiHasValidDateString = true; currentParseErrorMessage = null; } super.setInternalValue(newValue); } /** * Gets the resolution. * * @return int */ public Resolution getResolution() { return resolution; } /** * Sets the resolution of the DateField. * * The default resolution is {@link Resolution#DAY} since Vaadin 7.0. * * @param resolution * the resolution to set. */ public void setResolution(Resolution resolution) { this.resolution = resolution; updateRangeValidator(); markAsDirty(); } /** * Returns new instance calendar used in Date conversions. * * Returns new clone of the calendar object initialized using the the * current date (if available) * * If this is no calendar is assigned the Calendar.getInstance * is used. * * @return the Calendar. * @see #setCalendar(Calendar) */ private Calendar getCalendar() { // Makes sure we have an calendar instance if (calendar == null) { calendar = Calendar.getInstance(); // Start by a zeroed calendar to avoid having values for lower // resolution variables e.g. time when resolution is day int min, field; for (Resolution r : Resolution.getResolutionsLowerThan(resolution)) { field = r.getCalendarField(); min = calendar.getActualMinimum(field); calendar.set(field, min); } calendar.set(Calendar.MILLISECOND, 0); } // Clone the instance final Calendar newCal = (Calendar) calendar.clone(); final TimeZone currentTimeZone = getTimeZone(); if (currentTimeZone != null) { newCal.setTimeZone(currentTimeZone); } final Date currentDate = getValue(); if (currentDate != null) { newCal.setTime(currentDate); } return newCal; } /** * Sets formatting used by some component implementations. See * {@link SimpleDateFormat} for format details. * * By default it is encouraged to used default formatting defined by Locale, * but due some JVM bugs it is sometimes necessary to use this method to * override formatting. See Vaadin issue #2200. * * @param dateFormat * the dateFormat to set * * @see com.vaadin.ui.AbstractComponent#setLocale(Locale)) */ public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; markAsDirty(); } /** * Returns a format string used to format date value on client side or null * if default formatting from {@link Component#getLocale()} is used. * * @return the dateFormat */ public String getDateFormat() { return dateFormat; } /** * Specifies whether or not date/time interpretation in component is to be * lenient. * * @see Calendar#setLenient(boolean) * @see #isLenient() * * @param lenient * true if the lenient mode is to be turned on; false if it is to * be turned off. */ public void setLenient(boolean lenient) { this.lenient = lenient; markAsDirty(); } /** * Returns whether date/time interpretation is to be lenient. * * @see #setLenient(boolean) * * @return true if the interpretation mode of this calendar is lenient; * false otherwise. */ public boolean isLenient() { return lenient; } @Override public void addFocusListener(FocusListener listener) { addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, FocusListener.focusMethod); } /** * @deprecated As of 7.0, replaced by * {@link #addFocusListener(FocusListener)} **/ @Override @Deprecated public void addListener(FocusListener listener) { addFocusListener(listener); } @Override public void removeFocusListener(FocusListener listener) { removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); } /** * @deprecated As of 7.0, replaced by * {@link #removeFocusListener(FocusListener)} **/ @Override @Deprecated public void removeListener(FocusListener listener) { removeFocusListener(listener); } @Override public void addBlurListener(BlurListener listener) { addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, BlurListener.blurMethod); } /** * @deprecated As of 7.0, replaced by {@link #addBlurListener(BlurListener)} **/ @Override @Deprecated public void addListener(BlurListener listener) { addBlurListener(listener); } @Override public void removeBlurListener(BlurListener listener) { removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); } /** * @deprecated As of 7.0, replaced by * {@link #removeBlurListener(BlurListener)} **/ @Override @Deprecated public void removeListener(BlurListener listener) { removeBlurListener(listener); } /** * Checks whether ISO 8601 week numbers are shown in the date selector. * * @return true if week numbers are shown, false otherwise. */ public boolean isShowISOWeekNumbers() { return showISOWeekNumbers; } /** * Sets the visibility of ISO 8601 week numbers in the date selector. ISO * 8601 defines that a week always starts with a Monday so the week numbers * are only shown if this is the case. * * @param showWeekNumbers * true if week numbers should be shown, false otherwise. */ public void setShowISOWeekNumbers(boolean showWeekNumbers) { showISOWeekNumbers = showWeekNumbers; markAsDirty(); } /** * Validates the current value against registered validators if the field is * not empty. Note that DateField is considered empty (value == null) and * invalid if it contains text typed in by the user that couldn't be parsed * into a Date value. * * @see com.vaadin.ui.AbstractField#validate() */ @Override public void validate() throws InvalidValueException { /* * To work properly in form we must throw exception if there is * currently a parsing error in the datefield. Parsing error is kind of * an internal validator. */ if (!uiHasValidDateString) { throw new UnparsableDateString(currentParseErrorMessage); } super.validate(); } /** * Return the error message that is shown if the user inputted value can't * be parsed into a Date object. If * {@link #handleUnparsableDateString(String)} is overridden and it throws a * custom exception, the message returned by * {@link Exception#getLocalizedMessage()} will be used instead of the value * returned by this method. * * @see #setParseErrorMessage(String) * * @return the error message that the DateField uses when it can't parse the * textual input from user to a Date object */ public String getParseErrorMessage() { return defaultParseErrorMessage; } /** * Sets the default error message used if the DateField cannot parse the * text input by user to a Date field. Note that if the * {@link #handleUnparsableDateString(String)} method is overridden, the * localized message from its exception is used. * * @see #getParseErrorMessage() * @see #handleUnparsableDateString(String) * @param parsingErrorMessage */ public void setParseErrorMessage(String parsingErrorMessage) { defaultParseErrorMessage = parsingErrorMessage; } /** * Sets the time zone used by this date field. The time zone is used to * convert the absolute time in a Date object to a logical time displayed in * the selector and to convert the select time back to a Date object. * * If no time zone has been set, the current default time zone returned by * {@code TimeZone.getDefault()} is used. * * @see #getTimeZone() * @param timeZone * the time zone to use for time calculations. */ public void setTimeZone(TimeZone timeZone) { this.timeZone = timeZone; markAsDirty(); } /** * Gets the time zone used by this field. The time zone is used to convert * the absolute time in a Date object to a logical time displayed in the * selector and to convert the select time back to a Date object. * * If {@code null} is returned, the current default time zone returned by * {@code TimeZone.getDefault()} is used. * * @return the current time zone */ public TimeZone getTimeZone() { return timeZone; } public static class UnparsableDateString extends Validator.InvalidValueException { public UnparsableDateString(String message) { super(message); } } @Override public void readDesign(Element design, DesignContext designContext) { super.readDesign(design, designContext); if (design.hasAttr("value") && !design.attr("value").isEmpty()) { Date date = DesignAttributeHandler.getFormatter().parse( design.attr("value"), Date.class); // formatting will return null if it cannot parse the string if (date == null) { Logger.getLogger(DateField.class.getName()).info( "cannot parse " + design.attr("value") + " as date"); } this.setValue(date); } } @Override public void writeDesign(Element design, DesignContext designContext) { super.writeDesign(design, designContext); if (getValue() != null) { design.attr("value", DesignAttributeHandler.getFormatter().format(getValue())); } } /** * Returns current date-out-of-range error message. * * @see #setDateOutOfRangeMessage(String) * @since 7.4 * @return Current error message for dates out of range. */ public String getDateOutOfRangeMessage() { return dateOutOfRangeMessage; } } ackport/48425/stable29'>backport/48425/stable29 Nextcloud server, a safe home for all your data: https://github.com/nextcloud/serverwww-data
aboutsummaryrefslogtreecommitdiffstats
blob: 2aa52ef1b7fb8413827d6846faf485f7b68b176f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<?php

/**
 * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OCA\Files_Sharing\ShareBackend;

use OC\Files\Filesystem;
use OC\Files\View;
use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\Files_Sharing\Helper;
use OCP\Files\NotFoundException;
use OCP\IDBConnection;
use OCP\Server;
use OCP\Share\IShare;
use OCP\Share_Backend_File_Dependent;
use Psr\Log\LoggerInterface;

class File implements Share_Backend_File_Dependent {
	public const FORMAT_SHARED_STORAGE = 0;
	public const FORMAT_GET_FOLDER_CONTENTS = 1;
	public const FORMAT_FILE_APP_ROOT = 2;
	public const FORMAT_OPENDIR = 3;
	public const FORMAT_GET_ALL = 4;
	public const FORMAT_PERMISSIONS = 5;
	public const FORMAT_TARGET_NAMES = 6;

	private $path;

	public function __construct(
		private ?FederatedShareProvider $federatedShareProvider = null,
	) {
		if ($federatedShareProvider) {
			$this->federatedShareProvider = $federatedShareProvider;
		} else {
			$this->federatedShareProvider = Server::get(FederatedShareProvider::class);
		}
	}

	public function isValidSource($itemSource, $uidOwner) {
		try {
			$path = Filesystem::getPath($itemSource);
			// FIXME: attributes should not be set here,
			// keeping this pattern for now to avoid unexpected
			// regressions
			$this->path = Filesystem::normalizePath(basename($path));
			return true;
		} catch (NotFoundException $e) {
			return false;
		}
	}

	public function getFilePath($itemSource, $uidOwner) {
		if (isset($this->path)) {
			$path = $this->path;
			$this->path = null;
			return $path;
		} else {
			try {
				$path = Filesystem::getPath($itemSource);
				return $path;
			} catch (NotFoundException $e) {
				return false;
			}
		}
	}

	/**
	 * create unique target
	 *
	 * @param string $itemSource
	 * @param string $shareWith
	 * @return string
	 */
	public function generateTarget($itemSource, $shareWith) {
		$shareFolder = Helper::getShareFolder();
		$target = Filesystem::normalizePath($shareFolder . '/' . basename($itemSource));

		Filesystem::initMountPoints($shareWith);
		$view = new View('/' . $shareWith . '/files');

		if (!$view->is_dir($shareFolder)) {
			$dir = '';
			$subdirs = explode('/', $shareFolder);
			foreach ($subdirs as $subdir) {
				$dir = $dir . '/' . $subdir;
				if (!$view->is_dir($dir)) {
					$view->mkdir($dir);
				}
			}
		}

		return Helper::generateUniqueTarget($target, $view);
	}

	public function formatItems($items, $format, $parameters = null) {
		if ($format === self::FORMAT_SHARED_STORAGE) {
			// Only 1 item should come through for this format call
			$item = array_shift($items);
			return [
				'parent' => $item['parent'],
				'path' => $item['path'],
				'storage' => $item['storage'],
				'permissions' => $item['permissions'],
				'uid_owner' => $item['uid_owner'],
			];
		} elseif ($format === self::FORMAT_GET_FOLDER_CONTENTS) {
			$files = [];
			foreach ($items as $item) {
				$file = [];
				$file['fileid'] = $item['file_source'];
				$file['storage'] = $item['storage'];
				$file['path'] = $item['file_target'];
				$file['parent'] = $item['file_parent'];
				$file['name'] = basename($item['file_target']);
				$file['mimetype'] = $item['mimetype'];
				$file['mimepart'] = $item['mimepart'];
				$file['mtime'] = $item['mtime'];
				$file['encrypted'] = $item['encrypted'];
				$file['etag'] = $item['etag'];
				$file['uid_owner'] = $item['uid_owner'];
				$file['displayname_owner'] = $item['displayname_owner'];

				$storage = Filesystem::getStorage('/');
				$cache = $storage->getCache();
				$file['size'] = $item['size'];
				$files[] = $file;
			}
			return $files;
		} elseif ($format === self::FORMAT_OPENDIR) {
			$files = [];
			foreach ($items as $item) {
				$files[] = basename($item['file_target']);
			}
			return $files;
		} elseif ($format === self::FORMAT_GET_ALL) {
			$ids = [];
			foreach ($items as $item) {
				$ids[] = $item['file_source'];
			}
			return $ids;
		} elseif ($format === self::FORMAT_PERMISSIONS) {
			$filePermissions = [];
			foreach ($items as $item) {
				$filePermissions[$item['file_source']] = $item['permissions'];
			}
			return $filePermissions;
		} elseif ($format === self::FORMAT_TARGET_NAMES) {
			$targets = [];
			foreach ($items as $item) {
				$targets[] = $item['file_target'];
			}
			return $targets;
		}
		return [];
	}

	/**
	 * check if server2server share is enabled
	 *
	 * @param int $shareType
	 * @return boolean
	 */
	public function isShareTypeAllowed($shareType) {
		if ($shareType === IShare::TYPE_REMOTE) {
			return $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
		}

		if ($shareType === IShare::TYPE_REMOTE_GROUP) {
			return $this->federatedShareProvider->isOutgoingServer2serverGroupShareEnabled();
		}

		return true;
	}

	/**
	 * resolve reshares to return the correct source item
	 * @param array $source
	 * @return array source item
	 */
	protected static function resolveReshares($source) {
		if (isset($source['parent'])) {
			$parent = $source['parent'];
			while (isset($parent)) {
				$qb = Server::get(IDBConnection::class)->getQueryBuilder();
				$qb->select('parent', 'uid_owner')
					->from('share')
					->where(
						$qb->expr()->eq('id', $qb->createNamedParameter($parent))
					);
				$result = $qb->executeQuery();
				$item = $result->fetch();
				$result->closeCursor();
				if (isset($item['parent'])) {
					$parent = $item['parent'];
				} else {
					$fileOwner = $item['uid_owner'];
					break;
				}
			}
		} else {
			$fileOwner = $source['uid_owner'];
		}
		if (isset($fileOwner)) {
			$source['fileOwner'] = $fileOwner;
		} else {
			Server::get(LoggerInterface::class)->error('No owner found for reshare', ['app' => 'files_sharing']);
		}

		return $source;
	}

	/**
	 * @param string $target
	 * @param array $share
	 * @return array|false source item
	 */
	public static function getSource($target, $share) {
		if ($share['item_type'] === 'folder' && $target !== '') {
			// note: in case of ext storage mount points the path might be empty
			// which would cause a leading slash to appear
			$share['path'] = ltrim($share['path'] . '/' . $target, '/');
		}
		return self::resolveReshares($share);
	}
}