diff options
author | emkael <emkael@tlen.pl> | 2016-06-07 15:17:49 +0200 |
---|---|---|
committer | emkael <emkael@tlen.pl> | 2016-06-10 11:46:41 +0200 |
commit | 823d71ced9b4947b1a5a5ade7245d521ed490061 (patch) | |
tree | a9a6c7cb0de74ff705e8320c284de423a698f5b5 /app/frontend | |
parent | df401552aac363655ab8f056a6c910a7611954d6 (diff) |
* renaming php directory
Diffstat (limited to 'app/frontend')
104 files changed, 3850 insertions, 0 deletions
diff --git a/app/frontend/application.xml b/app/frontend/application.xml new file mode 100644 index 0000000..6937e53 --- /dev/null +++ b/app/frontend/application.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<application id="http" mode="Normal"> + + <include file="Application.caches" /> + + <include file="Application.db.config" /> + <include file="Application.model.config" /> + <include file="Application.facades.config" /> + + <include file="Application.user.config" /> + + <include file="Application.url.config" /> + + <include file="Application.i18n.config" /> + + <include file="Application.web.config" /> + <include file="Application.pages.config" /> + <include file="Application.controls.config" /> + + <include file="Application.events.config" /> + + <modules> + <!-- <module id="log" class="System.Util.TLogRouter"> + <route class="TBrowserLogRoute" + Levels="Debug" + Categories="System" /> + </module> --> + </modules> + +</application> diff --git a/app/frontend/caches.xml b/app/frontend/caches.xml new file mode 100644 index 0000000..8a88717 --- /dev/null +++ b/app/frontend/caches.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="cache" class="System.Caching.TDbCache" /> + </modules> +</configuration> diff --git a/app/frontend/components/FileUploadSecureFileSize.php b/app/frontend/components/FileUploadSecureFileSize.php new file mode 100644 index 0000000..1e60c9a --- /dev/null +++ b/app/frontend/components/FileUploadSecureFileSize.php @@ -0,0 +1,18 @@ +<?php + +Prado::using('Application.components.FileUploadSecureOption'); + +trait FileUploadSecureFileSize { + + use FileUploadSecureOption; + + public function getFileSize() { + if ($this->getIsSecure()) { + return filesize($this->getLocalName()); + } + return parent::getFileSize(); + } + +} + +?> diff --git a/app/frontend/components/FileUploadSecureFileType.php b/app/frontend/components/FileUploadSecureFileType.php new file mode 100644 index 0000000..ce16501 --- /dev/null +++ b/app/frontend/components/FileUploadSecureFileType.php @@ -0,0 +1,19 @@ +<?php + +Prado::using('Application.components.FileUploadSecureOption'); + +trait FileUploadSecureFileType { + + use FileUploadSecureOption; + + public function getFileType() { + if ($this->getIsSecure()) { + $fileInfo = new finfo(FILEINFO_MIME_TYPE); + return $fileInfo->file($this->getLocalName()); + } + return parent::getFileType(); + } + +} + +?> diff --git a/app/frontend/components/FileUploadSecureMethods.php b/app/frontend/components/FileUploadSecureMethods.php new file mode 100644 index 0000000..8a42240 --- /dev/null +++ b/app/frontend/components/FileUploadSecureMethods.php @@ -0,0 +1,16 @@ +<?php + +Prado::using('Application.components.FileUploadSecureOption'); +Prado::using('Application.components.FileUploadSecureFileSize'); +Prado::using('Application.components.FileUploadSecureFileType'); + +trait FileUploadSecureMethods { + use FileUploadSecureOption, FileUploadSecureFileSize, FileUploadSecureFileType { + FileUploadSecureOption::getIsSecure + insteadof FileUploadSecureFileType, FileUploadSecureFileSize; + FileUploadSecureOption::setIsSecure + insteadof FileUploadSecureFileType, FileUploadSecureFileSize; + } +} + +?> diff --git a/app/frontend/components/FileUploadSecureOption.php b/app/frontend/components/FileUploadSecureOption.php new file mode 100644 index 0000000..3550e21 --- /dev/null +++ b/app/frontend/components/FileUploadSecureOption.php @@ -0,0 +1,17 @@ +<?php + +trait FileUploadSecureOption { + + protected $_isSecure = TRUE; + + public function getIsSecure() { + return $this->_isSecure; + } + + public function setIsSecure($bool) { + $this->_isSecure = $bool; + } + +} + +?> diff --git a/app/frontend/components/SafeActiveFileUpload.php b/app/frontend/components/SafeActiveFileUpload.php new file mode 100644 index 0000000..69bffab --- /dev/null +++ b/app/frontend/components/SafeActiveFileUpload.php @@ -0,0 +1,13 @@ +<?php + +Prado::using('System.Web.UI.ActiveControls.TActiveFileUpload'); + +Prado::using('Application.components.FileUploadSecureMethods'); + +class SafeActiveFileUpload extends TActiveFileUpload { + + use FileUploadSecureMethods; + +} + +?> diff --git a/app/frontend/components/SafeFileUpload.php b/app/frontend/components/SafeFileUpload.php new file mode 100644 index 0000000..a8cbcae --- /dev/null +++ b/app/frontend/components/SafeFileUpload.php @@ -0,0 +1,11 @@ +<?php + +Prado::using('Application.components.FileUploadSecureMethods'); + +class SafeFileUpload extends TFileUpload { + + use FileUploadSecureMethods; + +} + +?> diff --git a/app/frontend/controls/AddToFilter.php b/app/frontend/controls/AddToFilter.php new file mode 100644 index 0000000..9146f3b --- /dev/null +++ b/app/frontend/controls/AddToFilter.php @@ -0,0 +1,41 @@ +<?php + +Prado::using('System.Web.UI.ActiveControls.TActiveCheckBox'); + +class AddToFilter extends UrlBasedCalendarControl { + + public function setDescription($val) { + $this->setViewState('Description', TPropertyValue::ensureString($val)); + } + + public function getDescription() { + return $this->getViewState('Description'); + } + + public function setUserPreference($sender, $param) { + $user = $this->getUserToManage(); + if ($user && !$user->IsGuest) { + if ($sender->Checked) { + $this->getFacade()->addToPreference($user, $this->getCalendar()->ID); + } else { + $this->getFacade()->removeFromPreference($user, $this->getCalendar()->ID); + } + $this->Page->CallbackClient->jQuery($this->Box, 'removeAttr', 'disabled'); + } + } + + public function getUserToManage() { + return $this->getControlState('user'); + } + + public function setUserToManage($user) { + $this->setControlState('user', $user); + } + + public function getPradoScriptDependencies() { + return ['jquery']; + } + +} + +?> diff --git a/app/frontend/controls/AddToFilter.tpl b/app/frontend/controls/AddToFilter.tpl new file mode 100644 index 0000000..a202aa7 --- /dev/null +++ b/app/frontend/controls/AddToFilter.tpl @@ -0,0 +1,8 @@ +<com:TActiveCheckBox ID="Box" OnCheckedChanged="setUserPreference" CssClass="addToFilterBox"> + <prop:Enabled><%= !$this->UserToManage->IsGuest %></prop:Enabled> + <prop:Checked><%= $this->Facade->isCalendarPreferred($this->UserToManage, $this->getCalendar()->ID) %></prop:Checked> + <prop:ToolTip><%= $this->UserToManage->IsGuest ? Prado::localize('log in to manage your selections') : '' %></prop:ToolTip> +</com:TActiveCheckBox> +<com:TLabel ForControl="Box"> + <prop:Text><%= $this->getDescription() %></prop:Text> +</com:TLabel> diff --git a/app/frontend/controls/CalendarDetails.php b/app/frontend/controls/CalendarDetails.php new file mode 100644 index 0000000..95ee563 --- /dev/null +++ b/app/frontend/controls/CalendarDetails.php @@ -0,0 +1,7 @@ +<?php + +class CalendarDetails extends UrlBasedCalendarControl { + +} + +?> diff --git a/app/frontend/controls/CalendarDetails.tpl b/app/frontend/controls/CalendarDetails.tpl new file mode 100644 index 0000000..2fd755c --- /dev/null +++ b/app/frontend/controls/CalendarDetails.tpl @@ -0,0 +1,13 @@ +<com:THeader2> + <%= $this->getCalendar()->Name %> +</com:THeader2> +<com:TImage> + <prop:ImageUrl><%= $this->getCalendar()->Image %></prop:ImageUrl> +</com:TImage> +<com:THyperLink Target="_blank"> + <prop:Text><%[ Source website ]%></prop:Text> + <prop:NavigateUrl><%= $this->getCalendar()->Website %></prop:NavigateUrl> +</com:THyperLink> +<p> + <%[ Last updated: ]%> <%= $this->getCalendar()->LastUpdated %> +</p> diff --git a/app/frontend/controls/CalendarGrid.php b/app/frontend/controls/CalendarGrid.php new file mode 100644 index 0000000..4ebfacd --- /dev/null +++ b/app/frontend/controls/CalendarGrid.php @@ -0,0 +1,59 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); +Prado::using('Application.facades.EventFacade'); +Prado::using('Application.user.DbUser'); + +class CalendarGrid extends FacadeTemplateControl { + + public function setMonth($month) { + $this->setControlState('Month', $month); + } + + public function getMonth() { + return $this->getControlState('Month'); + } + + public function setYear($year) { + $this->setControlState('Year', $year); + } + + public function getYear() { + return $this->getControlState('Year'); + } + + public function setUserToDisplay(DbUser $user) { + $this->setControlState('User', $user); + } + + public function getUserToDisplay() { + return $this->getControlState('User'); + } + + private function _getGrid() { + return $this->getFacade()->getCalendarListForUser( + $this->UserToDisplay, + $this->Month, + $this->Year + ); + } + + public function onPreRender($param) { + parent::onPreRender($param); + $this->Weeks->DataSource = $this->_getGrid()->Weeks; + $this->Weeks->dataBind(); + } + + public function weekDataBind($sender, $param) { + $param->Item->Days->DataSource = $param->Item->Data; + $param->Item->Days->dataBind(); + } + + public function dayDataBind($sender, $param) { + $param->Item->Events->DataSource = $param->Item->Data->Events; + $param->Item->Events->dataBind(); + } + +} + +?> diff --git a/app/frontend/controls/CalendarGrid.tpl b/app/frontend/controls/CalendarGrid.tpl new file mode 100644 index 0000000..2b3ada8 --- /dev/null +++ b/app/frontend/controls/CalendarGrid.tpl @@ -0,0 +1,28 @@ +<com:TRepeater ID="Weeks" OnItemDataBound="weekDataBind"> + <prop:ItemTemplate> + <div class="gridWeek"> + <com:TRepeater ID="Days" OnItemDataBound="SourceTemplateControl.dayDataBind"> + <prop:ItemTemplate> + <div class="gridDay"> + <%# $this->Data->Date %> + <com:TRepeater ID="Events"> + <prop:ItemTemplate> + <com:TConditional Condition="$this->Data"> + <prop:TrueTemplate> + <com:THtmlElement TagName="div"> + <prop:CssClass>gridEvent <%# $this->Parent->Parent->Data->Date == $this->Data->DateFrom ? 'beginDate' : '' %> <%# $this->Parent->Parent->Data->Date == $this->Data->DateTo ? 'endDate' : '' %></prop:CssClass> + <%# $this->Data->Name %> + </com:THtmlElement> + </prop:TrueTemplate> + <prop:FalseTemplate> + <div class="gridItem"> </div> + </prop:FalseTemplate> + </com:TConditional> + </prop:ItemTemplate> + </com:TRepeater> + </div> + </prop:ItemTemplate> + </com:TRepeater> + </div> + </prop:ItemTemplate> +</com:TRepeater> diff --git a/app/frontend/controls/CalendarGroupFilter.php b/app/frontend/controls/CalendarGroupFilter.php new file mode 100644 index 0000000..6f19bcd --- /dev/null +++ b/app/frontend/controls/CalendarGroupFilter.php @@ -0,0 +1,21 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); + +Prado::using('Application.facades.CalendarFacade'); + +class CalendarGroupFilter extends FacadeTemplateControl { + + public function onPreRender($param) { + parent::onPreRender($param); + $this->Categories->DataSource = $this->Facade->getCategories(); + $this->Categories->dataBind(); + } + + public function getPradoScriptDependencies() { + return ['jquery']; + } + +} + +?> diff --git a/app/frontend/controls/CalendarGroupFilter.tpl b/app/frontend/controls/CalendarGroupFilter.tpl new file mode 100644 index 0000000..ac82465 --- /dev/null +++ b/app/frontend/controls/CalendarGroupFilter.tpl @@ -0,0 +1,16 @@ +<com:TRepeater ID="Categories"> + <prop:HeaderTemplate> + <div class="selectAllGroups"> + <%[ All ]%> + <com:TCheckBox CssClass="box" Checked="True" /> + </div> + </prop:HeaderTemplate> + <prop:ItemTemplate> + <div class="selectGroup"> + <%# $this->Data->Name %> + <com:TCheckBox CssClass="box" Checked="True"> + <prop:Value><%# $this->Data->ID %></prop:Value> + </com:TCheckBox> + </div> + </prop:ItemTemplate> +</com:TRepeater> diff --git a/app/frontend/controls/CalendarLabel.php b/app/frontend/controls/CalendarLabel.php new file mode 100644 index 0000000..667e847 --- /dev/null +++ b/app/frontend/controls/CalendarLabel.php @@ -0,0 +1,13 @@ +<?php + +Prado::using('Application.controls.UrlBasedCalendarControl'); + +class CalendarLabel extends UrlBasedCalendarControl { + + public function getPradoScriptDependencies() { + return ['jquery']; + } + +} + +?> diff --git a/app/frontend/controls/CalendarLabel.tpl b/app/frontend/controls/CalendarLabel.tpl new file mode 100644 index 0000000..69e0147 --- /dev/null +++ b/app/frontend/controls/CalendarLabel.tpl @@ -0,0 +1,12 @@ +<com:TPanel CssClass="calendar"> + <prop:Attributes.data-group><%= $this->Calendar->GroupID %></prop:Attributes.data-group> + <com:THyperLink> + <prop:NavigateUrl><%= $this->Service->constructUrl('Calendar', ['calendar' => $this->Calendar->Url]) %></prop:NavigateUrl> + <prop:Text><%= $this->Calendar->Name %></prop:Text> + </com:THyperLink> + <com:AddToFilter> + <prop:Facade><%= $this->Facade %></prop:Facade> + <prop:CalendarUrl><%= $this->Calendar->Url %></prop:CalendarUrl> + <prop:UserToManage><%= $this->User %></prop:UserToManage> + </com:AddToFilter> +</com:TPanel> diff --git a/app/frontend/controls/CalendarScaffold.php b/app/frontend/controls/CalendarScaffold.php new file mode 100644 index 0000000..7f15a32 --- /dev/null +++ b/app/frontend/controls/CalendarScaffold.php @@ -0,0 +1,142 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); + +Prado::using('System.Web.UI.ActiveControls.TActiveDataGrid'); +Prado::using('System.Web.UI.ActiveControls.TActiveTextBox'); +Prado::using('Application.components.SafeActiveFileUpload'); + +Prado::using('Application.facades.CalendarFacade'); + +class CalendarScaffold extends FacadeTemplateControl { + + public function onPreRender($param) { + parent::onPreRender($param); + if (!$this->Page->IsPostBack && !$this->Page->IsCallBack) { + $this->_rebindData(); + } + } + + private function _rebindCalendars(array $calendars) { + $this->Calendars->DataSource = $calendars; + $this->Calendars->dataBind(); + } + + private function _rebindCategoryList(array $categories) { + foreach ($this->Calendars->Columns as $column) { + if ($column->ID === 'Category' + && $column instanceof TActiveDropDownListColumn) { + $column->ListDataSource = $categories; + } + } + } + + private function _rebindData(bool $refresh = FALSE) { + $this->_rebindCategoryList( + $this->_getCategories() + ); + $this->_rebindCalendars( + $this->_getCalendars($refresh) + ); + } + + private function _getCalendars(bool $refresh = FALSE) { + if ($refresh) { + $this->clearViewState('Calendars'); + } + $calendars = $this->getViewState( + 'Calendars', + $this->getFacade()->getAll() + ); + $this->setViewState('Calendars', $calendars); + return $calendars; + } + + private function _getCategories() { + $categories = $this->getViewState( + 'Categories', + $this->getFacade()->getCategories() + ); + $this->setViewState('Categories', $categories); + return $categories; + } + + public function editRow($sender, $param) { + $this->Calendars->EditItemIndex = $param->Item->ItemIndex; + $this->_rebindData(); + } + + private function _compileSaveData(TDataGridItem $item) { + return [ + 'CategoryID' => $item->Category->DropDownList->SelectedValue, + 'Visible' => $item->Visible->CheckBox->Checked, + 'CustomName' => $item->CustomName->TextBox->SafeText, + 'CustomUrl' => $item->CustomUrl->TextBox->SafeText, + 'CustomImage' => $item->CustomImage->Value->SafeText + ]; + } + + public function saveRow($sender, $param) { + $calendar = $this->getFacade()->get( + $sender->DataKeys[$param->Item->ItemIndex] + ); + if ($calendar) { + foreach ($calendar as $c) { + $c->saveData($this->_compileSaveData($param->Item)); + } + } else { + throw new TInvalidDataValueException( + Prado::localize('Calendar not found') + ); + } + $this->Calendars->EditItemIndex = -1; + $this->_rebindData(TRUE); + } + + public function cancelRowEdit($sender, $param) { + $this->Calendars->EditItemIndex = -1; + $this->_rebindData(); + } + + public function toggleDefaultState($sender, $param) { + $calendar = $this->getFacade()->get($sender->CustomData); + if ($calendar) { + $calendar[0]->Visible = $sender->Checked; + $calendar[0]->save(); + $this->_rebindData(TRUE); + } + } + + public function uploadRowFile($sender, $param) { + $fileType = $sender->getFileType(); + if (preg_match('/^image\//', $fileType)) { + $calendar = $this->getFacade()->get($sender->CustomData); + if ($calendar) { + $targetFile = $calendar[0]->getCustomImagePath( + $sender->getLocalName(), + $fileType + ); + if ($sender->saveAs($targetFile)) { + $sender->NamingContainer->CustomImage->Value->Text = basename( + $targetFile + ); + } + } else { + throw new TInvalidDataValueException( + Prado::localize('Calendar not found') + ); + } + } else { + throw new TInvalidDataTypeException( + Prado::localize('Invalid file type') + ); + } + } + + protected function getPradoScriptDependencies() { + return ['jquery']; + } + +} + +?> diff --git a/app/frontend/controls/CalendarScaffold.tpl b/app/frontend/controls/CalendarScaffold.tpl new file mode 100644 index 0000000..6a22bc2 --- /dev/null +++ b/app/frontend/controls/CalendarScaffold.tpl @@ -0,0 +1,81 @@ +<com:TPanel + CssClass="calendarScaffold"> + <com:TActiveDataGrid ID="Calendars" + DataKeyField="UID" + AutoGenerateColumns="false" + OnEditCommand="editRow" + OnCancelCommand="cancelRowEdit" + OnUpdateCommand="saveRow"> + <com:TActiveBoundColumn ID="Name" + ReadOnly="true" + DataField="Name"> + <prop:HeaderText><%[ Calendar ]%></prop:HeaderText> + </com:TActiveBoundColumn> + <com:TActiveHyperLinkColumn ID="Website" + Target="_blank" + DataNavigateUrlField="Website"> + <prop:HeaderText><%[ WWW ]%></prop:HeaderText> + <prop:Text><%[ [www] ]%></prop:Text> + </com:TActiveHyperLinkColumn> + <com:TActiveHyperLinkColumn ID="Url" + Target="_blank" + DataNavigateUrlField="Url"> + <prop:HeaderText><%[ ICS ]%></prop:HeaderText> + <prop:Text><%[ [ics] ]%></prop:Text> + </com:TActiveHyperLinkColumn> + <com:TActiveDropDownListColumn ID="Category" + DataTextField="Category.Name" + DataValueField="CategoryID" + ListValueField="ID" + ListTextField="Name"> + <prop:HeaderText><%[ Category ]%></prop:HeaderText> + </com:TActiveDropDownListColumn> + <com:TActiveTemplateColumn ID="Visible"> + <prop:HeaderText><%[ Default ]%></prop:HeaderText> + <prop:ItemTemplate> + <com:TActiveCheckBox + OnCheckedChanged="SourceTemplateControl.toggleDefaultState" + CssClass="visibilityToggle"> + <prop:Checked><%# $this->Parent->Data->Visible %></prop:Checked> + <prop:CustomData><%# $this->Parent->Data->UID %></prop:CustomData> + </com:TActiveCheckBox> + </prop:ItemTemplate> + <prop:EditItemTemplate> + <com:TCheckBox ID="CheckBox"> + <prop:Checked><%# $this->Parent->Data->Visible %></prop:Checked> + </com:TCheckBox> + </prop:EditItemTemplate> + </com:TActiveTemplateColumn> + <com:TActiveBoundColumn ID="CustomName" + DataField="CustomName"> + <prop:HeaderText><%[ Custom name ]%></prop:HeaderText> + </com:TActiveBoundColumn> + <com:TActiveBoundColumn ID="CustomUrl" + DataField="CustomUrl"> + <prop:HeaderText><%[ URL ]%></prop:HeaderText> + </com:TActiveBoundColumn> + <com:TActiveTemplateColumn ID="CustomImage"> + <prop:HeaderText><%[ Image ]%></prop:HeaderText> + <prop:ItemTemplate> + <com:TImage> + <prop:ImageUrl><%# $this->Parent->Data->CustomImageUrl %></prop:ImageUrl> + </com:TImage> + </prop:ItemTemplate> + <prop:EditItemTemplate> + <com:TActiveTextBox ID="Value"> + <prop:Text><%# $this->Parent->Data->CustomImage %></prop:Text> + </com:TActiveTextBox><br /> + <com:SafeActiveFileUpload + OnFileUpload="SourceTemplateControl.uploadRowFile"> + <prop:CustomData><%# $this->Parent->Data->UID %></prop:CustomData> + </com:SafeActiveFileUpload> + </prop:EditItemTemplate> + </com:TActiveTemplateColumn> + <com:TActiveEditCommandColumn + HeaderText=""> + <prop:EditText><%[ Edit ]%></prop:EditText> + <prop:UpdateText><%[ Save ]%></prop:UpdateText> + <prop:CancelText><%[ Cancel ]%></prop:CancelText> + </com:TActiveEditCommandColumn> + </com:TActiveDataGrid> +</com:TPanel> diff --git a/app/frontend/controls/CalendarSelection.php b/app/frontend/controls/CalendarSelection.php new file mode 100644 index 0000000..e53aa36 --- /dev/null +++ b/app/frontend/controls/CalendarSelection.php @@ -0,0 +1,17 @@ +<?php + +Prado::using('Application.controls.FacadeTemplateControl'); + +class CalendarSelection extends FacadeTemplateControl { + + public function onPreRender($param) { + parent::onPreRender($param); + if (!$this->Page->IsCallBack) { + $this->Calendars->DataSource = $this->Facade->getAll(); + $this->Calendars->dataBind(); + } + } + +} + +?> diff --git a/app/frontend/controls/CalendarSelection.tpl b/app/frontend/controls/CalendarSelection.tpl new file mode 100644 index 0000000..d6bdd83 --- /dev/null +++ b/app/frontend/controls/CalendarSelection.tpl @@ -0,0 +1,8 @@ +<com:TRepeater ID="Calendars"> + <prop:ItemTemplate> + <com:CalendarLabel> + <prop:Facade><%# $this->SourceTemplateControl->Facade %></prop:Facade> + <prop:CalendarUrl><%# $this->Data->CustomUrl %></prop:CalendarUrl> + </com:CalendarLabel> + </prop:ItemTemplate> +</com:TRepeater> diff --git a/app/frontend/controls/EventList.php b/app/frontend/controls/EventList.php new file mode 100644 index 0000000..d40e000 --- /dev/null +++ b/app/frontend/controls/EventList.php @@ -0,0 +1,59 @@ +<?php + +class EventList extends UrlBasedCalendarControl { + + private function _setDate($key, $date) { + $datetime = new DateTime($date, new DateTimeZone('UTC')); + if (!$datetime) { + throw new TInvalidDataValueException( + Prado::localize('Invalid date string: {date}', + ['date' => $date]) + ); + } + $this->setViewState($key, $datetime); + } + + public function setDateFrom($date) { + $this->_setDate('DateFrom', $date); + } + + public function getDateFrom() { + return $this->getViewState('DateFrom'); + } + + public function setDateTo($date) { + $this->_setDate('DateTo', $date); + } + + public function getDateTo() { + return $this->getViewState('DateTo'); + } + + public function setHeaderText($text) { + $this->setViewState('HeaderText', TPropertyValue::ensureString($text)); + } + + public function getHeaderText() { + return $this->getViewState('HeaderText'); + } + + public function setReverse($value) { + $this->setViewState('Reverse', TPropertyValue::ensureBoolean($value)); + } + + public function getReverse() { + return $this->getViewState('Reverse'); + } + + public function getEvents() { + return $this->getFacade()->getEventsForTimeframe( + $this->getCalendar(), + $this->getDateFrom() ?: new DateTime('0000-00-00'), + $this->getDateTo() ?: new DateTime('9999-99-99'), + $this->getReverse() ? 'DESC' : 'ASC' + ); + } + +} + +?> diff --git a/app/frontend/controls/EventList.tpl b/app/frontend/controls/EventList.tpl new file mode 100644 index 0000000..5881ce7 --- /dev/null +++ b/app/frontend/controls/EventList.tpl @@ -0,0 +1,6 @@ +<com:THeader3> + <%= $this->getHeaderText() %> +</com:THeader3> +<com:EventRepeater CalendarLinkVisible="false"> + <prop:Events><%= $this->getEvents() %></prop:Events> +</com:EventRepeater> diff --git a/app/frontend/controls/EventRepeater.php b/app/frontend/controls/EventRepeater.php new file mode 100644 index 0000000..4fb2812 --- /dev/null +++ b/app/frontend/controls/EventRepeater.php @@ -0,0 +1,25 @@ +<?php + +Prado::using('Application.web.TemplateControl'); + +class EventRepeater extends TemplateControl { + + public function setEvents($events) { + $this->Events->DataSource = $events; + $this->Events->dataBind(); + } + + public function setCalendarLinkVisible($value) { + $this->setViewState( + 'CalendarLinkVisible', + TPropertyValue::ensureBoolean($value) + ); + } + + public function getCalendarLinkVisible() { + return $this->getViewState('CalendarLinkVisible', TRUE); + } + +} + +?> diff --git a/app/frontend/controls/EventRepeater.tpl b/app/frontend/controls/EventRepeater.tpl new file mode 100644 index 0000000..69aaabf --- /dev/null +++ b/app/frontend/controls/EventRepeater.tpl @@ -0,0 +1,12 @@ +<com:TRepeater ID="Events"> + <prop:ItemTemplate> + <%# $this->Data->DateString %> + <%# $this->Data->Name %> + <com:THyperLink> + <prop:Visible><%# $this->SourceTemplateControl->CalendarLinkVisible %></prop:Visible> + <prop:Text>(<%# $this->Data->Calendar->Name %>)</prop:Text> + <prop:NavigateUrl><%# $this->Service->constructUrl('Calendar', ['calendar' => $this->Data->Calendar->Url]) %></prop:NavigateUrl> + </com:THyperLink> + <br /> + </prop:ItemTemplate> +</com:TRepeater> diff --git a/app/frontend/controls/HeaderMenu.php b/app/frontend/controls/HeaderMenu.php new file mode 100644 index 0000000..2488629 --- /dev/null +++ b/app/frontend/controls/HeaderMenu.php @@ -0,0 +1,26 @@ +<?php + +Prado::using('Application.web.TemplateControl'); + +Prado::using('System.Web.UI.ActiveControls.TActiveLinkButton'); + +class HeaderMenu extends TemplateControl { + + public function loginUser($sender, $param) { + $authModule = $this->Application->getModule('auth'); + $authModule->setReturnUrl($this->Request->RequestUri); + $this->Response->redirect( + $this->Service->ConstructUrl($authModule->LoginPage) + ); + } + + public function logoutUser($sender, $param) { + $this->Application->getModule('auth')->logout(); + $this->Response->redirect( + $this->Service->ConstructUrl(NULL) + ); + } + +} + +?> diff --git a/app/frontend/controls/HeaderMenu.tpl b/app/frontend/controls/HeaderMenu.tpl new file mode 100644 index 0000000..a760588 --- /dev/null +++ b/app/frontend/controls/HeaderMenu.tpl @@ -0,0 +1,36 @@ +<nav role="navigation"> + <com:TActiveLinkButton OnCommand="loginUser"> + <prop:Text><%[ Login ]%></prop:Text> + <prop:Visible><%= $this->User->IsGuest %></prop:Visible> + <prop:ClientSide.OnFailure>window.location.replace('<%= $this->Service->constructUrl('Login') %>')</prop:ClientSide.OnFailure> + <prop:ClientSide.OnException>window.location.replace('<%= $this->Service->constructUrl('Login') %>')</prop:ClientSide.OnException> + </com:TActiveLinkButton> + <com:THyperLink> + <prop:Text><%[ Profile ]%></prop:Text> + <prop:NavigateUrl><%= $this->Service->constructUrl('Profile') %></prop:NavigateUrl> + <prop:Visible><%= !$this->User->IsGuest %></prop:Visible> + </com:THyperLink> + <com:THyperLink> + <prop:Text><%[ Calendar list ]%></prop:Text> + <prop:NavigateUrl><%= $this->Service->constructUrl('Select') %></prop:NavigateUrl> + </com:THyperLink> + <com:TActiveLinkButton OnCommand="logoutUser"> + <com:TTranslate> + Logout ({name}) + <com:TTranslateParameter Key="name"><%= $this->User->Name %></com:TTranslateParameter> + </com:TTranslate> + <prop:Visible><%= !$this->User->IsGuest %></prop:Visible> + <prop:ClientSide.OnFailure>window.location.reload()</prop:ClientSide.OnFailure> + <prop:ClientSide.OnException>window.location.reload()</prop:ClientSide.OnException> + </com:TActiveLinkButton> + <com:THyperLink> + <prop:Text><%[ New user ]%></prop:Text> + <prop:NavigateUrl><%= $this->Service->constructUrl('Signup') %></prop:NavigateUrl> + <prop:Visible><%= $this->User->getIsAdmin() %></prop:Visible> + </com:THyperLink> + <com:THyperLink> + <prop:Text><%[ Admin calendars ]%></prop:Text> + <prop:NavigateUrl><%= $this->Service->constructUrl('Admin') %></prop:NavigateUrl> + <prop:Visible><%= $this->User->getIsAdmin() %></prop:Visible> + </com:THyperLink> +</nav> diff --git a/app/frontend/controls/LoginBox.php b/app/frontend/controls/LoginBox.php new file mode 100644 index 0000000..1136a79 --- /dev/null +++ b/app/frontend/controls/LoginBox.php @@ -0,0 +1,39 @@ +<?php + +Prado::using('Application.web.TemplateControl'); + +class LoginBox extends TemplateControl { + + public function onInit($param) { + parent::onInit($param); + if (!$this->Page->IsPostBack && !$this->User->IsGuest) { + $this->_afterLoginRedirect(); + } + } + + private function _afterLoginRedirect() { + $authModule = $this->Application->getModule('auth'); + $redirUrl = $authModule->ReturnUrl; + if (!$redirUrl + || $redirUrl == $this->Service->constructUrl($authModule->LoginPage)) { + $redirUrl = $this->Service->constructUrl(NULL); + } + $this->Response->redirect($redirUrl); + } + + public function loginUser($sender, $param) { + if ($this->Page->IsValid) { + $this->_afterLoginRedirect(); + } + } + + public function validatePassword($sender, $param) { + $param->IsValid = $this->Application->getModule('auth')->login( + $this->Login->Text, + $this->Password->Text + ); + } + +} + +?> diff --git a/app/frontend/controls/LoginBox.tpl b/app/frontend/controls/LoginBox.tpl new file mode 100644 index 0000000..070ab86 --- /dev/null +++ b/app/frontend/controls/LoginBox.tpl @@ -0,0 +1,33 @@ +<%[ Username: ]%> +<com:TTextBox ID="Login" + ValidationGroup="LoginGroup" /> +<com:TRequiredFieldValidator + ControlToValidate="Login" + Display="Dynamic" + ValidationGroup="LoginGroup"> + <prop:ErrorMessage><%[ Username cannot be empty ]%></prop:ErrorMessage> +</com:TRequiredFieldValidator> +<br /> +<%[ Password: ]%> +<com:TTextBox ID="Password" + TextMode="Password" + ValidationGroup="LoginGroup" /> +<com:TRequiredFieldValidator + ControlToValidate="Password" + Display="Dynamic" + ValidationGroup="LoginGroup"> + <prop:ErrorMessage><%[ Password cannot be empty ]%></prop:ErrorMessage> +</com:TRequiredFieldValidator> +<com:TCustomValidator + ControlToValidate="Password" + OnServerValidate="validatePassword" + Display="Dynamic" + ValidationGroup="LoginGroup"> + <prop:ErrorMessage><%[ Username and password don't match ]%></prop:ErrorMessage> +</com:TCustomValidator> +<br /> +<com:TButton + OnCommand="loginUser" + ValidationGroup="LoginGroup"> + <prop:Text><%[ Login ]%></prop:Text> +</com:TButton> diff --git a/app/frontend/controls/PasswordChange.php b/app/frontend/controls/PasswordChange.php new file mode 100644 index 0000000..45ce656 --- /dev/null +++ b/app/frontend/controls/PasswordChange.php @@ -0,0 +1,44 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); + +Prado::using('Application.user.DbUser'); +Prado::using('Application.facades.UserFacade'); + +class PasswordChange extends FacadeTemplateControl { + + public function getUserToChange() { + return $this->getControlState('user'); + } + + public function setUserToChange(DbUser $user) { + if ($user->IsGuest && !$this->Page->IsCallBack) { + throw new TInvalidDataValueException( + Prado::localize( + 'Password change impossible for guest user' + ) + ); + } + $this->setControlState('user', $user); + } + + public function checkPassword($sender, $param) { + $param->IsValid = $this->getFacade()->verifyUserPassword( + $this->Password->Text, $this->UserToChange + ); + } + + public function changePassword($sender, $param) { + $this->SuccessMessage->Visible = FALSE; + if ($this->Page->IsValid) { + $this->getFacade()->changePassword( + $this->UserToChange, + $this->NewPassword->Text + ); + $this->SuccessMessage->Visible = TRUE; + } + } + +} + +?> diff --git a/app/frontend/controls/PasswordChange.tpl b/app/frontend/controls/PasswordChange.tpl new file mode 100644 index 0000000..4eb9a8e --- /dev/null +++ b/app/frontend/controls/PasswordChange.tpl @@ -0,0 +1,59 @@ +<%[ Change password ]%><br /> +<%[ Current password: ]%> +<com:TTextBox ID="Password" + TextMode="Password" + ValidationGroup="ChangePasswordGroup" /> +<com:TRequiredFieldValidator + ControlToValidate="Password" + Display="Dynamic" + ValidationGroup="ChangePasswordGroup"> + <prop:ErrorMessage><%[ Current password cannot be empty ]%></prop:ErrorMessage> +</com:TRequiredFieldValidator> +<com:TCustomValidator + ControlToValidate="Password" + OnServerValidate="checkPassword" + Display="Dynamic" + ValidationGroup="ChangePasswordGroup"> + <prop:ErrorMessage><%[ Password is incorrect ]%></prop:ErrorMessage> +</com:TCustomValidator> +<br /> +<%[ New password: ]%> +<com:TTextBox ID="NewPassword" + TextMode="Password" + ValidationGroup="ChangePasswordGroup" /> +<com:TRequiredFieldValidator + ControlToValidate="NewPassword" + Display="Dynamic" + ValidationGroup="ChangePasswordGroup"> + <prop:ErrorMessage><%[ New password cannot be empty ]%></prop:ErrorMessage> +</com:TRequiredFieldValidator> +<br /> +<%[ Repeat password: ]%> +<com:TTextBox ID="ReNewPassword" + TextMode="Password" + ValidationGroup="ChangePasswordGroup" /> +<com:TRequiredFieldValidator + ControlToValidate="ReNewPassword" + Display="Dynamic" + ValidationGroup="ChangePasswordGroup"> + <prop:ErrorMessage><%[ New password cannot be empty ]%></prop:ErrorMessage> +</com:TRequiredFieldValidator> +<com:TCompareValidator + ControlToValidate="ReNewPassword" + ControlToCompare="NewPassword" + DataType="String" + Operator="Equal" + Display="Dynamic" + ValidationGroup="ChangePasswordGroup"> + <prop:ErrorMessage><%[ Passwords don't match ]%></prop:ErrorMessage> +</com:TCompareValidator> +<br /> +<com:TButton + OnCommand="changePassword" + ValidationGroup="ChangePasswordGroup"> + <prop:Text><%[ Change password ]%></prop:Text> +</com:TButton> +<com:TLabel ID="SuccessMessage" + Visible="false"> + <prop:Text><%[ Your password has been changed ]%></prop:Text> +</com:TLabel> diff --git a/app/frontend/controls/RegistrationForm.php b/app/frontend/controls/RegistrationForm.php new file mode 100644 index 0000000..46494e3 --- /dev/null +++ b/app/frontend/controls/RegistrationForm.php @@ -0,0 +1,28 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); + +Prado::using('Application.facades.UserFacade'); + +class RegistrationForm extends FacadeTemplateControl { + + public function checkUsername($sender, $param) { + $param->IsValid = $this->getFacade()->checkForUsername($this->Login->SafeText); + } + + public function registerUser($sender, $param) { + if ($this->Page->IsValid) { + $this->getFacade()->registerUser( + $this->Login->SafeText, + $this->Password->Text, + $this->Admin->Checked + ); + $this->Response->redirect( + $this->Service->constructUrl(NULL) + ); + } + } + +} + +?> diff --git a/app/frontend/controls/RegistrationForm.tpl b/app/frontend/controls/RegistrationForm.tpl new file mode 100644 index 0000000..9defe54 --- /dev/null +++ b/app/frontend/controls/RegistrationForm.tpl @@ -0,0 +1,66 @@ +<%[ Username: ]%> +<com:TTextBox ID="Login" + ValidationGroup="SignupGroup" /> +<com:TRequiredFieldValidator + ControlToValidate="Login" + Display="Dynamic" + ValidationGroup="SignupGroup"> + <prop:ErrorMessage><%[ Username cannot be empty ]%></prop:ErrorMessage> +</com:TRequiredFieldValidator> +<com:TRegularExpressionValidator + ControlToValidate="Login" + RegularExpression="[a-zA-Z0-9_]{6,255}" + Display="Dynamic" + ValidationGroup="SignupGroup"> + <prop:ErrorMessage><%[ Username must contain 6-255 characters, all Latin alphanumeric or underscore ]%></prop:ErrorMessage> +</com:TRegularExpressionValidator> +<com:TCustomValidator + ControlToValidate="Login" + OnServerValidate="checkUsername" + Display="Dynamic" + ValidationGroup="SignupGroup"> + <prop:ErrorMessage><%[ Username already exists ]%></prop:ErrorMessage> +</com:TCustomValidator> +<br /> +<%[ Password: ]%> +<com:TTextBox ID="Password" + TextMode="Password" + ValidationGroup="SignupGroup" /> +<com:TRequiredFieldValidator + ControlToValidate="Password" + Display="Dynamic" + ValidationGroup="SignupGroup"> + <prop:ErrorMessage><%[ Password cannot be empty ]%></prop:ErrorMessage> +</com:TRequiredFieldValidator> +<br /> +<%[ Repeat password: ]%> +<com:TTextBox ID="RePassword" + TextMode="Password" + ValidationGroup="SignupGroup" /> +<com:TRequiredFieldValidator + ControlToValidate="RePassword" + Display="Dynamic" + ValidationGroup="SignupGroup"> + <prop:ErrorMessage><%[ Password cannot be empty ]%></prop:ErrorMessage> +</com:TRequiredFieldValidator> +<com:TCompareValidator + ControlToValidate="RePassword" + ControlToCompare="Password" + DataType="String" + Operator="Equal" + Display="Dynamic" + ValidationGroup="SignupGroup"> + <prop:ErrorMessage><%[ Passwords don't match ]%></prop:ErrorMessage> +</com:TCompareValidator> +<br /> +<%[ Admin: ]%> +<com:TCheckBox ID="Admin" + ValidationGroup="SignupGroup" /> +<br /> +<com:TButton + OnCommand="registerUser" + ValidationGroup="SignupGroup"> + <prop:Text><%[ Create ]%></prop:Text> +</com:TButton> +<com:TValidationSummary + ValidationGroup="SignupGroup" /> diff --git a/app/frontend/controls/TimezoneSelect.php b/app/frontend/controls/TimezoneSelect.php new file mode 100644 index 0000000..25af453 --- /dev/null +++ b/app/frontend/controls/TimezoneSelect.php @@ -0,0 +1,58 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); + +Prado::using('Application.user.DbUser'); +Prado::using('Application.facades.UserFacade'); + +Prado::using('Application.dto.TimezoneDTO'); + +class TimezoneSelect extends FacadeTemplateControl { + + public function getUserToChange() { + return $this->getControlState('user'); + } + + public function setUserToChange(DbUser $user) { + if ($user->IsGuest && !$this->Page->IsCallBack) { + throw new TInvalidDataValueException( + Prado::localize( + 'Timezone preference change impossible for guest user' + ) + ); + } + $this->setControlState('user', $user); + } + + public function onPreRender($param) { + parent::onPreRender($param); + $this->Timezones->DataSource = $this->_getTimezones(); + $this->Timezones->DataValueField = 'Name'; + $this->Timezones->DataTextField = 'Label'; + $this->Timezones->dataBind(); + $this->Timezones->setSelectedValue( + $this->getFacade()->getTimezonePreference($this->UserToChange)->Name + ); + } + + public function saveTimezone($sender, $param) { + $this->getFacade()->setTimezonePreference( + $this->UserToChange, + $this->Timezones->SelectedValue + ); + } + + private function _getTimezones() { + $timezones = array_map( + function($tz) { + return new TimezoneDTO($tz); + }, + DateTimeZone::listIdentifiers() + ); + usort($timezones, ['TimezoneDTO', '__compare']); + return $timezones; + } + +} + +?> diff --git a/app/frontend/controls/TimezoneSelect.tpl b/app/frontend/controls/TimezoneSelect.tpl new file mode 100644 index 0000000..ee703d6 --- /dev/null +++ b/app/frontend/controls/TimezoneSelect.tpl @@ -0,0 +1,5 @@ +<com:TDropDownList ID="Timezones" /> +<com:TButton + OnCommand="saveTimezone"> + <prop:Text><%[ Save timezone ]%></prop:Text> +</com:TButton> diff --git a/app/frontend/controls/UpcomingEvents.php b/app/frontend/controls/UpcomingEvents.php new file mode 100644 index 0000000..27fc723 --- /dev/null +++ b/app/frontend/controls/UpcomingEvents.php @@ -0,0 +1,33 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); +Prado::using('Application.user.DbUser'); +Prado::using('Application.facades.EventFacade'); + +class UpcomingEvents extends FacadeTemplateControl { + + public function getUserToDisplay() { + return $this->getControlState('user'); + } + + public function setUserToDisplay(DbUser $user) { + $this->setControlState('user', $user); + } + + public function getEvents() { + return $this->_getEventsForUser($this->UserToDisplay); + } + + private function _getEventsForUser(DbUser $user) { + $utc = new DateTimeZone('UTC'); + $dateFrom = new DateTime('now', $utc); + $dateTo = new DateTime('+7 days', $utc); + return $this->getFacade()->getTimeframeListForUser( + $user, + $dateFrom, $dateTo + ); + } + +} + +?> diff --git a/app/frontend/controls/UpcomingEvents.tpl b/app/frontend/controls/UpcomingEvents.tpl new file mode 100644 index 0000000..ac5f60c --- /dev/null +++ b/app/frontend/controls/UpcomingEvents.tpl @@ -0,0 +1,5 @@ +<%[ Upcoming events: ]%> +<br /> +<com:EventRepeater ID="Events"> + <prop:Events><%= $this->getEvents() %></prop:Events> +</com:EventRepeater> diff --git a/app/frontend/controls/UrlBasedCalendarControl.php b/app/frontend/controls/UrlBasedCalendarControl.php new file mode 100644 index 0000000..a5be82e --- /dev/null +++ b/app/frontend/controls/UrlBasedCalendarControl.php @@ -0,0 +1,40 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); +Prado::using('Application.facades.CalendarFacade'); + +class UrlBasedCalendarControl extends FacadeTemplateControl { + + public function setCalendarUrl(string $url = NULL) { + if ($url) { + $calendar = $this->getFacade()->resolveUrl($url); + if ($calendar) { + $this->setControlState('Calendar', $calendar); + return; + } + } + if ($this->getRaiseException()) { + throw new THttpException( + 404, + Prado::localize('Page not found') + ); + } else { + $this->Visible = FALSE; + } + } + + public function getCalendar() { + return $this->getControlState('Calendar'); + } + + public function setRaiseException($value) { + $this->setControlState('RaiseException', TPropertyValue::ensureBoolean($value)); + } + + public function getRaiseException() { + return $this->getControlState('RaiseException', FALSE); + } + +} + +?> diff --git a/app/frontend/controls/UserSelection.php b/app/frontend/controls/UserSelection.php new file mode 100644 index 0000000..233f0bf --- /dev/null +++ b/app/frontend/controls/UserSelection.php @@ -0,0 +1,45 @@ +<?php + +Prado::using('Application.web.FacadeTemplateControl'); +Prado::using('Application.user.DbUser'); +Prado::using('Application.facades.CalendarFacade'); + +class UserSelection extends FacadeTemplateControl { + + public function getUserToDisplay() { + return $this->getControlState('user'); + } + + public function setUserToDisplay(DbUser $user = NULL) { + $this->setControlState('user', $user); + } + + public function onPreRender($param) { + parent::onPreRender($param); + $this->Categories->setDataSource( + $this->_getUserSelection($this->UserToDisplay) + ); + $this->Categories->dataBind(); + } + + public function categoryDataBind($sender, $param) { + $param->Item->Calendars->setDataSource($param->Item->Data->Calendars); + $param->Item->Calendars->dataBind(); + } + + public function removeFromSelection($sender, $param) { + if (!$this->UserToDisplay->IsGuest) { + return $this->getFacade()->removeFromPreference( + $this->UserToDisplay, + $param->CommandParameter + ); + } + } + + private function _getUserSelection(DbUser $user) { + return $this->getFacade()->getPreferenceList($user); + } + +} + +?> diff --git a/app/frontend/controls/UserSelection.tpl b/app/frontend/controls/UserSelection.tpl new file mode 100644 index 0000000..8d20365 --- /dev/null +++ b/app/frontend/controls/UserSelection.tpl @@ -0,0 +1,29 @@ +<%[ Selected calendars: ]%> +<br /> +<com:TRepeater ID="Categories" OnItemDataBound="categoryDataBind"> + <prop:ItemTemplate> + <%# $this->Data->Name %><br /> + <com:TRepeater ID="Calendars"> + <prop:ItemTemplate> + <com:TLinkButton + Text="[X]" + OnCommand="SourceTemplateControl.removeFromSelection"> + <prop:CommandParameter><%# $this->Data->ID %></prop:CommandParameter> + <prop:Visible><%# !$this->SourceTemplateControl->UserToDisplay->IsGuest %></prop:Visible> + </com:TLinkButton> + <%# $this->Data->Name %> + <com:THyperLink> + <prop:Text><%[ (details) ]%></prop:Text> + <prop:NavigateUrl><%# $this->Service->constructUrl('Calendar', ['calendar' => $this->Data->Url]) %></prop:NavigateUrl> + </com:THyperLink> + <com:THyperLink + Text="(www)" + Target="_blank"> + <prop:NavigateUrl><%# $this->Data->Website %></prop:NavigateUrl> + </com:THyperLink> + <br /> + </prop:ItemTemplate> + </com:TRepeater> + <br /> + </prop:ItemTemplate> +</com:TRepeater> diff --git a/app/frontend/controls/config.xml b/app/frontend/controls/config.xml new file mode 100644 index 0000000..61d7e5b --- /dev/null +++ b/app/frontend/controls/config.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <paths> + <using namespace="Application.controls.*" /> + </paths> +</configuration> diff --git a/app/frontend/controls/scripts/AddToFilter.js b/app/frontend/controls/scripts/AddToFilter.js new file mode 100644 index 0000000..e6d7b39 --- /dev/null +++ b/app/frontend/controls/scripts/AddToFilter.js @@ -0,0 +1,5 @@ +$(document).on('ready', function() { + $('input.addToFilterBox').on('change', function() { + $(this).attr('disabled', true); + }); +}); diff --git a/app/frontend/controls/scripts/CalendarGroupFilter.js b/app/frontend/controls/scripts/CalendarGroupFilter.js new file mode 100644 index 0000000..e6c1d73 --- /dev/null +++ b/app/frontend/controls/scripts/CalendarGroupFilter.js @@ -0,0 +1,29 @@ +$(document).on('ready', function() { + var selectBoxes = $('.selectGroup input.box'); + var selectAllBox = $('.selectAllGroups input.box'); + selectBoxes.on('change', function() { + var groups = []; + var allSelected = true; + $('.selectGroup input.box').each(function() { + var box = $(this); + if (box.is(':checked')) { + groups.push(box.val()); + } else { + allSelected = false; + } + }); + if (allSelected) { + selectAllBox.prop('checked', true); + } else { + selectAllBox.removeAttr('checked'); + } + $(document).trigger('Application.calendarGroupFilterChanged', {groups: groups}); + }); + selectAllBox.on('change', function() { + if ($(this).is(':checked')) { + selectBoxes.prop('checked', true).trigger('change'); + } else { + selectBoxes.removeAttr('checked').trigger('change'); + } + }); +}); diff --git a/app/frontend/controls/scripts/CalendarLabel.js b/app/frontend/controls/scripts/CalendarLabel.js new file mode 100644 index 0000000..8193e56 --- /dev/null +++ b/app/frontend/controls/scripts/CalendarLabel.js @@ -0,0 +1,11 @@ +$(document).on('Application.calendarGroupFilterChanged', function(event, args) { + var selectedGroups = args.groups || []; + $('.calendar').each(function() { + var label = $(this); + if (selectedGroups.indexOf(label.attr('data-group')) >= 0) { + label.show(); + } else { + label.hide(); + } + }); +}); diff --git a/app/frontend/controls/scripts/CalendarScaffold.js b/app/frontend/controls/scripts/CalendarScaffold.js new file mode 100644 index 0000000..d4b8ec5 --- /dev/null +++ b/app/frontend/controls/scripts/CalendarScaffold.js @@ -0,0 +1,8 @@ +$('body').on( + 'click', + 'main .calendarScaffold tbody a[href^="javascript:;//"], main .calendarScaffold tbody input.visibilityToggle', + function(e) { + var loader = $('<div>').addClass('calendarScaffoldLoader'); + $('main .calendarScaffold div[id$="_Container"]').append(loader); + } +); diff --git a/app/frontend/controls/styles/CalendarGrid.css b/app/frontend/controls/styles/CalendarGrid.css new file mode 100644 index 0000000..4710993 --- /dev/null +++ b/app/frontend/controls/styles/CalendarGrid.css @@ -0,0 +1,16 @@ +div.gridWeek { + clear: both; + display: flex; + flex-flow: row nowrap; +} +div.gridDay { + width: 14%; + min-height: 8em; + flex: 1 1 auto; +} +div.gridEvent, div.gridItem { height: 1.5em; padding: 0.3em 0.5em; margin: 0.1em 0; } +div.gridEvent { overflow: hidden; white-space: nowrap; background: #ddd } +div.gridEvent.beginDate { border-top-left-radius: 1.5em; + border-bottom-left-radius: 1.5em } +div.gridEvent.endDate { border-top-right-radius: 1.5em; + border-bottom-right-radius: 1.5em } diff --git a/app/frontend/controls/styles/CalendarScaffold.css b/app/frontend/controls/styles/CalendarScaffold.css new file mode 100644 index 0000000..071f672 --- /dev/null +++ b/app/frontend/controls/styles/CalendarScaffold.css @@ -0,0 +1,11 @@ +.calendarScaffold { + position: relative; +} +.calendarScaffold .calendarScaffoldLoader { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: url(preloader.gif) no-repeat center 5% rgba(255,255,255,0.8); +} diff --git a/app/frontend/db/ActiveRecord.php b/app/frontend/db/ActiveRecord.php new file mode 100644 index 0000000..1176767 --- /dev/null +++ b/app/frontend/db/ActiveRecord.php @@ -0,0 +1,69 @@ +<?php + +class ActiveRecord extends TActiveRecord { + + private function _getMappedPropertyName($name) { + if (isset(static::$COLUMN_MAPPING[$name])) { + return static::$COLUMN_MAPPING[$name]; + } + return $name; + } + + const DYNAMIC_METHODS = [ + 'findby', + 'findallby', + 'deleteby', + 'deleteallby' + ]; + + private function _getMappedMethodName($method) { + if (static::$COLUMN_MAPPING) { + $methodParts = []; + if (preg_match('/^(' . implode('|', self::DYNAMIC_METHODS) . ')(.*)$/i', $method, $methodParts)) { + $methodParameters = []; + $columnString = implode( + '|', + array_merge( + array_keys(static::$COLUMN_MAPPING), + array_values(static::$COLUMN_MAPPING) + ) + ); + $parameterRegex = '/(' . $columnString . ')(and|_and_|or|_or_)?/i'; + $method = $methodParts[1]; + if (preg_match_all($parameterRegex, $methodParts[2], $methodParameters, PREG_SET_ORDER)) { + foreach ($methodParameters as $parameter) { + $mappedColumn = array_search($parameter[1], static::$COLUMN_MAPPING); + $method .= ($mappedColumn !== FALSE) ? $mappedColumn : $parameter[1]; + if (count($parameter) > 2) { + $method .= $parameter[2]; + } + } + } + } + } + return $method; + } + + public function __get($name) { + $name = $this->_getMappedPropertyName($name); + if (property_exists($this, $name)) { + return $this->$name; + } + return parent::__get($name); + } + + public function __set($name, $value) { + $name = $this->_getMappedPropertyName($name); + if (property_exists($this, $name)) { + return $this->$name = $value; + } + return parent::__set($name, $value); + } + + public function __call($method, $args) { + return parent::__call($this->_getMappedMethodName($method), $args); + } + +} + +?> diff --git a/app/frontend/db/DBConnection.php b/app/frontend/db/DBConnection.php new file mode 100644 index 0000000..92ab0fb --- /dev/null +++ b/app/frontend/db/DBConnection.php @@ -0,0 +1,28 @@ +<?php + +Prado::using('Application.db.DBTransaction'); +Prado::using('System.Data.TDbConnection'); + +class DBConnection extends TDbConnection { + + private $_transaction = NULL; + public function getCurrentTransaction() { + if (!$this->_transaction->getActive()) { + $this->_transaction = NULL; + } + return $this->_transaction; + } + + public function beginTransaction() { + if ($this->_transaction && $this->_transaction->getActive()) { + $this->_transaction->beginNestedTransaction(); + } + else { + $this->_transaction = parent::beginTransaction(); + } + return $this->_transaction; + } + +} + +?> diff --git a/app/frontend/db/DBModule.php b/app/frontend/db/DBModule.php new file mode 100644 index 0000000..462b6f6 --- /dev/null +++ b/app/frontend/db/DBModule.php @@ -0,0 +1,40 @@ +<?php + +Prado::using('System.Data.TDataSourceConfig'); + +class DBModule extends TDataSourceConfig { + + private $_config; + + public function init($xml) { + $newXML = new TXmlElement('module'); + foreach ($xml->getAttributes() as $attr => $val) { + $newXML->setAttribute($attr, $val); + } + $dbXML = new TXmlElement('database'); + $config = json_decode(file_get_contents( + Prado::getPathOfNamespace($this->_config, '.json') + )); + if (isset($config->cset)) { + $dbXML->setAttribute('Charset', $config->cset); + } + $dbXML->setAttribute('Username', $config->user); + $dbXML->setAttribute('Password', $config->pass); + $dbXML->setAttribute( + 'ConnectionString', + sprintf( + '%s:host=%s;dbname=%s', + $config->type, $config->host, $config->name + ) + ); + $newXML->Elements[] = $dbXML; + parent::init($newXML); + } + + public function setConfig($config) { + $this->_config = TPropertyValue::ensureString($config); + } + +} + +?> diff --git a/app/frontend/db/DBTransaction.php b/app/frontend/db/DBTransaction.php new file mode 100644 index 0000000..b176453 --- /dev/null +++ b/app/frontend/db/DBTransaction.php @@ -0,0 +1,53 @@ +<?php + +Prado::using('Application.db.DBConnection'); +Prado::using('System.Data.TDbTransaction'); + +class DBTransaction extends TDbTransaction { + + private $_nestedCount = 0; + private $_rolledBack = FALSE; + + public function beginNestedTransaction() { + if ($this->getActive()) { + $this->_nestedCount++; + } + } + + public function commit() { + if ($this->_rolledBack) { + $childTransaction = (bool)($this->_nestedCount); + $this->rollback(); + if (!$childTransaction) { + throw new TDbException('Nested transaction was rolled back, unable to commit.'); + } + } + else { + if ($this->_nestedCount) { + $this->_nestedCount--; + } + else { + parent::commit(); + } + } + } + + public function rollback() { + if (!$this->getActive()) { + $this->_nestedCount = 0; + return; + } + if ($this->_nestedCount) { + $this->_rolledBack = TRUE; + $this->_nestedCount--; + } + else { + parent::rollback(); + $this->_nestedCount = 0; + $this->_rolledBack = FALSE; + } + } + +} + +?> diff --git a/app/frontend/db/config.json b/app/frontend/db/config.json new file mode 120000 index 0000000..89a492f --- /dev/null +++ b/app/frontend/db/config.json @@ -0,0 +1 @@ +../../../config/db.json
\ No newline at end of file diff --git a/app/frontend/db/config.xml b/app/frontend/db/config.xml new file mode 100644 index 0000000..3210593 --- /dev/null +++ b/app/frontend/db/config.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="db" + class="Application.db.DBModule" + config="Application.db.config" + ConnectionClass="Application.db.DBConnection" + DbConnection.TransactionClass="Application.db.DBTransaction" /> + </modules> +</configuration> diff --git a/app/frontend/dto/CalendarDTO.php b/app/frontend/dto/CalendarDTO.php new file mode 100644 index 0000000..4468941 --- /dev/null +++ b/app/frontend/dto/CalendarDTO.php @@ -0,0 +1,31 @@ +<?php + +Prado::using('Application.model.Calendar'); + +class CalendarDTO { + + public $ID; + public $Name; + public $Website; + public $Image; + public $Url; + public $LastUpdated; + public $GroupID; + + public function loadRecord(Calendar $calendarRecord) { + $this->ID = $calendarRecord->UID; + $this->Name = $calendarRecord->CustomName ?: $calendarRecord->Name; + $this->Website = $calendarRecord->Website; + $this->Image = $calendarRecord->CustomImageUrl; + $this->Url = $calendarRecord->CustomUrl; + $this->LastUpdated = $calendarRecord->LastUpdated; + $this->GroupID = $calendarRecord->CategoryID; + } + + public static function __compare(CalendarDTO $cal1, CalendarDTO $cal2) { + return strcmp($cal1->Name, $cal2->Name); + } + +} + +?> diff --git a/app/frontend/dto/CalendarGridDTO.php b/app/frontend/dto/CalendarGridDTO.php new file mode 100644 index 0000000..f5d91a5 --- /dev/null +++ b/app/frontend/dto/CalendarGridDTO.php @@ -0,0 +1,84 @@ +<?php + +Prado::using('Application.dto.CalendarGridDayDTO'); +Prado::using('Application.dto.GridEventDTO'); + +class CalendarGridDTO { + + public $DateFrom; + public $DateTo; + public $Weeks = []; + + public function __construct($events, DateTime $dateFrom, DateTime $dateTo) { + $this->DateFrom = DateTimeImmutable::createFromMutable($dateFrom); + $this->DateTo = DateTimeImmutable::createFromMutable($dateTo); + $date = $this->DateFrom; + $days = []; + $previousDay = NULL; + while ($date <= $this->DateTo) { + $day = $this->_getGridDay($date, $events, $previousDay); + $days[] = $day; + $previousDay = $day; + $date = $date->modify('+1 day'); + } + $this->Weeks = array_chunk($days, 7); + } + + private function _getContinuedEventGridPositions( + CalendarGridDayDTO $day, + CalendarGridDayDTO $previousDay = NULL) { + $eventPositions = []; + if ($previousDay) { + foreach ($day->Events as $event) { + if (in_array($event, $previousDay->Events)) { + $eventPositions[] = $event->GridPosition; + } + } + } + return $eventPositions; + } + + private function _alignEvents(array $eventPositions, array $events) { + $previousCount = count($eventPositions); + foreach ($events as $event) { + if ($event->GridPosition === NULL) { + $event->GridPosition = min( + array_diff( + range(0, count($events) + $previousCount), + $eventPositions + ) + ); + $eventPositions[] = $event->GridPosition; + } + } + usort($events, ['GridEventDTO', '__compare']); + return $events; + } + + private function _fillEventGrid(array $events) { + $previousEvent = -1; + foreach ($events as $event) { + $eventStep = $event->GridPosition - $previousEvent; + if ($eventStep > 1) { + array_splice( + $events, $previousEvent + 1, 0, array_fill(0, $eventStep - 1, NULL) + ); + } + $previousEvent = $event->GridPosition; + } + return $events; + } + + private function _getGridDay(DateTimeImmutable $date, + array $events, + CalendarGridDayDTO $previousDay = NULL) { + $day = new CalendarGridDayDTO($date, $events); + $eventPositions = $this->_getContinuedEventGridPositions($day, $previousDay); + $day->Events = $this->_alignEvents($eventPositions, $day->Events); + $day->Events = $this->_fillEventGrid($day->Events); + return $day; + } + +} + +?> diff --git a/app/frontend/dto/CalendarGridDayDTO.php b/app/frontend/dto/CalendarGridDayDTO.php new file mode 100644 index 0000000..ba65eb9 --- /dev/null +++ b/app/frontend/dto/CalendarGridDayDTO.php @@ -0,0 +1,28 @@ +<?php + +Prado::using('Application.dto.EventDTO'); +Prado::using('Application.dto.GridEventDTO'); + +class CalendarGridDayDTO { + + public $Date; + public $Events; + + public function __construct(DateTimeImmutable $date, array $events) { + $this->Date = $date->format('Y-m-d'); + $this->Events = array_filter($events, [$this, '_checkEventDate']); + // initial sort (date and calendar name) + // events are going to be re-sorted after assigning grid priorities + usort($this->Events, ['EventDTO', '__compare']); + } + + private function _checkEventDate(GridEventDTO $event) { + if (!$this->Date) { + return FALSE; + } + return ($this->Date >= $event->DateFrom) && ($this->Date <= $event->DateTo); + } + +} + +?> diff --git a/app/frontend/dto/CalendarGroupDTO.php b/app/frontend/dto/CalendarGroupDTO.php new file mode 100644 index 0000000..7b64c6e --- /dev/null +++ b/app/frontend/dto/CalendarGroupDTO.php @@ -0,0 +1,44 @@ +<?php + +Prado::using('Application.model.Category'); +Prado::using('Application.dto.CalendarDTO'); + +class CalendarGroupDTO { + + public $Name; + public $ID; + public $Priority; + public $Calendars = []; + + public function loadRecord(Category $categoryRecord, array $calendars) { + $this->Name = $categoryRecord->Name; + $this->ID = $categoryRecord->ID; + $this->Priority = $categoryRecord->Priority; + $this->Calendars = array_map( + function($calendarRecord) { + $dto = new CalendarDTO(); + $dto->loadRecord($calendarRecord); + return $dto; + }, + array_filter( + $calendars, + function($calendarRecord) use($categoryRecord) { + return $categoryRecord->ID == $calendarRecord->CategoryID; + } + ) + ); + usort($this->Calendars, ['CalendarDTO', '__compare']); + } + + public static function __compare(CalendarGroupDTO $cat1, + CalendarGroupDTO $cat2) { + $cmp = ($cat1->Priority ?: PHP_MAX_INT) - ($cat2->Priority ?: PHP_MAX_INT); + if ($cmp !== 0) { + return $cmp; + } + return strcmp($cat1->Name, $cat2->Name); + } + +} + +?> diff --git a/app/frontend/dto/EventDTO.php b/app/frontend/dto/EventDTO.php new file mode 100644 index 0000000..8f5cdf5 --- /dev/null +++ b/app/frontend/dto/EventDTO.php @@ -0,0 +1,85 @@ +<?php + +Prado::using('Application.model.Entry'); +Prado::using('Application.dto.CalendarDTO'); +Prado::using('Application.facades.UserFacade'); + +class EventDTO { + + public $DateString; + public $Name; + public $Location; + public $Calendar; + + private $_utc; + private $_targetTZ; + + public function __construct(TimezoneDTO $tz = NULL) { + $this->_utc = new DateTimeZone('UTC'); + $this->_targetTZ = new DateTimeZone( + $tz + ? $tz->Name + : UserFacade::getInstance()->getTimezonePreference( + Prado::getApplication()->getUser() + )->Name + ); + } + + private $_beginDate; + protected function getBeginDate(Entry $event) { + if (!$this->_beginDate) { + $this->_beginDate = new DateTime($event->BeginDate, $this->_utc); + } + return $this->_beginDate; + } + + private $_endDate; + protected function getEndDate(Entry $event) { + if (!$this->_endDate) { + $this->_endDate = new DateTime($event->EndDate, $this->_utc); + if ($event->AllDay) { + $this->_endDate = $this->_endDate->modify('-1 day'); + } + } + return $this->_endDate; + } + + public function loadRecord(Entry $event, array $calendars) { + $this->Name = $event->Name; + $this->Location = $event->Location; + + if ($event->AllDay) { + $this->DateString = $this->getBeginDate($event)->format('Y-m-d'); + if ($this->getBeginDate($event) != $this->getEndDate($event)) { + $this->DateString .= sprintf( + ' - %s', + $this->getEndDate($event)->format('Y-m-d') + ); + } + } else { + $beginDate = $this->getBeginDate($event)->setTimezone($this->_targetTZ); + $this->DateString = $beginDate->format('Y-m-d H:i'); + } + + $calendars = array_filter( + $calendars, + function ($calendar) use($event) { + return $calendar->UID == $event->CalendarID; + } + ); + $this->Calendar = new CalendarDTO(); + $this->Calendar->loadRecord( + $calendars ? array_values($calendars)[0] : $event->Calendar + ); + } + + public static function __compare(EventDTO $ev1, EventDTO $ev2) { + if ($ev1->DateString === $ev2->DateString) { + return strcmp($ev1->Calendar->Name, $ev2->Calendar->Name); + } + return strcmp($ev1->DateString, $ev2->DateString); + } + +} + +?> diff --git a/app/frontend/dto/GridEventDTO.php b/app/frontend/dto/GridEventDTO.php new file mode 100644 index 0000000..0d2bb37 --- /dev/null +++ b/app/frontend/dto/GridEventDTO.php @@ -0,0 +1,28 @@ +<?php + +Prado::using('Application.dto.EventDTO'); + +class GridEventDTO extends EventDTO { + + public $DateFrom; + public $DateTo; + public $AllDay; + public $GridPosition; + + public function loadRecord(Entry $event, array $calendars) { + parent::loadRecord($event, $calendars); + $this->AllDay = TPropertyValue::ensureBoolean($event->AllDay); + $this->DateFrom = $this->getBeginDate($event)->format('Y-m-d'); + $this->DateTo = $this->getEndDate($event)->format('Y-m-d'); + } + + public static function __compare(EventDTO $ev1, EventDTO $ev2) { + if ($ev1->GridPosition === NULL || $ev2->GridPosition === NULL) { + return parent::__compare($ev1, $ev2); + } + return $ev1->GridPosition - $ev2->GridPosition; + } + +} + +?> diff --git a/app/frontend/dto/TimezoneDTO.php b/app/frontend/dto/TimezoneDTO.php new file mode 100644 index 0000000..e4078e6 --- /dev/null +++ b/app/frontend/dto/TimezoneDTO.php @@ -0,0 +1,46 @@ +<?php + +class TimezoneDTO { + + public $Label; + public $Name; + public $Offset; + public $OffsetHours; + public $OffsetMinutes; + public $Location; + public $FirstDayOfTheWeek; + + public function __construct(string $name) { + $tz = new DateTimeZone($name); + $this->Name = $tz->getName(); + $this->Offset = $tz->getOffset(new DateTime()); + $this->OffsetHours = $this->Offset / 3600; + $this->OffsetMinutes = $this->Offset % 3600 / 60; + $this->Location = $tz->getLocation()['country_code']; + $this->FirstDayOfTheWeek = $this->_getFirstDayOfTheWeek(); + $this->Label = sprintf('UTC%+03d:%02d %s', $this->OffsetHours, $this->OffsetMinutes, $this->Name); + } + + private function _getFirstDayOfTheWeek() { + $dayMapping = json_decode( + file_get_contents( + Prado::getPathOfNamespace('Application.dto.weekdays', '.json') + ), + TRUE + ); + if ($this->Location && isset($dayMapping[$this->Location])) { + return ucfirst($dayMapping[$this->Location]); + } + return ucfirst($dayMapping['001']); + } + + + + public static function __compare(TimezoneDTO $tz1, TimezoneDTO $tz2) { + $diff = $tz1->Offset - $tz2->Offset; + return ($diff == 0) ? strcmp($tz1->Name, $tz2->Name) : $diff; + } + +} + +?> diff --git a/app/frontend/dto/weekdays.json b/app/frontend/dto/weekdays.json new file mode 120000 index 0000000..325c801 --- /dev/null +++ b/app/frontend/dto/weekdays.json @@ -0,0 +1 @@ +../../../config/weekdays.json
\ No newline at end of file diff --git a/app/frontend/events/CalendarPreferenceEvents.php b/app/frontend/events/CalendarPreferenceEvents.php new file mode 100644 index 0000000..76fa071 --- /dev/null +++ b/app/frontend/events/CalendarPreferenceEvents.php @@ -0,0 +1,16 @@ +<?php + +Prado::using('Application.events.EventModule'); +Prado::using('Application.model.User'); +Prado::using('Application.facades.CalendarFacade'); + +class CalendarPreferenceEvents extends EventModule { + + public function onUserRegistered(User $user) { + $facade = CalendarFacade::getInstance(); + $facade->setPreferredCalendars($user, $facade->getDefaultPreference()); + } + +} + +?> diff --git a/app/frontend/events/EventModule.php b/app/frontend/events/EventModule.php new file mode 100644 index 0000000..6474523 --- /dev/null +++ b/app/frontend/events/EventModule.php @@ -0,0 +1,53 @@ +<?php + +class EventModule extends TModule { + + public function init($config) { + $reflection = new ReflectionClass($this); + foreach ($reflection->getMethods() as $method) { + if (is_a($method->class, get_class(), TRUE)) { + $eventPattern = []; + if (preg_match('/^on(.*)/', $method->name, $eventPattern)) { + $this->_registerEventHandler( + $eventPattern[1], + $method->getClosure($this) + ); + } + } + } + if (get_class() === get_called_class()) { + $directory = dirname(__FILE__); + foreach (scandir($directory) as $dirEntry) { + $classNameMatch = []; + if (preg_match('/(.*)\.php$/', $dirEntry, $classNameMatch)) { + $className = $classNameMatch[1]; + include_once($directory . DIRECTORY_SEPARATOR . $dirEntry); + if ($className != get_class() + && is_a($className, get_class(), TRUE)) { + $class = new $className(); + $class->init(NULL); + } + } + } + } + } + + protected static $_handlers = []; + private function _registerEventHandler($event, $handler) { + if (!isset(self::$_handlers[$event])) { + self::$_handlers[$event] = []; + } + self::$_handlers[$event][] = $handler; + } + + public function raiseApplicationEvent($event, ...$params) { + if (isset(self::$_handlers[$event])) { + foreach (self::$_handlers[$event] as $handler) { + call_user_func_array($handler, $params); + } + } + } + +} + +?> diff --git a/app/frontend/events/config.xml b/app/frontend/events/config.xml new file mode 100644 index 0000000..d1c1e3f --- /dev/null +++ b/app/frontend/events/config.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="events" class="Application.events.EventModule" /> + </modules> +</configuration> diff --git a/app/frontend/facades/CalendarFacade.php b/app/frontend/facades/CalendarFacade.php new file mode 100644 index 0000000..1f78594 --- /dev/null +++ b/app/frontend/facades/CalendarFacade.php @@ -0,0 +1,212 @@ +<?php + +Prado::using('Application.facades.Facade'); +Prado::using('Application.facades.EventFacade'); +Prado::using('Application.dto.CalendarDTO'); +Prado::using('Application.dto.CalendarGroupDTO'); +Prado::using('Application.dto.TimezoneDTO'); +Prado::using('Application.model.Calendar'); +Prado::using('Application.model.Category'); +Prado::using('Application.model.UserPreference'); +Prado::using('Application.user.DbUser'); + +class CalendarFacade extends Facade { + + private function _getCategoriesForCalendars(array $calendars) { + return Category::finder()->findAllByPks( + array_map( + function($calendar) { + return $calendar->CategoryID; + }, + $calendars + ) + ); + } + + private $_defaultPreference = NULL; + public function getDefaultPreference() { + if ($this->_defaultPreference === NULL) { + $this->_defaultPreference = Calendar::finder()->findAllByIsVisible(1); + } + return $this->_defaultPreference; + } + + public function getCalendarPreference(DbUser $user) { + if ($user->IsGuest) { + return $this->getDefaultPreference(); + } else { + return $user->DbRecord->Calendars; + } + } + + public function getPreferenceList(DbUser $user) { + $calendars = $this->getCalendarPreference($user); + if ($calendars) { + $categories = array_map( + function($category) use($calendars) { + $dto = new CalendarGroupDTO(); + $dto->loadRecord($category, $calendars); + return $dto; + }, + $this->_getCategoriesForCalendars($calendars) + ); + usort($categories, ['CalendarGroupDTO', '__compare']); + return $categories; + } + return []; + } + + public function isCalendarPreferred(DbUser $user, $calendarID) { + return in_array( + $calendarID, + array_map( + function($calendar) { + return $calendar->UID; + }, + $this->getCalendarPreference($user) + ) + ); + } + + public function addToPreference(DbUser $user, $calendarID) { + if (!$user->IsGuest) { + $calendar = Calendar::finder()->findByPk($calendarID); + if ($calendar) { + $this->setPreferredCalendar($user->DbRecord, $calendar); + } + } + } + + public function removeFromPreference(DbUser $user, $calendarID) { + if (!$user->IsGuest) { + $preferenceRecord = UserPreference::finder()->find( + '_user = ? AND _calendar = ?', + $user->DbRecord->ID, + $calendarID + ); + if ($preferenceRecord) { + $preferenceRecord->delete(); + } + } + } + + public function setPreferredCalendar(User $user, Calendar $calendar) { + $preference = new UserPreference(); + $preference->CalendarID = $calendar->UID; + $preference->UserID = $user->ID; + $preference->save(); + } + + public function setPreferredCalendars(User $user, array $calendars) { + //TODO: remove old preference, optionally + $transaction = $this->beginTransaction(); + try { + foreach ($calendars as $calendar) { + $this->setPreferredCalendar($user, $calendar); + } + $transaction->commit(); + } catch (Exception $e) { + $transaction->rollback(); + throw $e; + } + } + + public function getEventsForTimeframe(CalendarDTO $calendar, + DateTime $dateFrom, + DateTime $dateTo, + string $order = 'ASC') { + $calendar = Calendar::finder()->findAllByUID($calendar->ID); + if ($calendar) { + $events = EventFacade::getInstance()->getEventList( + $dateFrom->format('Y-m-d H:i:s'), + $dateTo->format('Y-m-d H:i:s'), + $calendar, + $order + ); + return array_map( + function($event) use($calendar) { + $dto = new EventDTO(); + $dto->loadRecord($event, $calendar); + return $dto; + }, + $events + ); + } + return []; + } + + public function getAll() { + $records = Calendar::finder()->withCategory()->findAll('ORDER BY name ASC'); + foreach ($records as $record) { + $this->_fillUrlCache($record); + } + return $records; + } + + public function getCategories() { + $categories = array_map( + function($record) { + $dto = new CalendarGroupDTO(); + $dto->loadRecord($record, []); + return $dto; + }, + Category::finder()->findAll() + ); + usort($categories, ['CalendarGroupDTO', '__compare']); + return $categories; + } + + public function get($uid) { + $records = Calendar::finder()->withCategory()->findAllByPks($uid); + foreach ($records as $record) { + $this->_fillUrlCache($record); + } + return $records; + } + + private $_urlCache = []; + private function _fillUrlCache(Calendar $record = NULL) { + if ($record && $record->CustomUrl + && !isset($this->_urlCache[$record->CustomUrl])) { + $dto = new CalendarDTO(); + if ($record) { + $dto->loadRecord($record); + } else { + $dto = NULL; + } + return $this->_urlCache[$record->CustomUrl] = $dto; + } + } + + public function resolveUrl(string $url = NULL) { + if ($url) { + if (isset($this->_urlCache[$url])) { + return $this->_urlCache[$url]; + } + $record = Calendar::finder()->findByCustomUrl($url); + if ($record) { + return $this->_fillUrlCache($record); + } + } + return NULL; + } + + public function getCalendarBoundaries($year, $month, TimezoneDTO $timezone) { + $firstDay = new DateTime(sprintf('%d-%02d', $year, $month), + new DateTimeZone($timezone->Name)); + $firstDayAfter = clone $firstDay; + $firstDayAfter->modify('last day of this month')->modify('+1 day'); + $firstDayOfTheWeek = $timezone->FirstDayOfTheWeek; + if ($firstDay->format('D') !== $firstDayOfTheWeek) { + $firstDay->modify('last ' . $firstDayOfTheWeek); + } + if ($firstDayAfter->format('D') !== $firstDayOfTheWeek) { + $firstDayAfter->modify('next ' . $firstDayOfTheWeek); + } + $firstDayAfter->modify('-1 day'); + return [$firstDay, $firstDayAfter]; + } + +} + +?> diff --git a/app/frontend/facades/EventFacade.php b/app/frontend/facades/EventFacade.php new file mode 100644 index 0000000..14f809d --- /dev/null +++ b/app/frontend/facades/EventFacade.php @@ -0,0 +1,115 @@ +<?php + +Prado::using('Application.facades.Facade'); +Prado::using('Application.dto.EventDTO'); +Prado::using('Application.dto.TimezoneDTO'); +Prado::using('Application.dto.GridEventDTO'); +Prado::using('Application.dto.CalendarGridDTO'); +Prado::using('Application.model.Calendar'); +Prado::using('Application.facades.CalendarFacade'); +Prado::using('Application.user.DbUser'); + +class EventFacade extends Facade { + + public function getEventList(string $dateFrom=NULL, string $dateTo=NULL, + array $calendars=NULL, string $order='ASC') { + $calendarClause = '1=1'; + if ($calendars) { + $calendarClause = sprintf( + '_calendar IN (%s)', + implode( + ',', + array_map( + function($calendar) { + return $this->quoteString($calendar->UID); + }, + $calendars + ) + ) + ); + } + return $this->fetchList( + 'getEvents', + [ + 'date_from' => $dateFrom ?: '0000-00-00 00:00:00', + 'date_to' => $dateTo ?: '9999-99-99', + 'calendar_clause' => $calendarClause, + 'order_clause' => $order + ] + ); + } + + private function _compileEventObjects(array $events, array $calendars, + TimezoneDTO $tz, + string $class = 'Application.dto.EventDTO') { + return array_map( + function($event) use($calendars, $class, $tz) { + $dto = Prado::createComponent($class, $tz); + $dto->loadRecord($event, $calendars); + return $dto; + }, + $events + ); + } + + public function getTimeframeListForUser( + DbUser $user, + DateTime $dateFrom, DateTime $dateTo, + string $returnClass = 'Application.dto.EventDTO') { + $calendars = CalendarFacade::getInstance()->getCalendarPreference($user); + if ($calendars) { + $events = $this->getEventList( + $dateFrom->format('Y-m-d H:i:s'), + $dateTo->format('Y-m-d H:i:s'), + $calendars + ); + $calendars = $this->_getCalendarsForEvents($events); + return $this->_compileEventObjects( + $events, $calendars, + UserFacade::getInstance()->getTimezonePreference($user), + $returnClass); + } + return []; + } + + public function getCalendarListForUser(DbUser $user, + $month, $year) { + if (!$year) { + $year = intval(date('Y')); + } + if (!$month) { + $month = intval(date('m')); + } + $timezone = $user + ? UserFacade::getInstance()->getTimezonePreference($user) + : new TimezoneDTO(date_default_timezone_get()); + $timeframe = CalendarFacade::getInstance()->getCalendarBoundaries( + $year, $month, $timezone + ); + return new CalendarGridDTO( + $this->getTimeframeListForUser( + $user, + $timeframe[0], $timeframe[1], + 'Application.dto.GridEventDTO' + ), + ...$timeframe + ); + } + + private function _getCalendarsForEvents(array $events) { + if ($events) { + return Calendar::finder()->findAllByPks( + array_map( + function($event) { + return $event->CalendarID; + }, + $events + ) + ); + } + return []; + } + +} + +?> diff --git a/app/frontend/facades/Facade.php b/app/frontend/facades/Facade.php new file mode 100644 index 0000000..346024a --- /dev/null +++ b/app/frontend/facades/Facade.php @@ -0,0 +1,62 @@ +<?php + +Prado::using('System.Data.SqlMap.TSqlMapGateway'); + +class Facade { + + protected static $_instances = []; + + protected $_sqlMap; + + protected function __construct() { + } + + public function __sleep() { + $this->_sqlMap = NULL; + return array(); + } + + public static function getInstance() { + $className = get_called_class(); + if (!isset(static::$_instances[$className])) { + static::$_instances[$className] = new static(); + } + return static::$_instances[$className]; + } + + protected function getClient() { + if (!$this->_sqlMap) { + $this->_sqlMap = Prado::getApplication()->getModule('sqlmap')->Client; + } + return $this->_sqlMap; + } + + protected function quoteString($string) { + return $this->getClient()->DbConnection->quoteString($string); + } + + protected function fetch($sqlMap, $params) { + return $this->getClient()->queryForObject($sqlMap, $params); + } + + protected function fetchList($sqlMap, $params) { + return $this->getClient()->queryForList($sqlMap, $params); + } + + protected function fetchMap($sqlMap, $params, $key, $value=NULL) { + return $this->getClient()->queryForMap($sqlMap, $params, $key, $value); + } + + protected function beginTransaction() { + return $this->getClient()->DbConnection->beginTransaction(); + } + + protected function raiseEvent($event, ...$params) { + return Prado::getApplication()->getModule('events')->raiseApplicationEvent( + $event, ...$params + ); + } + +} + +?> diff --git a/app/frontend/facades/UserFacade.php b/app/frontend/facades/UserFacade.php new file mode 100644 index 0000000..1604a70 --- /dev/null +++ b/app/frontend/facades/UserFacade.php @@ -0,0 +1,78 @@ +<?php + +Prado::using('Application.facades.Facade'); +Prado::using('Application.user.DbUser'); +Prado::using('Application.model.User'); +Prado::using('Application.dto.TimezoneDTO'); + +class UserFacade extends Facade { + + public function findByLogin(string $login) { + return User::finder()->findByLogin($login); + } + + public function checkForUsername(string $login) { + return !User::finder()->count('login = ?', $login); + } + + public function registerUser(string $login, string $password, bool $admin) { + $transaction = $this->beginTransaction(); + try { + $newUser = new User(); + $newUser->Login = $login; + $newUser->Password = $this->generatePassword($password); + $newUser->IsAdmin = $admin; + $newUser->save(); + $this->raiseEvent('UserRegistered', $newUser); + $transaction->commit(); + return $newUser; + } catch (Exception $e) { + $transaction->rollback(); + throw $e; + } + } + + public function changePassword(DbUser $user, string $pass) { + if (!$user->IsGuest) { + $user->DbRecord->Password = $this->generatePassword($pass); + $user->DbRecord->save(); + } + } + + public function verifyUserPassword(string $password, DbUser $user) { + $dbPassword = $user->IsGuest ? '' : $user->DbRecord->Password; + return $this->verifyPassword($password, $dbPassword); + } + + public function generatePassword(string $password) { + return password_hash($password, PASSWORD_DEFAULT); + } + + public function verifyPassword(string $password, string $dbPassword) { + return password_verify($password, $dbPassword); + } + + public function setTimezonePreference(DbUser $user, string $timezone) { + if ($user->IsGuest) { + throw new TInvalidDataException( + Prado::localize( + 'Timezone preference change impossible for guest user' + ) + ); + } + $user->DbRecord->Timezone = $timezone; + $user->DbRecord->save(); + } + + public function getTimezonePreference(DbUser $user) { + if (!$user->IsGuest) { + try { + return new TimezoneDTO($user->DbRecord->Timezone); + } catch(Exception $e) {} + } + return new TimezoneDTO(date_default_timezone_get()); + } + +} + +?> diff --git a/app/frontend/facades/config.xml b/app/frontend/facades/config.xml new file mode 100644 index 0000000..8d5a298 --- /dev/null +++ b/app/frontend/facades/config.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="sqlmap" + class="System.Data.SqlMap.TSqlMapConfig" + ConnectionID="db" + ConfigFile="Application.sqlmap.config" + EnableCache="true" /> + </modules> +</configuration> diff --git a/app/frontend/i18n/config.xml b/app/frontend/i18n/config.xml new file mode 100644 index 0000000..c80b46d --- /dev/null +++ b/app/frontend/i18n/config.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <paths> + <using namespace="System.I18N.*" /> + </paths> + <modules> + <module id="globalization" class="System.I18N.TGlobalization"> + <translation type="gettext" + source="Application.i18n" + autosave="true" + cache="true" /> + </module> + </modules> +</configuration> diff --git a/app/frontend/layouts/Layout.php b/app/frontend/layouts/Layout.php new file mode 100644 index 0000000..324c69f --- /dev/null +++ b/app/frontend/layouts/Layout.php @@ -0,0 +1,11 @@ +<?php + +class Layout extends TTemplateControl { + + public function generateViewID() { + return $this->ViewID->Value ?: md5(mt_rand()); + } + +} + +?> diff --git a/app/frontend/layouts/MainLayout.php b/app/frontend/layouts/MainLayout.php new file mode 100644 index 0000000..5843952 --- /dev/null +++ b/app/frontend/layouts/MainLayout.php @@ -0,0 +1,9 @@ +<?php + +Prado::using('Application.layouts.Layout'); + +class MainLayout extends Layout { + +} + +?> diff --git a/app/frontend/layouts/MainLayout.tpl b/app/frontend/layouts/MainLayout.tpl new file mode 100644 index 0000000..0deb816 --- /dev/null +++ b/app/frontend/layouts/MainLayout.tpl @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> + <com:THead> + <com:TMetaTag HttpEquiv="Content-Type" Content="text/html; charset=utf-8" /> + <title><com:TContentPlaceHolder ID="Title" /></title> + </com:THead> + <body> + <com:TForm> + <header role="banner"> + <com:HeaderMenu /> + </header> + <main role="main"> + <com:TContentPlaceHolder ID="Content" /> + </main> + <footer role="contentinfo"> + </footer> + <com:THiddenField ID="ViewID"> + <prop:Value><%= $this->generateViewID() %></prop:Value> + </com:THiddenField> + </com:TForm> + </body> +</html> diff --git a/app/frontend/model/Calendar.php b/app/frontend/model/Calendar.php new file mode 100644 index 0000000..b49bf92 --- /dev/null +++ b/app/frontend/model/Calendar.php @@ -0,0 +1,92 @@ +<?php + +Prado::using('Application.db.ActiveRecord'); +Prado::using('Application.model.Entry'); +Prado::using('Application.model.Category'); + +class Calendar extends ActiveRecord { + + const TABLE = 'calendars'; + + public $UID; + public $Url; + public $Name; + public $Website; + public $Visible; + public $CustomName; + public $CustomImage; + public $CustomUrl; + public $LastUpdated; + + public $CategoryID; + + public static $COLUMN_MAPPING = [ + 'uid' => 'UID', + 'url' => 'Url', + 'name' => 'Name', + 'website' => 'Website', + 'visible' => 'Visible', + 'last_updated' => 'LastUpdated', + 'custom_name' => 'CustomName', + 'custom_image' => 'CustomImage', + 'custom_url' => 'CustomUrl', + '_category' => 'CategoryID' + ]; + + public static $RELATIONS = [ + 'Entries' => [self::HAS_MANY, 'Entry', '_calendar'], + 'Category' => [self::BELONGS_TO, 'Category', '_category'] + ]; + + public static function finder($className=__CLASS__) { + return parent::finder($className); + } + + const CUSTOM_IMAGE_PATH = 'resources/images/calendars'; + + public function getCustomImageUrl() { + if ($this->CustomImage) { + if (!preg_match('#^//#', $this->CustomImage)) { + return Prado::getApplication()->getAssetManager()->publishFilePath( + implode( + DIRECTORY_SEPARATOR, + [ + Prado::getApplication()->getBasePath(), + self::CUSTOM_IMAGE_PATH, + $this->CustomImage + ] + ), + TRUE + ); + } + return $this->CustomImage; + } + } + + public function getCustomImagePath($forFile = NULL, $type = '') { + $pathParts = [ + Prado::getApplication()->getBasePath(), + self::CUSTOM_IMAGE_PATH + ]; + if ($forFile) { + $pathParts[] = $this->_getCustomImageHash($forFile, $type); + } + return implode(DIRECTORY_SEPARATOR, $pathParts); + } + + private function _getCustomImageHash($file, $type) { + $hash = md5($file . md5_file($file) . filemtime($file)); + if ($type) { + $hash .= '.' . preg_replace('#^image/#', '', $type); + } + return $hash; + } + + public function saveData($data) { + $this->copyFrom($data); + return $this->save(); + } + +} + +?> diff --git a/app/frontend/model/Category.php b/app/frontend/model/Category.php new file mode 100644 index 0000000..87b97b6 --- /dev/null +++ b/app/frontend/model/Category.php @@ -0,0 +1,30 @@ +<?php + +Prado::using('Application.db.ActiveRecord'); +Prado::using('Application.model.Calendar'); + +class Category extends ActiveRecord { + + const TABLE = 'categories'; + + public $ID; + public $Name; + public $Priority; + + public static $COLUMN_MAPPING = [ + 'id' => 'ID', + 'name' => 'Name', + 'priority' => 'Priority' + ]; + + public static $RELATIONS = [ + 'Calendars' => [self::HAS_MANY, 'Calendar', '_category'] + ]; + + public static function finder($className=__CLASS__) { + return parent::finder($className); + } + +} + +?> diff --git a/app/frontend/model/Entry.php b/app/frontend/model/Entry.php new file mode 100644 index 0000000..4b93f04 --- /dev/null +++ b/app/frontend/model/Entry.php @@ -0,0 +1,43 @@ +<?php + +Prado::using('Application.db.ActiveRecord'); +Prado::using('Application.model.Calendar'); + +class Entry extends ActiveRecord { + + const TABLE = 'entries'; + + public $ID; + public $UID; + public $BeginDate; + public $EndDate; + public $AllDay; + public $Name; + public $Location; + public $LastModified; + + public $CalendarID; + + public static $COLUMN_MAPPING = [ + 'id' => 'ID', + 'uid' => 'UID', + 'begin_date' => 'BeginDate', + 'end_date' => 'EndDate', + 'all_day' => 'AllDay', + 'name' => 'Name', + 'location' => 'Location', + 'last_modified' => 'LastModified', + '_calendar' => 'CalendarID' + ]; + + public static $RELATIONS = [ + 'Calendar' => [self::BELONGS_TO, 'Calendar', '_calendar'] + ]; + + public static function finder($className=__CLASS__) { + return parent::finder($className); + } + +} + +?> diff --git a/app/frontend/model/User.php b/app/frontend/model/User.php new file mode 100644 index 0000000..d431183 --- /dev/null +++ b/app/frontend/model/User.php @@ -0,0 +1,36 @@ +<?php + +Prado::using('Application.db.ActiveRecord'); +Prado::using('Application.model.Calendar'); + +class User extends ActiveRecord { + + const TABLE = 'users'; + + public $ID; + public $Login; + public $Password; + public $IsAdmin; + public $Timezone; + public $LastLogin; + + public static $COLUMN_MAPPING = [ + 'id' => 'ID', + 'login' => 'Login', + 'password' => 'Password', + 'is_admin' => 'IsAdmin', + 'timezone' => 'Timezone', + 'last_login' => 'LastLogin' + ]; + + public static $RELATIONS = [ + 'Calendars' => [self::MANY_TO_MANY, 'Calendar', 'user_selections'] + ]; + + public static function finder($className=__CLASS__) { + return parent::finder($className); + } + +} + +?> diff --git a/app/frontend/model/UserPreference.php b/app/frontend/model/UserPreference.php new file mode 100644 index 0000000..90fa221 --- /dev/null +++ b/app/frontend/model/UserPreference.php @@ -0,0 +1,23 @@ +<?php + +Prado::using('Application.db.ActiveRecord'); + +class UserPreference extends ActiveRecord { + + const TABLE = 'user_selections'; + + public $UserID; + public $CalendarID; + + public static $COLUMN_MAPPING = [ + '_user' => 'UserID', + '_calendar' => 'CalendarID' + ]; + + public static function finder($className=__CLASS__) { + return parent::finder($className); + } + +} + +?> diff --git a/app/frontend/model/config.xml b/app/frontend/model/config.xml new file mode 100644 index 0000000..a729150 --- /dev/null +++ b/app/frontend/model/config.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="active-record" + class="System.Data.ActiveRecord.TActiveRecordConfig" + ConnectionID="db" + EnableCache="true" /> + </modules> +</configuration> diff --git a/app/frontend/pages/Admin.page b/app/frontend/pages/Admin.page new file mode 100644 index 0000000..b130583 --- /dev/null +++ b/app/frontend/pages/Admin.page @@ -0,0 +1,5 @@ +<com:TContent ID="Content"> + <com:CalendarScaffold> + <prop:Facade><%= CalendarFacade::getInstance() %></prop:Facade> + </com:CalendarScaffold> +</com:TContent> diff --git a/app/frontend/pages/Calendar.page b/app/frontend/pages/Calendar.page new file mode 100644 index 0000000..f414dae --- /dev/null +++ b/app/frontend/pages/Calendar.page @@ -0,0 +1,28 @@ +<com:TContent ID="Content"> + <com:CalendarDetails RaiseException="True"> + <prop:Facade><%= CalendarFacade::getInstance() %></prop:Facade> + <prop:CalendarUrl><%= $this->Request->itemAt('calendar') %></prop:CalendarUrl> + </com:CalendarDetails> + <com:AddToFilter> + <prop:Facade><%= CalendarFacade::getInstance() %></prop:Facade> + <prop:UserToManage><%= $this->getUser() %></prop:UserToManage> + <prop:CalendarUrl><%= $this->Request->itemAt('calendar') %></prop:CalendarUrl> + <prop:Description> + <%[ calendar visible in current filter selection ]%> + </prop:Description> + </com:AddToFilter> + <com:EventList DateFrom="now" DateTo="+1 year"> + <prop:Facade><%= CalendarFacade::getInstance() %></prop:Facade> + <prop:CalendarUrl><%= $this->Request->itemAt('calendar') %></prop:CalendarUrl> + <prop:HeaderText> + <%[ Upcoming events in the calendar: ]%> + </prop:HeaderText> + </com:EventList> + <com:EventList DateTo="now" Reverse="true"> + <prop:Facade><%= CalendarFacade::getInstance() %></prop:Facade> + <prop:CalendarUrl><%= $this->Request->itemAt('calendar') %></prop:CalendarUrl> + <prop:HeaderText> + <%[ Past events in the calendar: ]%> + </prop:HeaderText> + </com:EventList> +</com:TContent> diff --git a/app/frontend/pages/Home.page b/app/frontend/pages/Home.page new file mode 100644 index 0000000..fb60066 --- /dev/null +++ b/app/frontend/pages/Home.page @@ -0,0 +1,8 @@ +<com:TContent ID="Content"> + <com:CalendarGrid> + <prop:Facade><%= EventFacade::getInstance() %></prop:Facade> + <prop:Month><%= $this->Request->itemAt('month') %></prop:Month> + <prop:Year><%= $this->Request->itemAt('year') %></prop:Year> + <prop:UserToDisplay><%= $this->User %></prop:UserToDisplay> + </com:CalendarGrid> +</com:TContent> diff --git a/app/frontend/pages/Login.page b/app/frontend/pages/Login.page new file mode 100644 index 0000000..15bc93e --- /dev/null +++ b/app/frontend/pages/Login.page @@ -0,0 +1,3 @@ +<com:TContent ID="Content"> + <com:LoginBox /> +</com:TContent> diff --git a/app/frontend/pages/Profile.page b/app/frontend/pages/Profile.page new file mode 100644 index 0000000..163d3fa --- /dev/null +++ b/app/frontend/pages/Profile.page @@ -0,0 +1,21 @@ +<com:TContent ID="Content"> + <com:PasswordChange> + <prop:Facade><%= UserFacade::getInstance() %></prop:Facade> + <prop:UserToChange><%= $this->User %></prop:UserToChange> + </com:PasswordChange> + <br /> + <com:TimezoneSelect> + <prop:Facade><%= UserFacade::getInstance() %></prop:Facade> + <prop:UserToChange><%= $this->User %></prop:UserToChange> + </com:TimezoneSelect> + <br /> + <com:UserSelection> + <prop:UserToDisplay><%= $this->User %></prop:UserToDisplay> + <prop:Facade><%= CalendarFacade::getInstance() %></prop:Facade> + </com:UserSelection> + <br /> + <com:UpcomingEvents> + <prop:UserToDisplay><%= $this->User %></prop:UserToDisplay> + <prop:Facade><%= EventFacade::getInstance() %></prop:Facade> + </com:UpcomingEvents> +</com:TContent> diff --git a/app/frontend/pages/Select.page b/app/frontend/pages/Select.page new file mode 100644 index 0000000..5e1f232 --- /dev/null +++ b/app/frontend/pages/Select.page @@ -0,0 +1,8 @@ +<com:TContent ID="Content"> + <com:CalendarGroupFilter> + <prop:Facade><%= CalendarFacade::getInstance() %></prop:Facade> + </com:CalendarGroupFilter> + <com:CalendarSelection> + <prop:Facade><%= CalendarFacade::getInstance() %></prop:Facade> + </com:CalendarSelection> +</com:TContent> diff --git a/app/frontend/pages/Signup.page b/app/frontend/pages/Signup.page new file mode 100644 index 0000000..834b7cf --- /dev/null +++ b/app/frontend/pages/Signup.page @@ -0,0 +1,5 @@ +<com:TContent ID="Content"> + <com:RegistrationForm> + <prop:Facade><%= UserFacade::getInstance() %></prop:Facade> + </com:RegistrationForm> +</com:TContent> diff --git a/app/frontend/pages/config.xml b/app/frontend/pages/config.xml new file mode 100644 index 0000000..305651e --- /dev/null +++ b/app/frontend/pages/config.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + + <services> + <service id="page" + class="TPageService" + ClientScriptManagerClass="Application.web.ClientScriptManager"> + <modules> + <module id="theme" + class="Application.web.ThemeManager" + BasePath="Web._themes" /> + </modules> + </service> + </services> + + <pages MasterClass="Application.layouts.MainLayout" + Theme="default" + StatePersisterClass="System.Web.UI.TCachePageStatePersister" + StatePersister.CacheModuleID="cache" + StatePersister.CacheTimeout="3600" /> + + <authorization> + <allow pages="Admin,Signup" roles="Admin" /> + <deny pages="Admin,Signup" /> + <deny pages="Profile" users="?" /> + </authorization> + +</configuration> diff --git a/app/frontend/resources b/app/frontend/resources new file mode 120000 index 0000000..bc76415 --- /dev/null +++ b/app/frontend/resources @@ -0,0 +1 @@ +../../resources
\ No newline at end of file diff --git a/app/frontend/runtime b/app/frontend/runtime new file mode 120000 index 0000000..2cee2e0 --- /dev/null +++ b/app/frontend/runtime @@ -0,0 +1 @@ +../../cache/prado
\ No newline at end of file diff --git a/app/frontend/sqlmap/config.xml b/app/frontend/sqlmap/config.xml new file mode 100644 index 0000000..101e1b7 --- /dev/null +++ b/app/frontend/sqlmap/config.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<sqlMapConfig> + <provider class="TAdodbProvider"> + <datasource ConnectionID="db" /> + </provider> + <sqlMaps> + <sqlMap name="events" resource="events.xml" /> + </sqlMaps> +</sqlMapConfig> diff --git a/app/frontend/sqlmap/events.xml b/app/frontend/sqlmap/events.xml new file mode 100644 index 0000000..030a773 --- /dev/null +++ b/app/frontend/sqlmap/events.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8" ?> +<sqlMapConfig> + <select id="getEvents" resultClass="Entry"> + <![CDATA[ + SELECT * FROM entries + WHERE end_date >= #date_from# AND begin_date <= #date_to# + AND $calendar_clause$ + ORDER BY begin_date $order_clause$; + ]]> + </select> +</sqlMapConfig> diff --git a/app/frontend/themes/default/preloader.gif b/app/frontend/themes/default/preloader.gif Binary files differnew file mode 100644 index 0000000..6505467 --- /dev/null +++ b/app/frontend/themes/default/preloader.gif diff --git a/app/frontend/url/UrlManager.php b/app/frontend/url/UrlManager.php new file mode 100644 index 0000000..a33d98e --- /dev/null +++ b/app/frontend/url/UrlManager.php @@ -0,0 +1,57 @@ +<?php + +Prado::using('System.Web.TUrlMapping'); + +class UrlManager extends TUrlMapping { + + public function constructUrl($serviceID, $serviceParam, $getItems, $encodeAmpersand, $encodeGetItems) { + $url = parent::constructUrl( + $serviceID, + $serviceParam, + $getItems, + $encodeAmpersand, + $encodeGetItems + ); + return rtrim( + preg_replace( + '#^/' . $serviceParam . '#', + '/' . $this->_convertServiceParam($serviceParam), + preg_replace('#^/' . $serviceID . '#', '', $url) + ), + '/' + ) . '/'; + } + + public function parseUrl() { + $params = parent::parseUrl(); + if ($this->MatchingPattern) { + $serviceID = $this->MatchingPattern->ServiceID; + if (isset($params[$serviceID])) { + $params[$serviceID] = $this->_parseServiceParam($params[$serviceID]); + } + } + return $params; + } + + /** + * Convert service param from camelCase to hyphenated-form. + **/ + private function _convertServiceParam($param) { + return implode( + '-', + array_map('mb_strtolower', array_filter(preg_split('/(?=[A-Z])/', $param))) + ); + } + + /** + * Convert service param from hyphenated-form to camelCase. + **/ + private function _parseServiceParam($param) { + return implode( + '', + array_map('ucfirst', explode('-', $param)) + ); + } +} + +?> diff --git a/app/frontend/url/config.xml b/app/frontend/url/config.xml new file mode 100644 index 0000000..b072b2d --- /dev/null +++ b/app/frontend/url/config.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="url" + class="Application.url.UrlManager" + UrlPrefix="/" + EnableCustomUrl="True"> + + <url ServiceParameter="Home" + UrlFormat="HiddenPath" + pattern="{month}/{year}/" + parameters.month="\d{2}" + parameters.year="\d{4}" /> + <url ServiceParameter="Home" + UrlFormat="HiddenPath" + pattern="{month}/" + parameters.month="\d{2}" /> + <url ServiceParameter="Home" + UrlFormat="HiddenPath" + EnableCustomUrl="false" + pattern="" /> + + <url ServiceParameter="Calendar" + UrlFormat="HiddenPath" + pattern="calendar/{calendar}/" + parameters.calendar=".*" /> + + <url ServiceParameter="*" + UrlFormat="HiddenPath" + EnableCustomUrl="false" + pattern="{*}" /> + </module> + + <module id="request" + class="THttpRequest" + UrlFormat="HiddenPath" + UrlParamSeparator="/" + UrlManager="url" /> + <module id="response" + class="THttpResponse" + CacheControl="public" + CacheExpire="10" /> + </modules> +</configuration> diff --git a/app/frontend/user/DbUser.php b/app/frontend/user/DbUser.php new file mode 100644 index 0000000..d636e8b --- /dev/null +++ b/app/frontend/user/DbUser.php @@ -0,0 +1,60 @@ +<?php + +Prado::using('System.Security.TDbUserManager'); +Prado::using('Application.model.User'); +Prado::using('Application.facades.UserFacade'); + +class DbUser extends TDbUser { + + private $_record; + + public function setDbRecord(User $record) { + $this->_record = $record; + } + + public function getDbRecord() { + if (!$this->_record) { + $this->_record = UserFacade::getInstance()->findByLogin($this->Name); + } + return $this->_record; + } + + public function createUser($username) { + $dbUser = UserFacade::getInstance()->findByLogin($username); + if (!$dbUser) { + return NULL; + } + $user = new DbUser($this->Manager); + $user->setDbRecord($dbUser); + $user->Name = $dbUser->Login; + if ($dbUser->IsAdmin) { + $user->Roles = 'Admin'; + } + $user->IsGuest = FALSE; + return $user; + } + + public function validateUser($login, $password) { + $user = UserFacade::getInstance()->findByLogin($login); + $dbPassword = $user ? $user->Password : ''; + if (UserFacade::getInstance()->verifyPassword($password, $dbPassword) + && $user) { + $user->LastLogin = date('Y-m-d H:i:s'); + $user->save(); + return TRUE; + } else { + return FALSE; + } + } + + public function __call($name, $args) { + $match = []; + if (preg_match('/^getIs(.+)$/', $name, $match)) { + return $this->isInRole($match[1]); + } + throw new Exception('Unimplemented CustomDbUser method'); + } + +} + +?> diff --git a/app/frontend/user/config.xml b/app/frontend/user/config.xml new file mode 100644 index 0000000..80027e5 --- /dev/null +++ b/app/frontend/user/config.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="auth" class="System.Security.TAuthManager" + UserManager="users" LoginPage="Login" /> + <module id="users" class="System.Security.TDbUserManager" + UserClass="Application.user.DbUser" /> + </modules> +</configuration> diff --git a/app/frontend/web/AssetManager.php b/app/frontend/web/AssetManager.php new file mode 100644 index 0000000..2677585 --- /dev/null +++ b/app/frontend/web/AssetManager.php @@ -0,0 +1,11 @@ +<?php + +Prado::using('Application.web.BaseUrlDerivedFromBasePath'); + +class AssetManager extends TAssetManager { + + use BaseUrlDerivedFromBasePath; + +} + +?> diff --git a/app/frontend/web/BaseUrlDerivedFromBasePath.php b/app/frontend/web/BaseUrlDerivedFromBasePath.php new file mode 100644 index 0000000..2da277f --- /dev/null +++ b/app/frontend/web/BaseUrlDerivedFromBasePath.php @@ -0,0 +1,29 @@ +<?php + +trait BaseUrlDerivedFromBasePath { + + public function init($config) { + if ($this->BaseUrl === NULL && $this->BasePath !== NULL) { + $appWebPath = preg_replace( + '#' . $this->Application->Request->ApplicationUrl . '$#', + '', + $this->Application->Request->ApplicationFilePath + ); + $appBaseUrl = preg_replace( + '#^' . $appWebPath . '#', + '', + dirname($this->Application->Request->ApplicationFilePath) + ); + $this->BaseUrl = $appBaseUrl + . preg_replace( + '#^' . Prado::getPathOfNamespace('Web') . '#', + '', + $this->BasePath + ); + } + parent::init($config); + } + +} + +?> diff --git a/app/frontend/web/ClientScriptManager.php b/app/frontend/web/ClientScriptManager.php new file mode 100644 index 0000000..223c6e2 --- /dev/null +++ b/app/frontend/web/ClientScriptManager.php @@ -0,0 +1,572 @@ +<?php + +Prado::using('Application.layouts.Layout'); + +class ClientScriptManager extends TClientScriptManager { + + private $_page; + public function __construct(TPage $page) { + $this->_page = $page; + parent::__construct($page); + } + + // Base path of the entire application + private function _getBasePath() { + return Prado::getPathOfNamespace('Web'); + } + + // Translate URLs to filesystem paths, index return array by URLs + private function _getBasePaths(array $urls) { + $basePath = $this->_getBasePath(); + return array_combine( + $urls, + array_map( + function($path) use($basePath) { + return $basePath . DIRECTORY_SEPARATOR . $path; + }, + $urls + ) + ); + } + + // Base cache path, suffixed with subdirectory, create on demand + private function _getCachePath(string $subdir = 'assets') { + $cachePath = $this->Application->RuntimePath + . DIRECTORY_SEPARATOR + . $subdir; + if (!is_dir($cachePath)) { + if (file_exists($cachePath)) { + throw new TIOException( + sprintf( + 'Client script manager cache path "%s" exists and is not a directory', + $cachePath + ) + ); + } else { + mkdir($cachePath); + } + } + return $cachePath; + } + + // Cache path for a file of specified type + private function _getCacheFilePath($path, $type) { + return $this->_getCachePath($type) + . DIRECTORY_SEPARATOR + . $path; + } + + // Cache key for specific file set, including current theme + private function _getFileCollectionCacheKey(array $files) { + sort($files); + if ($this->_page->Theme) { + $files[] = $this->_page->Theme->Name; + } + return md5(implode(PHP_EOL, $files)); + } + + // Last modification time of a file set + private function _getFileCollectionMTime(array $files) { + return max(array_map('filemtime', $files)); + } + + // Storage (application cache) key for list of rendered assets of specified type + // Rendered[ASSET_TYPE].[VIEW_ID] (VIEW_ID as rendered in Layout hidden field) + private function _getRenderedAssetsStoreKey($type) { + $template = $this->_page->Master; + if (!$template instanceof Layout) { + throw new TNotSupportedException( + 'Compiled assets may only be used within Layout master class controls' + ); + } + return sprintf('Rendered%s.%s', $type, $template->generateViewID()); + } + + // Shorthand for JS assets cache key + private function _getRenderedScriptsStoreKey() { + return $this->_getRenderedAssetsStoreKey('Scripts'); + } + + // Shorthand for CSS assets cache key + private function _getRenderedSheetsStoreKey() { + return $this->_getRenderedAssetsStoreKey('Sheets'); + } + + // Application (primary) cache module, required to keep track of assets rendered on current page + private function _getCache() { + $cache = $this->Application->Cache; + if (!$cache) { + throw new TNotSupportedException( + 'Compiled assets require cache to be configured' + ); + } + return $cache; + } + + // Check cache file validity, comparing to source file set + private function _isCacheValid($cacheFile, array $paths) { + return file_exists($cacheFile) + && (filemtime($cacheFile) >= $this->_getFileCollectionMTime($paths)); + } + + // Determine whether specific URL points to a local asset (i.e. existing on the filesystem) + private function _isFileLocal($file) { + $basePath = $this->_getBasePath(); + return file_exists($basePath . DIRECTORY_SEPARATOR . $file); + } + + // Filter URL set to leave only local assets + private function _determineLocalFiles($files) { + return array_filter( + $files, [$this, '_isFileLocal'] + ); + } + + // Scripts - internal methods + + private $_renderedScriptsInitialized = FALSE; + + // Retrieve scripts already rendered on current page from application cache, + // maintaining the state over callbacks + private function _getRenderedScripts() { + $sessionKey = $this->_getRenderedScriptsStoreKey(); + if ($this->_page->IsCallBack || $this->_renderedScriptsInitialized) { + return $this->_getCache()->get($sessionKey) ?: []; + } else { + $this->_getCache()->delete($sessionKey); + $this->_renderedScriptsInitialized = TRUE; + return []; + } + } + + // Store information on rendered scripts in application cache + private function _appendRenderedScripts(array $newScripts, $compiledFileKey) { + $scripts = $this->_getRenderedScripts(); + if (!isset($scripts[$compiledFileKey])) { + $scripts[$compiledFileKey] = []; + } + $scripts[$compiledFileKey] = array_unique( + array_merge( + $scripts[$compiledFileKey], + $newScripts + ) + ); + $this->_getCache()->set( + $this->_getRenderedScriptsStoreKey(), + $scripts + ); + } + + // Compress JS file and return its content + private function _getCompressedScript($path) { + return trim(TJavaScript::JSMin( + file_get_contents($path) + )); + } + + // Join multiple script files into single asset, mark all of them as rendered in parent + private function _compileScriptFiles(array $files) { + foreach ($files as $file) { + $this->markScriptFileAsRendered($file); + } + $paths = $this->_getBasePaths($files); + $cacheFile = $this->_getCacheFilePath( + $this->_getFileCollectionCacheKey($paths) . '.js', + 'scripts' + ); + $this->_appendRenderedScripts($files, $cacheFile); + if (!$this->_isCacheValid($cacheFile, $paths)) { + $scriptContent = implode( + PHP_EOL, + array_map( + [$this, '_getCompressedScript'], + $paths + ) + ); + file_put_contents($cacheFile, $scriptContent); + } + return $this->Application->AssetManager->publishFilePath($cacheFile); + } + + // Write output tag for a single, compiled JS asset, consisting of multiple local assets + private function _renderLocalScriptFiles(THtmlWriter $writer, array $localFiles) { + if ($localFiles) { + $assetPath = $this->_compileScriptFiles($localFiles); + $writer->write(TJavascript::renderScriptFile($assetPath)); + } + } + + // Keep track of external JS scripts rendered on the page + private function _renderExternalScriptFiles(THtmlWriter $writer, array $externalFiles) { + if ($externalFiles) { + foreach ($externalFiles as $file) { + $this->markScriptFileAsRendered($file); + $this->_appendRenderedScripts([$file], $file); + } + parent::renderScriptFiles($writer, $externalFiles); + } + } + + // Determine actual asset URL that a source JS file was rendered as (after compilation/compression) + // FALSE means script wasn't rendered at all (i.e. was just registered in current callback) + private function _getRenderedScriptUrl($registeredScript) { + $renderedScripts = $this->_getRenderedScripts(); + foreach ($renderedScripts as $compiledFile => $scripts) { + if (in_array($registeredScript, $scripts)) { + if (file_exists($compiledFile)) { + return $this->Application->AssetManager->getPublishedUrl( + $compiledFile + ); + } else { + return $registeredScript; + } + } + } + return FALSE; + } + + // Scripts - public interface overrides + + // In application modes "higher" than Debug, compile JS assets to as few files as possible + public function renderScriptFiles($writer, Array $files) { + if ($this->getApplication()->getMode() !== TApplicationMode::Debug) { + if ($files) { + $localFiles = $this->_determineLocalFiles($files); + $this->_renderLocalScriptFiles($writer, $localFiles); + $externalFiles = array_diff($files, $localFiles); + $this->_renderExternalScriptFiles($writer, $externalFiles); + } + } else { + parent::renderScriptFiles($writer, $files); + } + } + + // When above compilation occurs, list of JS URLs a callback requires + // significantly deviates from parent implementation. + // Also, new scripts that have been registered in current callback may need compiling, too. + public function getScriptUrls() { + if ($this->getApplication()->getMode() !== TApplicationMode::Debug) { + $registeredScripts = array_unique( + array_merge( + $this->_scripts, $this->_headScripts + ) + ); + $scriptUrls = []; + $newScripts = []; + foreach ($registeredScripts as $registeredScript) { + $renderedScriptUrl = $this->_getRenderedScriptUrl($registeredScript); + if ($renderedScriptUrl) { + $scriptUrls[] = $renderedScriptUrl; + } else { + $newScripts[] = $registeredScript; + } + } + $newLocalScripts = $this->_determineLocalFiles($newScripts); + $newRemoteScripts = array_diff($newScripts, $newLocalScripts); + if ($newLocalScripts) { + $scriptUrls[] = $this->_compileScriptFiles($newLocalScripts); + } + $scriptUrls = array_values( + array_unique(array_merge($scriptUrls, $newRemoteScripts)) + ); + return $scriptUrls; + } + return parent::getScriptUrls(); + } + + private $_scripts = []; + private $_headScripts = []; + + // Keep track of what we're registering + public function registerScriptFile($key, $file) { + $this->_scripts[$key] = $file; + return parent::registerScriptFile($key, $file); + } + + // Keep track of what we're registering + public function registerHeadScriptFile($key, $file) { + $this->_headScripts[$key] = $file; + return parent::registerHeadScriptFile($key, $file); + } + + // Stylesheets - internal methods + + private $_renderedSheetsInitialized = FALSE; + + // Retrieve stylesheets already rendered on current page from application cache, + // maintaining the state over callbacks + private function _getRenderedSheets() { + $sessionKey = $this->_getRenderedSheetsStoreKey(); + if ($this->_page->IsCallBack || $this->_renderedSheetsInitialized) { + return $this->_getCache()->get($sessionKey) ?: []; + } else { + $this->_getCache()->delete($sessionKey); + $this->_renderedSheetsInitialized = TRUE; + return []; + } + } + + // Store information on rendered stylesheets in application cache + private function _appendRenderedSheets(array $newSheets, $compiledFileKey) { + $sheets = $this->_getRenderedSheets(); + if (!isset($sheets[$compiledFileKey])) { + $sheets[$compiledFileKey] = []; + } + $sheets[$compiledFileKey] = array_merge( + $sheets[$compiledFileKey], + $newSheets + ); + $this->_getCache()->set( + $this->_getRenderedSheetsStoreKey(), + $sheets + ); + } + + // Resolve all "url(FILE)" CSS directives pointing + // to relative resources to specified path + private function _fixStyleSheetPaths($content, $originalUrl) { + $originalDir = dirname($originalUrl . '.'); + return preg_replace_callback( + '/url\s*\([\'"]?(.*?)[\'"]?\)/', + function($matches) use($originalDir) { + $url = parse_url($matches[1]); + // ignore absolute URLs and paths + if (isset($url['scheme']) + || isset($url['host']) + || $url['path'][0] == '/') { + return $matches[0]; + } + // resolve relative paths + return str_replace( + $matches[1], + $originalDir . '/' . $matches[1], + $matches[0] + ); + }, + $content + ); + } + + // Compress CSS file and return its content + private function _getCompressedSheet($origPath, $path) { + Prado::using('Lib.cssmin.CssMin'); + return trim(CssMin::minify( + $this->_fixStyleSheetPaths( + file_get_contents($path), + $origPath + ) + )); + } + + // Join multiple stylesheet files into single asset + private function _compileSheetFiles(array $files) { + $paths = $this->_getBasePaths( + array_map('reset', $files) + ); + // determine if file was registered as a themed sheet + $correctedPaths = []; + foreach ($paths as $url => $path) { + $correctedPaths[ + in_array($url, $this->_themeStyles) && $this->_page->Theme + ? $this->_page->Theme->BaseUrl . DIRECTORY_SEPARATOR + : $url + ] = $path; + } + $cacheFile = $this->_getCacheFilePath( + $this->_getFileCollectionCacheKey($paths) . '.css', + 'styles' + ); + $this->_appendRenderedSheets($files, $cacheFile); + if (!$this->_isCacheValid($cacheFile, $paths)) { + $styleContent = implode( + PHP_EOL, + array_map( + [$this, '_getCompressedSheet'], + array_keys($correctedPaths), + $correctedPaths + ) + ); + file_put_contents($cacheFile, $styleContent); + } + return $this->Application->AssetManager->publishFilePath($cacheFile); + } + + // Filter only local stylesheet file entries + private function _determineLocalSheetFiles(array $files) { + $basePath = $this->_getBasePath(); + return array_filter( + $files, + function($file) use($basePath) { + return file_exists($basePath . DIRECTORY_SEPARATOR . $file[0]); + } + ); + } + + // Write HTML markup for CSS stylesheet + private function _renderSheetFileTag(THtmlWriter $writer, $href, $media) { + $writer->addAttribute('rel', 'stylesheet'); + $writer->addAttribute('type', 'text/css'); + $writer->addAttribute('media', $media); + $writer->addAttribute('href', $href); + $writer->renderBeginTag('link'); + $writer->write(PHP_EOL); + } + + // Group registered local CSS assets by media query string and render markup for compiled sheets + private function _renderLocalSheetFiles(THtmlWriter $writer, array $localFiles) { + if ($localFiles) { + $fileTypes = []; + foreach ($localFiles as $file) { + $type = $file[1] ?: 'all'; + if (!isset($fileTypes[$type])) { + $fileTypes[$type] = []; + } + $fileTypes[$type][] = $file; + } + foreach ($fileTypes as $type => $files) { + $assetPath = $this->_compileSheetFiles($files); + $this->_renderSheetFileTag($writer, $assetPath, $type); + } + } + } + + // Render markup for external stylesheets + private function _renderExternalSheetFiles(THtmlWriter $writer, array $externalFiles) { + if ($externalFiles) { + foreach ($externalFiles as $file) { + $this->_appendRenderedSheets([$file], $file[0]); + $this->_renderSheetFileTag($writer, $file[0], $file[1] ?: 'all'); + } + } + } + + // Determine actual asset URL that a source CSS file was rendered as (after compilation/compression) + // FALSE means sheet wasn't rendered at all (i.e. was just registered in current callback) + // Media query types can easily be ignored in a callback request, only URLs matter + private function _getRenderedSheetUrl($registeredSheet) { + $renderedSheets = $this->_getRenderedSheets(); + foreach ($renderedSheets as $compiledFile => $sheets) { + foreach ($sheets as $sheet) { + if ($registeredSheet[0] == $sheet[0]) { + if (file_exists($compiledFile)) { + return $this->Application->AssetManager->getPublishedUrl( + $compiledFile + ); + } else { + return $registeredSheet[0]; + } + } + } + } + return FALSE; + } + + // Stylesheets - public interface overrides + + // In application modes "higher" than Debug, compile CSS assets to as few files as possible + public function renderStyleSheetFiles($writer) { + if ($this->getApplication()->getMode() !== TApplicationMode::Debug) { + $files = $this->_styles; + if ($files) { + $localFiles = $this->_determineLocalSheetFiles($files); + $this->_renderLocalSheetFiles($writer, $localFiles); + $externalFiles = array_diff_key($files, $localFiles); + $this->_renderExternalSheetFiles($writer, $externalFiles); + } + } else { + parent::renderStyleSheetFiles($writer); + } + } + + // When above compilation occurs, list of CSS URLs a callback requires + // significantly deviates from parent implementation. + // New stylesheets may need compiling, as well. + public function getStyleSheetUrls() { + if ($this->getApplication()->getMode() !== TApplicationMode::Debug) { + $registeredSheets = $this->_styles; + $sheetUrls = []; + $newSheets = []; + foreach ($registeredSheets as $registeredSheet) { + $renderedSheetUrl = $this->_getRenderedSheetUrl( + $registeredSheet + ); + if ($renderedSheetUrl) { + $sheetUrls[] = $renderedSheetUrl; + } else { + $newSheets[] = $registeredSheet; + } + } + $newLocalSheets = $this->_determineLocalSheetFiles($newSheets); + $newLocalUrls = array_map('reset', $newLocalSheets); + $newRemoteSheets = array_filter( + $newSheets, + function($sheet) use($newLocalUrls) { + return !in_array($sheet[0], $newLocalUrls); + } + ); + $newRemoteUrls = array_map('reset', $newRemoteSheets); + if ($newLocalSheets) { + $sheetUrls[] = $this->_compileSheetFiles($newLocalSheets); + } + $sheetUrls = array_values( + array_unique(array_merge($sheetUrls, $newRemoteUrls)) + ); + return $sheetUrls; + } + // And even in Debug mode, theme sheets before fixing paths + // might also have been published via assets manager, + // so we have to discard these from parent list. + return array_diff( + parent::getStyleSheetUrls(), + $this->_fixedStyleFiles + ); + } + + private $_styles = []; + + // Keep track of what we're registering + public function registerStyleSheetFile($key, $file, $media = '') { + $this->_styles[$key] = [$file, $media]; + return parent::registerStyleSheetFile($key, $file, $media ?: 'all'); + } + + private $_themeStyles = []; + private $_fixedStyleFiles = []; + + // New method, automatically corrects URLs within stylesheet to current page theme + // when sheets are not compiled (when they are, it's done on compilation anyways). + // Such files are rewritten (not in place, though) and registered so that they don't show up + // within published assets returned from asset manager, as they don't end up in the markup. + public function registerThemeStyleSheetFile($key, $file, $media = '') { + if ($this->getApplication()->getMode() !== TApplicationMode::Debug) { + $this->_themeStyles[$key] = $file; + } else { + if ($this->_isFileLocal($file) && $this->_page->Theme) { + $tempFile = $this->_getCacheFilePath( + $this->_getFileCollectionCacheKey([ + $this->_getBasePath() + . DIRECTORY_SEPARATOR + . $file + ]) + . '.' . basename($file), + $this->_page->Theme->Name + ); + file_put_contents( + $tempFile, + $this->_fixStyleSheetPaths( + file_get_contents( + $this->_getBasePath() . DIRECTORY_SEPARATOR . $file + ), + $this->_page->Theme->BaseUrl . DIRECTORY_SEPARATOR + ) + ); + $this->_fixedStyleFiles[] = $file; + $file = $this->Application->AssetManager->publishFilePath($tempFile); + } + } + return $this->registerStyleSheetFile($key, $file, $media ?: 'all'); + } + +} + +?> diff --git a/app/frontend/web/FacadeTemplateControl.php b/app/frontend/web/FacadeTemplateControl.php new file mode 100644 index 0000000..05d148c --- /dev/null +++ b/app/frontend/web/FacadeTemplateControl.php @@ -0,0 +1,27 @@ +<?php + +Prado::using('Application.facades.Facade'); +Prado::using('Application.web.TemplateControl'); + +class FacadeTemplateControl extends TemplateControl { + + public function setFacade(Facade $facade) { + $this->setControlState('Facade', $facade); + } + + public function getFacade() { + return $this->getControlState('Facade'); + } + + public function onPreRender($param) { + parent::onPreRender($param); + if (!$this->getFacade()) { + throw new TInvalidDataValueException( + 'FacadeTemplateControl requires a Facade instance' + ); + } + } + +} + +?> diff --git a/app/frontend/web/TemplateControl.php b/app/frontend/web/TemplateControl.php new file mode 100644 index 0000000..aa95d75 --- /dev/null +++ b/app/frontend/web/TemplateControl.php @@ -0,0 +1,176 @@ +<?php + +class TemplateControl extends TTemplateControl { + + public function onPreRender($param) { + parent::onPreRender($param); + $scriptFile = $this->_getControlScriptPath(get_class($this)); + if (file_exists($scriptFile)) { + $this->_registerScriptFile($scriptFile); + } + $styleFile = $this->_getControlStylePath(get_class($this)); + if (file_exists($styleFile)) { + $this->_registerStyleFile($styleFile); + } + } + + protected function getExternalScriptDependencies() { + return []; + } + + protected function getLibScriptDependencies() { + return []; + } + + protected function getPradoScriptDependencies() { + return []; + } + + protected function getControlScriptDependencies() { + return []; + } + + protected function getExternalStyleDependencies() { + return []; + } + + protected function getLibStyleDependencies() { + return []; + } + + protected function getControlStyleDependencies() { + return []; + } + + private function _getControlScriptPath($className) { + return Prado::getPathOfNamespace('Application.controls.scripts') + . DIRECTORY_SEPARATOR + . $className + . '.js'; + } + + private function _getControlStylePath($className) { + return Prado::getPathOfNamespace('Application.controls.styles') + . DIRECTORY_SEPARATOR + . $className + . '.css'; + } + + private function _getLibPath($identifier, $extension = '') { + return Prado::getPathOfNamespace('Lib') + . DIRECTORY_SEPARATOR + . $identifier + . $extension; + } + + private function _registerScriptFile($scriptFile) { + $this->_registerExternalScriptDependencies( + $this->getExternalScriptDependencies() + ); + $this->_registerLibScriptDependencies( + $this->getLibScriptDependencies() + ); + $this->_registerPradoScriptDependencies( + $this->getPradoScriptDependencies() + ); + $this->_registerControlScriptDependencies( + $this->getControlScriptDependencies() + ); + $this->Page->ClientScript->registerScriptFile( + 'TemplateControl.' . get_class($this), + $this->Application->AssetManager->publishFilePath($scriptFile) + ); + } + + private function _registerExternalScriptDependencies(array $dependencies) { + foreach ($dependencies as $dependency) { + $this->Page->ClientScript->registerHeadScriptFile( + $dependency, $dependency + ); + } + } + + private function _registerLibScriptDependencies(array $dependencies) { + foreach ($dependencies as $dependency) { + $this->Page->ClientScript->registerScriptFile( + 'LibScript.' . $dependency, + $this->Application->AssetManager->publishFilePath( + $this->_getLibPath($dependency, '.js') + ) + ); + } + } + + private function _registerPradoScriptDependencies(array $dependencies) { + foreach ($dependencies as $dependency) { + $this->Page->ClientScript->registerPradoScript($dependency); + } + } + + private function _registerControlScriptDependencies(array $dependencies) { + foreach ($dependencies as $dependency) { + $this->Page->ClientScript->registerScriptFile( + 'TemplateControl.' . $dependency, + $this->Application->AssetManager->publishFilePath( + $this->_getControlScriptPath($dependency) + ) + ); + } + } + + private function _registerStyleFile($styleFile) { + $this->_registerExternalStyleDependencies( + $this->getExternalStyleDependencies() + ); + $this->_registerLibStyleDependencies( + $this->getLibStyleDependencies() + ); + $this->_registerControlStyleDependencies( + $this->getControlStyleDependencies() + ); + if (method_exists($this->Page->ClientScript, 'registerThemeStyleSheetFile')) { + $this->Page->ClientScript->registerThemeStyleSheetFile( + 'TemplateControl.' . get_class($this), + $this->Application->AssetManager->publishFilePath($styleFile) + ); + } else { + $this->Page->ClientScript->registerStyleSheetFile( + 'TemplateControl.' . get_class($this), + $this->Application->AssetManager->publishFilePath($styleFile) + ); + } + } + + private function _registerExternalStyleDependencies(array $dependencies) { + foreach ($dependencies as $dependency) { + $this->Page->ClientScript->registerStyleSheetFile( + $dependency, $dependency + ); + } + } + + private function _registerLibStyleDependencies(array $dependencies) { + foreach ($dependencies as $dependency) { + $this->Page->ClientScript->registerStyleSheetFile( + 'LibStyle.' . $dependency, + $this->Application->AssetManager->publishFilePath( + $this->_getLibPath($dependency, '.css') + ) + ); + } + } + + private function _registerControlStyleDependencies(array $dependencies) { + foreach ($dependencies as $dependency) { + $this->Page->ClientScript->registerStyleSheetFile( + 'TemplateControl.' . $dependency, + $this->Application->AssetManager->publishFilePath( + $this->_getControlStylePath($dependency) + ) + ); + } + } + +} + +?> diff --git a/app/frontend/web/ThemeManager.php b/app/frontend/web/ThemeManager.php new file mode 100644 index 0000000..9dcae76 --- /dev/null +++ b/app/frontend/web/ThemeManager.php @@ -0,0 +1,11 @@ +<?php + +Prado::using('Application.web.BaseUrlDerivedFromBasePath'); + +class ThemeManager extends TThemeManager { + + use BaseUrlDerivedFromBasePath; + +} + +?> diff --git a/app/frontend/web/config.xml b/app/frontend/web/config.xml new file mode 100644 index 0000000..49bbcc6 --- /dev/null +++ b/app/frontend/web/config.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <modules> + <module id="asset" class="Application.web.AssetManager" + BasePath="Web._assets" /> + </modules> +</configuration> |