summaryrefslogtreecommitdiff
path: root/app/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'app/frontend')
-rw-r--r--app/frontend/application.xml30
-rw-r--r--app/frontend/caches.xml6
-rw-r--r--app/frontend/components/FileUploadSecureFileSize.php18
-rw-r--r--app/frontend/components/FileUploadSecureFileType.php19
-rw-r--r--app/frontend/components/FileUploadSecureMethods.php16
-rw-r--r--app/frontend/components/FileUploadSecureOption.php17
-rw-r--r--app/frontend/components/SafeActiveFileUpload.php13
-rw-r--r--app/frontend/components/SafeFileUpload.php11
-rw-r--r--app/frontend/controls/AddToFilter.php41
-rw-r--r--app/frontend/controls/AddToFilter.tpl8
-rw-r--r--app/frontend/controls/CalendarDetails.php7
-rw-r--r--app/frontend/controls/CalendarDetails.tpl13
-rw-r--r--app/frontend/controls/CalendarGrid.php59
-rw-r--r--app/frontend/controls/CalendarGrid.tpl28
-rw-r--r--app/frontend/controls/CalendarGroupFilter.php21
-rw-r--r--app/frontend/controls/CalendarGroupFilter.tpl16
-rw-r--r--app/frontend/controls/CalendarLabel.php13
-rw-r--r--app/frontend/controls/CalendarLabel.tpl12
-rw-r--r--app/frontend/controls/CalendarScaffold.php142
-rw-r--r--app/frontend/controls/CalendarScaffold.tpl81
-rw-r--r--app/frontend/controls/CalendarSelection.php17
-rw-r--r--app/frontend/controls/CalendarSelection.tpl8
-rw-r--r--app/frontend/controls/EventList.php59
-rw-r--r--app/frontend/controls/EventList.tpl6
-rw-r--r--app/frontend/controls/EventRepeater.php25
-rw-r--r--app/frontend/controls/EventRepeater.tpl12
-rw-r--r--app/frontend/controls/HeaderMenu.php26
-rw-r--r--app/frontend/controls/HeaderMenu.tpl36
-rw-r--r--app/frontend/controls/LoginBox.php39
-rw-r--r--app/frontend/controls/LoginBox.tpl33
-rw-r--r--app/frontend/controls/PasswordChange.php44
-rw-r--r--app/frontend/controls/PasswordChange.tpl59
-rw-r--r--app/frontend/controls/RegistrationForm.php28
-rw-r--r--app/frontend/controls/RegistrationForm.tpl66
-rw-r--r--app/frontend/controls/TimezoneSelect.php58
-rw-r--r--app/frontend/controls/TimezoneSelect.tpl5
-rw-r--r--app/frontend/controls/UpcomingEvents.php33
-rw-r--r--app/frontend/controls/UpcomingEvents.tpl5
-rw-r--r--app/frontend/controls/UrlBasedCalendarControl.php40
-rw-r--r--app/frontend/controls/UserSelection.php45
-rw-r--r--app/frontend/controls/UserSelection.tpl29
-rw-r--r--app/frontend/controls/config.xml6
-rw-r--r--app/frontend/controls/scripts/AddToFilter.js5
-rw-r--r--app/frontend/controls/scripts/CalendarGroupFilter.js29
-rw-r--r--app/frontend/controls/scripts/CalendarLabel.js11
-rw-r--r--app/frontend/controls/scripts/CalendarScaffold.js8
-rw-r--r--app/frontend/controls/styles/CalendarGrid.css16
-rw-r--r--app/frontend/controls/styles/CalendarScaffold.css11
-rw-r--r--app/frontend/db/ActiveRecord.php69
-rw-r--r--app/frontend/db/DBConnection.php28
-rw-r--r--app/frontend/db/DBModule.php40
-rw-r--r--app/frontend/db/DBTransaction.php53
l---------app/frontend/db/config.json1
-rw-r--r--app/frontend/db/config.xml10
-rw-r--r--app/frontend/dto/CalendarDTO.php31
-rw-r--r--app/frontend/dto/CalendarGridDTO.php84
-rw-r--r--app/frontend/dto/CalendarGridDayDTO.php28
-rw-r--r--app/frontend/dto/CalendarGroupDTO.php44
-rw-r--r--app/frontend/dto/EventDTO.php85
-rw-r--r--app/frontend/dto/GridEventDTO.php28
-rw-r--r--app/frontend/dto/TimezoneDTO.php46
l---------app/frontend/dto/weekdays.json1
-rw-r--r--app/frontend/events/CalendarPreferenceEvents.php16
-rw-r--r--app/frontend/events/EventModule.php53
-rw-r--r--app/frontend/events/config.xml6
-rw-r--r--app/frontend/facades/CalendarFacade.php212
-rw-r--r--app/frontend/facades/EventFacade.php115
-rw-r--r--app/frontend/facades/Facade.php62
-rw-r--r--app/frontend/facades/UserFacade.php78
-rw-r--r--app/frontend/facades/config.xml10
-rw-r--r--app/frontend/i18n/config.xml14
-rw-r--r--app/frontend/layouts/Layout.php11
-rw-r--r--app/frontend/layouts/MainLayout.php9
-rw-r--r--app/frontend/layouts/MainLayout.tpl22
-rw-r--r--app/frontend/model/Calendar.php92
-rw-r--r--app/frontend/model/Category.php30
-rw-r--r--app/frontend/model/Entry.php43
-rw-r--r--app/frontend/model/User.php36
-rw-r--r--app/frontend/model/UserPreference.php23
-rw-r--r--app/frontend/model/config.xml9
-rw-r--r--app/frontend/pages/Admin.page5
-rw-r--r--app/frontend/pages/Calendar.page28
-rw-r--r--app/frontend/pages/Home.page8
-rw-r--r--app/frontend/pages/Login.page3
-rw-r--r--app/frontend/pages/Profile.page21
-rw-r--r--app/frontend/pages/Select.page8
-rw-r--r--app/frontend/pages/Signup.page5
-rw-r--r--app/frontend/pages/config.xml28
l---------app/frontend/resources1
l---------app/frontend/runtime1
-rw-r--r--app/frontend/sqlmap/config.xml9
-rw-r--r--app/frontend/sqlmap/events.xml11
-rw-r--r--app/frontend/themes/default/preloader.gifbin0 -> 7681 bytes
-rw-r--r--app/frontend/url/UrlManager.php57
-rw-r--r--app/frontend/url/config.xml44
-rw-r--r--app/frontend/user/DbUser.php60
-rw-r--r--app/frontend/user/config.xml9
-rw-r--r--app/frontend/web/AssetManager.php11
-rw-r--r--app/frontend/web/BaseUrlDerivedFromBasePath.php29
-rw-r--r--app/frontend/web/ClientScriptManager.php572
-rw-r--r--app/frontend/web/FacadeTemplateControl.php27
-rw-r--r--app/frontend/web/TemplateControl.php176
-rw-r--r--app/frontend/web/ThemeManager.php11
-rw-r--r--app/frontend/web/config.xml7
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">&nbsp;</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
new file mode 100644
index 0000000..6505467
--- /dev/null
+++ b/app/frontend/themes/default/preloader.gif
Binary files differ
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>