1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates an empty calItem view used to display read-only calendar items.
 26  * @constructor
 27  * @class
 28  * Simple read-only view of an appointment or task. It looks more or less like a
 29  * message - the notes have their own area at the bottom, and everything else
 30  * goes into a header section at the top.
 31  *
 32  * @author Parag Shah
 33  * @author Conrad Damon
 34  *
 35  * @param {DwtComposite}	parent		the parent widget
 36  * @param {constant}	posStyle	the positioning style
 37  * @param {ZmController}	controller	the owning controller
 38  * 
 39  * @extends		ZmMailMsgView
 40  * 
 41  * @private
 42  */
 43 ZmCalItemView = function(parent, posStyle, controller, id) {
 44 	if (arguments.length == 0) return;
 45 
 46 	params = {parent: parent, posStyle: posStyle, controller: controller};
 47 	if (id) {
 48 		params.id = id;
 49 	}
 50 	ZmMailMsgView.call(this, params);
 51 };
 52 
 53 ZmCalItemView.prototype = new ZmMailMsgView;
 54 ZmCalItemView.prototype.constructor = ZmCalItemView;
 55 
 56 // Public methods
 57 
 58 ZmCalItemView.prototype.isZmCalItemView = true;
 59 ZmCalItemView.prototype.toString = function() { return "ZmCalItemView"; };
 60 
 61 ZmCalItemView.prototype.getController =
 62 function() {
 63 	return this._controller;
 64 };
 65 
 66 // Following public overrides are a hack to allow this view to pretend it's a list view,
 67 // as well as a calendar view
 68 ZmCalItemView.prototype.getSelection =
 69 function() {
 70 	return [this._calItem];
 71 };
 72 
 73 ZmCalItemView.prototype.getSelectionCount =
 74 function() {
 75 	return 1;
 76 };
 77 
 78 ZmCalItemView.prototype.needsRefresh =
 79 function() {
 80 	return false;
 81 };
 82 
 83 ZmCalItemView.prototype.addSelectionListener = function() {};
 84 ZmCalItemView.prototype.addActionListener = function() {};
 85 ZmCalItemView.prototype.handleActionPopdown = function(ev) {};
 86 
 87 ZmCalItemView.prototype.getTitle =
 88 function() {
 89 	// override
 90 };
 91 
 92 ZmCalItemView.prototype.set =
 93 function(calItem, prevView, mode) {
 94 	if (this._calItem == calItem) { return; }
 95 
 96 	// So that Close button knows which view to go to
 97     // condition introduced to avoid irrelevant view being persisted as previous view
 98 	var viewMgr = this._controller._viewMgr;
 99 	this._prevView = prevView || (viewMgr && (calItem.folderId != ZmFolder.ID_TRASH) ?
100 	                              viewMgr.getCurrentViewName() : this._prevView);
101 
102 	this.reset();
103 	this._calItem = this._item = calItem;
104 	this._mode = mode;
105 	this._renderCalItem(calItem, true);
106 };
107 
108 ZmCalItemView.prototype.reset =
109 function() {
110 	ZmMailMsgView.prototype.reset.call(this);
111 	this._calItem = this._item = null;
112 };
113 
114 ZmCalItemView.prototype.close = function() {}; // override
115 ZmCalItemView.prototype.move = function() {}; // override
116 ZmCalItemView.prototype.changeReminder = function() {}; // override
117 
118 
119 // Private / protected methods
120 
121 ZmCalItemView.prototype._renderCalItem =
122 function(calItem, renderButtons) {
123 	this._lazyCreateObjectManager();
124 
125 	var subs = this._getSubs(calItem);
126 	var closeBtnCellId = this._htmlElId + "_closeBtnCell";
127 	var editBtnCellId = this._htmlElId + "_editBtnCell";
128 	this._hdrTableId = this._htmlElId + "_hdrTable";
129 
130     var calendar = calItem.getFolder();
131     var isReadOnly = calendar.isReadOnly();
132     subs.allowEdit = !isReadOnly && (appCtxt.get(ZmSetting.CAL_APPT_ALLOW_ATTENDEE_EDIT) || calItem.isOrg);
133 
134 	var el = this.getHtmlElement();
135 	el.innerHTML = AjxTemplate.expand("calendar.Appointment#ReadOnlyView", subs);
136 	var offlineHandler = appCtxt.webClientOfflineHandler;
137 	if (offlineHandler) {
138 		var linkIds = [ZmCalItem.ATT_LINK_IMAGE, ZmCalItem.ATT_LINK_MAIN, ZmCalItem.ATT_LINK_DOWNLOAD];
139 		var getLinkIdCallback = this._getAttachmentLinkId.bind(this);
140 		offlineHandler._handleAttachmentsForOfflineMode(calItem.getAttachments(), getLinkIdCallback, linkIds);
141 	}
142 
143     if (renderButtons) {
144         // add the close button
145         this._closeButton = new DwtButton({parent:this, className:"DwtToolbarButton"});
146         this._closeButton.setImage("Close");
147         this._closeButton.setText(ZmMsg.close);
148         this._closeButton.addSelectionListener(new AjxListener(this, this.close));
149         this._closeButton.reparentHtmlElement(closeBtnCellId);
150 
151         if (document.getElementById(editBtnCellId)) {
152             // add the save button for reminders and  move select
153             this._editButton = new DwtButton({parent:this, className:"DwtToolbarButton"});
154             this._editButton.setImage("Edit");
155             this._editButton.setText(ZmMsg.edit);
156             this._editButton.addSelectionListener(new AjxListener(this, this.edit));
157             var calendar = calItem && appCtxt.getById(calItem.folderId);
158             var isTrash = calendar && calendar.id == ZmOrganizer.ID_TRASH;
159             this._editButton.setEnabled(!isTrash);
160             this._editButton.reparentHtmlElement(editBtnCellId);
161         }
162     }
163 
164 	// content/body
165 	var hasHtmlPart = (calItem.notesTopPart && calItem.notesTopPart.getContentType() == ZmMimeTable.MULTI_ALT);
166 	var mode = (hasHtmlPart && appCtxt.get(ZmSetting.VIEW_AS_HTML))
167 		? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN;
168 
169 	var bodyPart = calItem.getNotesPart(mode);
170 	if (bodyPart) {
171 		this._msg = this._msg || this._calItem._currentlyLoaded;
172         if (mode === ZmMimeTable.TEXT_PLAIN) {
173             bodyPart = AjxStringUtil.convertToHtml(bodyPart);
174         }
175 		this._makeIframeProxy({container: el, html:bodyPart, isTextMsg:(mode == ZmMimeTable.TEXT_PLAIN)});
176 	}
177 };
178 
179 ZmCalItemView.prototype._getSubs =
180 function(calItem) {
181 	// override
182 };
183 
184 ZmCalItemView.prototype._getTimeString =
185 function(calItem) {
186 	// override
187 };
188 
189 ZmCalItemView.prototype._setAttachmentLinks =
190 function() {
191 	// do nothing since calItem view renders attachments differently
192 };
193 
194 // returns true if given dates are w/in a single day
195 ZmCalItemView.prototype._isOneDayAppt =
196 function(sd, ed) {
197 	var start = new Date(sd.getTime());
198 	var end = new Date(ed.getTime());
199 
200 	start.setHours(0, 0, 0, 0);
201 	end.setHours(0, 0, 0, 0);
202 
203 	return start.valueOf() == end.valueOf();
204 };
205 
206 
207 
208 ZmCalItemView.prototype._getAttachString =
209 function(calItem) {
210 	var str = [];
211 	var j = 0;
212 
213 	var attachList = calItem.getAttachments();
214 	if (attachList) {
215 		var getLinkIdCallback = this._getAttachmentLinkId.bind(this);
216 		for (var i = 0; i < attachList.length; i++) {
217 			str[j++] = ZmApptViewHelper.getAttachListHtml(calItem, attachList[i], false, getLinkIdCallback);
218 		}
219 	}
220 
221 	return str.join("");
222 };
223 
224 ZmCalItemView.rfc822Callback =
225 function(invId, partId) {
226 	AjxDispatcher.require("MailCore", false);
227 	ZmMailMsgView.rfc822Callback(invId, partId);
228 };
229 
230 /**
231  * Creates an empty appointment view.
232  * @constructor
233  * @class
234  * Simple read-only view of an appointment. It looks more or less like a message -
235  * the notes have their own area at the bottom, and everything else goes into a
236  * header section at the top.
237  *
238  * @author Parag Shah
239  * @author Conrad Damon
240  *
241  * @param {DwtComposite}	parent		the parent widget
242  * @param {constant}	posStyle	the positioning style
243  * @param {ZmController}	controller	the owning controller
244  * 
245  * @extends		ZmCalItemView
246  * 
247  * @private
248  */
249 ZmApptView = function(parent, posStyle, controller) {
250 
251 	ZmCalItemView.call(this, parent, posStyle, controller);
252 };
253 
254 ZmApptView.prototype = new ZmCalItemView;
255 ZmApptView.prototype.constructor = ZmApptView;
256 
257 ZmApptView.prototype.isZmApptView = true;
258 ZmApptView.prototype.toString = function() { return "ZmApptView"; };
259 
260 // Public methods
261 
262 ZmApptView.prototype.getTitle =
263 function() {
264     return [ZmMsg.zimbraTitle, ZmMsg.appointment].join(": ");
265 };
266 
267 ZmApptView.prototype.edit =
268 function(ev) {
269 	var item = this._calItem;
270 
271     if(!item.isOrg && !(this._editWarningDialog && this._editWarningDialog.isPoppedUp())){
272         var msgDialog = this._editWarningDialog = appCtxt.getMsgDialog();
273         msgDialog.setMessage(ZmMsg.attendeeEditWarning, DwtMessageDialog.WARNING_STYLE);
274         msgDialog.popup();
275         msgDialog.registerCallback(DwtDialog.OK_BUTTON, this.edit, this);
276         return;
277     }else if(this._editWarningDialog){
278         this._editWarningDialog.popdown();
279 		this._editWarningDialog.reset();
280     }
281     
282 	var mode = ZmCalItem.MODE_EDIT;
283 	if (item.isRecurring()) {
284 		mode = this._mode || ZmCalItem.MODE_EDIT_SINGLE_INSTANCE;
285 	}
286 	item.setViewMode(mode);
287 	var app = this._controller._app;
288 	app.getApptComposeController().show(item, mode);
289 };
290 
291 ZmApptView.prototype.setBounds =
292 function(x, y, width, height) {
293 	// dont reset the width!
294 	ZmMailMsgView.prototype.setBounds.call(this, x, y, Dwt.DEFAULT, height);
295 };
296 
297 ZmApptView.prototype._renderCalItem =
298 function(calItem) {
299 
300 	this._lazyCreateObjectManager();
301 
302 	var subs = this._getSubs(calItem);
303 	subs.subject = AjxStringUtil.htmlEncode(subs.subject);
304 
305 	this._hdrTableId = this._htmlElId + "_hdrTable";
306 
307     var calendar = calItem.getFolder();
308     var isReadOnly = calendar.isReadOnly() || calendar.isInTrash();
309     subs.allowEdit = !isReadOnly && (appCtxt.get(ZmSetting.CAL_APPT_ALLOW_ATTENDEE_EDIT) || calItem.isOrg);
310 
311 	var el = this.getHtmlElement();
312 	el.innerHTML = AjxTemplate.expand("calendar.Appointment#ReadOnlyView", subs);
313 	var offlineHandler = appCtxt.webClientOfflineHandler;
314 	if (offlineHandler) {
315 		var linkIds = [ZmCalItem.ATT_LINK_IMAGE, ZmCalItem.ATT_LINK_MAIN, ZmCalItem.ATT_LINK_DOWNLOAD];
316 		var getLinkIdCallback = this._getAttachmentLinkId.bind(this);
317 		offlineHandler._handleAttachmentsForOfflineMode(calItem.getAttachments(), getLinkIdCallback, linkIds);
318 	}
319 
320 	// Set tab name as Appointment subject
321 	var subject = AjxStringUtil.trim(calItem.getName());
322 	if (subject) {
323 		var tabButtonText = subject.substring(0, ZmAppViewMgr.TAB_BUTTON_MAX_TEXT);
324 		appCtxt.getAppViewMgr().setTabTitle(this._controller.getCurrentViewId(), tabButtonText);
325 	}
326 
327 	this._createBubbles();
328 
329     var selParams = {parent: this, id: Dwt.getNextId('ZmNeedActionSelect_')};
330     var statusSelect = new DwtSelect(selParams);
331 
332     var ptst = {};
333     ptst[ZmCalBaseItem.PSTATUS_NEEDS_ACTION] = ZmMsg.ptstMsgNeedsAction;
334     ptst[ZmCalBaseItem.PSTATUS_ACCEPT] = ZmMsg.ptstMsgAccepted;
335     ptst[ZmCalBaseItem.PSTATUS_TENTATIVE] = ZmMsg.ptstMsgTentative;
336     ptst[ZmCalBaseItem.PSTATUS_DECLINED] = ZmMsg.ptstMsgDeclined;
337 
338     this._ptst = ptst;
339     //var statusMsgs = {};
340     var calItemPtst = calItem.ptst || ZmCalBaseItem.PSTATUS_ACCEPT;
341 
342     var data = null;
343     for (var stat in ptst) {
344         //stat = ptst[index];
345         if (stat === ZmCalBaseItem.PSTATUS_NEEDS_ACTION && calItemPtst !== ZmCalBaseItem.PSTATUS_NEEDS_ACTION) { continue; }
346         data = new DwtSelectOptionData(stat, ZmCalItem.getLabelForParticipationStatus(stat), false, null, ZmCalItem.getParticipationStatusIcon(stat), Dwt.getNextId('ZmNeedActionOption_' + stat + '_'));
347         statusSelect.addOption(data);
348         if (stat == calItemPtst){
349             statusSelect.setSelectedValue(stat);
350         }
351     }
352     if (isReadOnly) { statusSelect.setEnabled(false); }
353 
354     this._statusSelect = statusSelect;
355     this._origPtst = calItemPtst;
356     statusSelect.reparentHtmlElement(this._htmlElId + "_responseActionSelectCell");
357     statusSelect.addChangeListener(new AjxListener(this, this._statusSelectListener));
358 
359     this._statusMsgEl = document.getElementById(this._htmlElId + "_responseActionMsgCell");
360     this._statusMsgEl.innerHTML = ptst[calItemPtst];
361 
362 	// content/body
363 	var hasHtmlPart = (calItem.notesTopPart && calItem.notesTopPart.getContentType() == ZmMimeTable.MULTI_ALT);
364 	var mode = (hasHtmlPart && appCtxt.get(ZmSetting.VIEW_AS_HTML))
365 		? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN;
366 
367 	var bodyPart = calItem.getNotesPart(mode);
368 	if (bodyPart) {
369 		this._msg = this._msg || this._calItem._currentlyLoaded;
370         if (mode === ZmMimeTable.TEXT_PLAIN) {
371             bodyPart = AjxStringUtil.convertToHtml(bodyPart);
372         }
373 		this._makeIframeProxy({container: el, html:bodyPart, isTextMsg:(mode == ZmMimeTable.TEXT_PLAIN)});
374 	}
375 };
376 
377 ZmApptView.prototype._getSubs =
378 function(calItem) {
379 	var subject   = calItem.getName();
380 	var location  = calItem.location;
381 	var equipment = calItem.getAttendeesText(ZmCalBaseItem.EQUIPMENT, true);
382 	var isException = calItem._orig.isException;
383 	var dateStr = this._getTimeString(calItem);
384 
385 	this._clearBubbles();
386 	var reqAttendees = this._getAttendeesByRoleCollapsed(calItem.getAttendees(ZmCalBaseItem.PERSON), ZmCalBaseItem.PERSON, ZmCalItem.ROLE_REQUIRED);
387 	var optAttendees = this._getAttendeesByRoleCollapsed(calItem.getAttendees(ZmCalBaseItem.PERSON), ZmCalBaseItem.PERSON, ZmCalItem.ROLE_OPTIONAL);
388 	var hasAttendees = reqAttendees || optAttendees;
389 
390 	var organizer, obo;
391 	var recurStr = calItem.isRecurring() ? calItem.getRecurBlurb() : null;
392 	var attachStr = this._getAttachString(calItem);
393 
394 	if (hasAttendees) { // I really don't know why this check here but it's the way it was before so keeping it. (I just renamed the var)
395 		organizer = new AjxEmailAddress(calItem.getOrganizer(), null, calItem.getOrganizerName());
396 
397 		var sender = calItem.message.getAddress(AjxEmailAddress.SENDER);
398 		var from = calItem.message.getAddress(AjxEmailAddress.FROM);
399 		var address = sender || from;
400 		if (!organizer && address)	{
401 			organizer = address.toString();
402 		}
403 		if (sender && organizer) {
404 			obo = from ? new AjxEmailAddress(from.toString()) : organizer;
405 		}
406 	}
407 
408 	organizer = organizer && this._getBubbleHtml(organizer);
409 	obo = obo && this._getBubbleHtml(obo);
410 
411 	return {
412 		id:             this._htmlElId,
413 		subject:        subject,
414 		location:       location,
415 		equipment:      equipment,
416 		isException:    isException,
417 		dateStr:        dateStr,
418         isAttendees:    hasAttendees,
419         reqAttendees:   reqAttendees,
420 		optAttendees:   optAttendees,
421 		org:            organizer,
422 		obo:            obo,
423 		recurStr:       recurStr,
424 		attachStr:      attachStr,
425 		folder:         appCtxt.getTree(ZmOrganizer.CALENDAR).getById(calItem.folderId),
426 		folderLabel:    ZmMsg.calendar,
427 		reminderLabel:  ZmMsg.reminder,
428 		alarm:          calItem.alarm,
429 		isAppt:         true,
430         _infoBarId:     this._infoBarId
431 	};
432 };
433 
434 /**
435  * Creates a string of attendees by role. If an item doesn't have a name, its address is used.
436  *
437  * calls common code from mail msg view to get the collapse/expand "show more" funcitonality for large lists.
438  *
439  * @param list					[array]			list of attendees (ZmContact or ZmResource)
440  * @param type					[constant]		attendee type
441  * @param role      		        [constant]      attendee role
442  */
443 ZmApptView.prototype._getAttendeesByRoleCollapsed = function(list, type, role) {
444 
445 	if (!(list && list.length)) {
446 		return "";
447 	}
448 	var attendees = ZmApptViewHelper.getAttendeesArrayByRole(list, role);
449 
450 	var emails = [];
451 	for (var i = 0; i < attendees.length; i++) {
452 		var att = attendees[i];
453 		emails.push(new AjxEmailAddress(att.getEmail(), type, att.getFullName(), att.getFullName(), att.isGroup(), att.canExpand));
454 	}
455 
456 	var options = {};
457 	options.shortAddress = appCtxt.get(ZmSetting.SHORT_ADDRESS);
458 	var addressInfo = this.getAddressesFieldHtmlHelper(emails, options, role);
459 	return addressInfo.html;
460 };
461 
462 ZmApptView.prototype._getTimeString =
463 function(calItem) {
464 	var sd = calItem._orig.startDate;
465 	var ed = calItem._orig.endDate;
466     var tz = AjxMsg[AjxTimezone.DEFAULT] || AjxTimezone.getServerId(AjxTimezone.DEFAULT)
467 
468 	if (calItem.isRecurring() && this._mode == ZmCalItem.MODE_EDIT_SERIES) {
469 		sd = calItem.startDate;
470 		ed = calItem.endDate;
471         var seriesTZ = calItem.getTimezone();
472 
473         //convert to client timezone if appt's timezone differs
474         if(seriesTZ != AjxTimezone.getServerId(AjxTimezone.DEFAULT)) {
475             var offset1 = AjxTimezone.getOffset(AjxTimezone.DEFAULT, sd);
476 		    var offset2 = AjxTimezone.getOffset(AjxTimezone.getClientId(seriesTZ), sd);
477             sd.setTime(sd.getTime() + (offset1 - offset2)*60*1000);
478             ed.setTime(ed.getTime() + (offset1 - offset2)*60*1000);
479             calItem.setTimezone(AjxTimezone.getServerId(AjxTimezone.DEFAULT));
480         }
481 	}
482 
483 	var isAllDay = calItem.isAllDayEvent();
484 	var isMultiDay = calItem.isMultiDay();
485 	if (isAllDay && isMultiDay) {
486 		var endDate = new Date(ed.getTime());
487 		ed.setDate(endDate.getDate()-1);
488 	}
489 
490 	var pattern = isAllDay ?
491 				  (isMultiDay ? ZmMsg.apptTimeAllDayMulti   : ZmMsg.apptTimeAllDay) :
492 				  (isMultiDay ? ZmMsg.apptTimeInstanceMulti : ZmMsg.apptTimeInstance);
493 	var params = [sd, ed, tz];
494 
495 	return AjxMessageFormat.format(pattern, params);
496 };
497 
498 ZmApptView.prototype.set =
499 function(appt, mode) {
500 	this.reset();
501 	this._calItem = this._item = appt;
502 	this._mode = mode;
503 	this._renderCalItem(appt, false);
504 };
505 
506 ZmApptView.prototype.reEnableDesignMode =
507 function() {
508 
509 };
510 
511 ZmApptView.prototype.isDirty =
512 function() {
513     var retVal = false,
514         value = this._statusSelect.getValue();
515     if(this._origPtst != value) {
516         retVal = true;
517     }
518     return retVal;
519 };
520 
521 ZmApptView.prototype.isValid =
522 function() {
523     // No fields to validate
524     return true;
525 }
526 
527 ZmApptView.prototype.setOrigPtst =
528 function(value) {
529     this._origPtst = value;
530     this._statusSelectListener();
531 };
532 
533 ZmApptView.prototype.cleanup =
534 function() {
535     return false;
536 };
537 
538 ZmApptView.prototype.close =
539 function() {
540     this._controller._closeView();
541 };
542 
543 ZmApptView.prototype.getOpValue =
544 function() {
545     var value = this._statusSelect.getValue(),
546         statusToOp = {};
547     statusToOp[ZmCalBaseItem.PSTATUS_NEEDS_ACTION] = null;
548     statusToOp[ZmCalBaseItem.PSTATUS_ACCEPT] = ZmOperation.REPLY_ACCEPT;
549     statusToOp[ZmCalBaseItem.PSTATUS_TENTATIVE] = ZmOperation.REPLY_TENTATIVE;
550     statusToOp[ZmCalBaseItem.PSTATUS_DECLINED] = ZmOperation.REPLY_DECLINE;
551     return statusToOp[value];
552 };
553 
554 ZmApptView.prototype._statusSelectListener =
555 function() {
556     var saveButton = this.getController().getCurrentToolbar().getButton(ZmOperation.SAVE),
557         value = this._statusSelect.getValue();
558     saveButton.setEnabled(this._origPtst != value);
559     this._statusMsgEl.innerHTML = this._ptst[value];
560 };
561 
562 ZmApptView.prototype._getDialogXY =
563 function() {
564 	var loc = Dwt.toWindow(this.getHtmlElement(), 0, 0);
565 	return new DwtPoint(loc.x + ZmApptComposeView.DIALOG_X, loc.y + ZmApptComposeView.DIALOG_Y);
566 };
567