oUser = new User($this->oDb); $this->oLang = new Translator('', self::DEFAULT_LANG); $this->oProject = new Project($this->oDb); $this->oMedia = new Media($this->oDb, $this->oProject); } protected function install() { //Install DB $this->oDb->install(); $this->oUser->addUser('admin@admin.com', $this->oLang->getLanguage(), date_default_timezone_get()); } public function syncPics() { if(Settings::DEBUG) { return (new Media($this->oDb, $this->oProject))->syncFileFolder(); } } protected function getSqlOptions() { return array ( 'tables' => array ( Feed::MSG_TABLE => array('ref_msg_id', Db::getId(Feed::FEED_TABLE), 'type', 'latitude', 'longitude', 'iso_time', 'site_time', 'timezone', 'unix_time', 'content', 'battery_state'), Feed::FEED_TABLE => array('ref_feed_id', Db::getId(Feed::SPOT_TABLE), Db::getId(Project::PROJ_TABLE), 'name', 'description', 'status', 'last_update'), Feed::SPOT_TABLE => array('ref_spot_id', 'name', 'model'), Project::PROJ_TABLE => array('name', 'codename', 'active_from', 'active_to'), self::POST_TABLE => array(Db::getId(Project::PROJ_TABLE), Db::getId(User::USER_TABLE), 'name', 'content', 'site_time', 'timezone'), Media::MEDIA_TABLE => array(Db::getId(Project::PROJ_TABLE), 'filename', 'type', 'taken_on', 'posted_on', 'timezone', 'rotate', 'comment'), User::USER_TABLE => array('name', 'email', 'gravatar', 'language', 'timezone', 'active', 'clearance'), self::MAP_TABLE => array('codename', 'geo_name', 'min_zoom', 'max_zoom', 'attribution'), self::MAPPING_TABLE => array(Db::getId(self::MAP_TABLE) , Db::getId(Project::PROJ_TABLE)) ), 'types' => array ( 'active' => "BOOLEAN DEFAULT ".User::USER_INACTIVE, 'clearance' => "TINYINT(1) DEFAULT ".User::CLEARANCE_USER, 'active_from' => "TIMESTAMP DEFAULT 0", 'active_to' => "TIMESTAMP DEFAULT 0", 'battery_state' => "VARCHAR(10)", 'codename' => "VARCHAR(100)", 'content' => "LONGTEXT", 'comment' => "LONGTEXT", 'description' => "VARCHAR(100)", 'email' => "VARCHAR(320) NOT NULL", 'filename' => "VARCHAR(100) NOT NULL", 'iso_time' => "VARCHAR(24)", 'language' => "VARCHAR(2)", 'last_update' => "TIMESTAMP DEFAULT 0", 'latitude' => "DECIMAL(7,5)", 'longitude' => "DECIMAL(8,5)", 'model' => "VARCHAR(20)", 'name' => "VARCHAR(100)", 'posted_on' => "TIMESTAMP DEFAULT 0", 'ref_feed_id' => "VARCHAR(40)", 'ref_msg_id' => "INT", 'ref_spot_id' => "VARCHAR(10)", 'rotate' => "SMALLINT", 'site_time' => "TIMESTAMP DEFAULT 0", //DEFAULT 0 removes auto-set to current time 'status' => "VARCHAR(10)", 'taken_on' => "TIMESTAMP DEFAULT 0", 'timezone' => "CHAR(64)", //see mysql.time_zone_name 'type' => "VARCHAR(20)", 'unix_time' => "INT", 'geo_name' => "VARCHAR(100)", 'min_zoom' => "TINYINT UNSIGNED", 'max_zoom' => "TINYINT UNSIGNED", 'attribution' => "VARCHAR(100)", 'gravatar' => "LONGTEXT" ), 'constraints' => array ( Feed::MSG_TABLE => array("UNIQUE KEY `uni_ref_msg_id` (`ref_msg_id`)", "INDEX(`ref_msg_id`)"), Feed::FEED_TABLE => array("UNIQUE KEY `uni_ref_feed_id` (`ref_feed_id`)", "INDEX(`ref_feed_id`)"), Feed::SPOT_TABLE => array("UNIQUE KEY `uni_ref_spot_id` (`ref_spot_id`)", "INDEX(`ref_spot_id`)"), Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)", Media::MEDIA_TABLE => "UNIQUE KEY `uni_file_name` (`filename`)", User::USER_TABLE => "UNIQUE KEY `uni_email` (`email`)" ), 'cascading_delete' => array ( Feed::SPOT_TABLE => array(Feed::MSG_TABLE) ) ); } public function getAppMainPage() { //Cache Page List $asPages = array_diff($this->asMasks, array('email_update', 'email_conf')); if(!$this->oUser->checkUserClearance(User::CLEARANCE_ADMIN)) { $asPages = array_diff($asPages, array('admin', 'upload')); } return parent::getMainPage( array( 'vars' => array( 'chunk_size' => self::FEED_CHUNK_SIZE, 'default_project_codename' => $this->oProject->getProjectCodeName(), 'projects' => $this->oProject->getProjects(), 'user' => $this->oUser->getUserInfo() ), 'consts' => array( 'server' => $this->asContext['serv_name'], 'geo_server' => Settings::GEO_SERVER, 'modes' => Project::MODES, 'default_timezone' => Settings::TIMEZONE ) ), 'index', array( 'host_url' => $this->asContext['serv_name'], 'filepath_css' => self::addTimestampToFilePath('style/spot.css'), 'filepath_js_d3' => self::addTimestampToFilePath('script/d3.min.js'), 'filepath_js_leaflet' => self::addTimestampToFilePath('script/leaflet.min.js'), 'filepath_js_jquery' => self::addTimestampToFilePath('script/jquery.min.js'), 'filepath_js_jquery_mods' => self::addTimestampToFilePath('script/jquery.mods.js'), 'filepath_js_spot' => self::addTimestampToFilePath('script/spot.js'), 'filepath_js_lightbox' => self::addTimestampToFilePath('script/lightbox.js') ), $asPages ); } public function checkUserClearance($iClearance) { return $this->oUser->checkUserClearance($iClearance); } /* Managing projects */ public function setProjectId($iProjectId=0) { $this->oProject->setProjectId($iProjectId); } public function updateProject() { $bNewMsg = false; $bSuccess = true; $sDesc = ''; //Update all feeds belonging to the project $asFeeds = $this->oProject->getFeedIds(); foreach($asFeeds as $iFeedId) { $oFeed = new Feed($this->oDb, $iFeedId); $bNewMsg = $bNewMsg || $oFeed->checkUpdateFeed($this->oProject->getMode()); } //Send Update Email if($bNewMsg) { $oEmail = new Email($this->asContext['serv_name'], 'email_update'); $oEmail->setDestInfo($this->oUser->getActiveUsersInfo()); //Add Position $asMessages = $this->getSpotMessages(); $asLastMessage = end($asMessages); $asLastMessage['token'] = Settings::GEO_SERVER_TOKEN; $oEmail->oTemplate->setTags($asLastMessage); $oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i'); //Add latest news feed $asNews = $this->getNewsFeed(0, true); $iPostCount = 0; foreach($asNews as $asPost) { if($asPost['type'] != 'message') { $oEmail->oTemplate->newInstance('news'); $oEmail->oTemplate->setInstanceTags('news', array( 'local_server' => $this->asContext['serv_name'], 'project' => $this->oProject->getProjectCodeName(), 'type' => $asPost['type'], 'id' => $asPost['id_'.$asPost['type']]) ); $oEmail->oTemplate->addInstance($asPost['type'], $asPost); $oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']); $iPostCount++; } if($iPostCount == self::MAIL_CHUNK_SIZE) break; } $bSuccess = $oEmail->send(); if(!$bSuccess) $sDesc = $oEmail->ErrorInfo; else $sDesc = 'mail_sent'; } else $sDesc = 'no_new_msg'; return self::getJsonResult($bSuccess, $sDesc); } public function genCronFile() { //$bSuccess = (file_put_contents('spot_cron.sh', '#!/bin/bash'."\n".'cd '.dirname($_SERVER['SCRIPT_FILENAME'])."\n".'php -f index.php a=update_feed')!==false); $sFileName = 'spot_cron.sh'; $sContent = '#!/bin/bash'."\n". 'wget -qO- '.$this->asContext['serv_name'].'index.php?a=update_project > /dev/null'."\n". '#Crontab job: 0 * * * * . '.dirname($_SERVER['SCRIPT_FILENAME']).'/'.$sFileName.' > /dev/null'."\n"; $bSuccess = (file_put_contents($sFileName, $sContent)!==false); return self::getJsonResult($bSuccess, ''); } public function getMarkers() { $asMessages = $this->getSpotMessages(); //Add medias if(!empty($asMessages)) { $asMedias = $this->getMedias('taken_on'); //Assign medias to closest message $iIndex = 0; $iMaxIndex = count($asMessages) - 1; foreach($asMedias as $asMedia) { while($iIndex <= $iMaxIndex && $asMedia['unix_time'] > $asMessages[$iIndex]['unix_time']) { $iIndex++; } if($iIndex == 0) $iMsgIndex = $iIndex; elseif($iIndex > $iMaxIndex) $iMsgIndex = $iMaxIndex; else { $iHalfWayPoint = ($asMessages[$iIndex - 1]['unix_time'] + $asMessages[$iIndex]['unix_time'])/2; $iMsgIndex = ($asMedia['unix_time'] >= $iHalfWayPoint)?$iIndex:($iIndex - 1); } $asMessages[$iMsgIndex]['medias'][] = $asMedia; } } //Add Project Last Update $asLastUpdate = array(); $this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate()); return self::getJsonResult(true, '', array( 'messages' => $asMessages, 'maps' => $this->oProject->getMaps(), 'last_update' => $asLastUpdate )); } public function subscribe($sEmail) { $asResult = $this->oUser->addUser($sEmail, $this->oLang->getLanguage(), date_default_timezone_get()); $asUserInfo = $this->oUser->getUserInfo(); //Send Confirmation Email if($asResult['result'] && $asResult['desc']=='lang:nl_subscribed') { $oConfEmail = new Email($this->asContext['serv_name'], 'email_conf'); $oConfEmail->setDestInfo($asUserInfo); $oConfEmail->send(); } return self::getJsonResult($asResult['result'], $asResult['desc'], $asUserInfo); } public function unsubscribe() { $asResult = $this->oUser->removeUser(); return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']); } public function unsubscribeFromEmail($iUserId) { $this->oUser->setUserId($iUserId); $this->oLang->setLanguage($this->oUser->getLang(), self::DEFAULT_LANG); $asResult = $this->oUser->removeUser(); $sDesc = explode(':', $asResult['desc'])[1]; return $this->oLang->getTranslation($sDesc); } private function getSpotMessages() { $asMessages = array(); //Get messages from all feeds belonging to the project $asFeeds = $this->oProject->getFeedIds(); foreach($asFeeds as $iFeedId) { $oFeed = new Feed($this->oDb, $iFeedId); $asMessages = $oFeed->getMessages($this->oProject->getActivePeriod()); foreach($asMessages as &$asMessage) { $asMessage['latitude'] = floatval($asMessage['latitude']); $asMessage['longitude'] = floatval($asMessage['longitude']); $asMessage['lat_dms'] = self::decToDms($asMessage['latitude'], 'lat'); $asMessage['lon_dms'] = self::decToDms($asMessage['longitude'], 'lon'); $this->addTimeStamp($asMessage, $asMessage['unix_time'], $asMessage['timezone']); } } //Sort chronologically usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); //Add Display ID $asSortedMessages = array_values($asMessages); foreach($asSortedMessages as $iIndex=>&$asSortedMessage) $asSortedMessage['displayed_id'] = $iIndex + 1; return $asSortedMessages; } /** * Get valid medias based on $sTimeRefField: * - taken_on: Date/time on which the media was taken * - posted_on: Date/time on which the media was uploaded * @param String $sTimeRefField Field to calculate relative times * @return Array Medias info */ private function getMedias($sTimeRefField) { $asMedias = $this->oMedia->getMediasInfo(); $asValidMedias = array(); foreach($asMedias as $asMedia) { $sTimeRef = $asMedia[$sTimeRefField]; if($sTimeRef >= $this->oProject->getActivePeriod('from') && $sTimeRef <= $this->oProject->getActivePeriod('to')) { $iTimeStampTakenOn = strtotime($asMedia['taken_on']); $iTimeStampPostedOn = strtotime($asMedia['posted_on']); $asMedia['taken_on_formatted'] = $this->getTimeFormat($iTimeStampTakenOn); $asMedia['taken_on_formatted_local'] = $this->getTimeFormat($iTimeStampTakenOn, $asMedia['timezone']); $asMedia['posted_on_formatted'] = $this->getTimeFormat($iTimeStampPostedOn); $asMedia['posted_on_formatted_local'] = $this->getTimeFormat($iTimeStampPostedOn, $asMedia['timezone']); $this->addTimeStamp($asMedia, strtotime($sTimeRef), $asMedia['timezone']); $asValidMedias[] = $asMedia; } } usort($asValidMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); $asSortedMedias = array_values($asValidMedias); foreach($asSortedMedias as $iIndex=>&$asSortedMedia) $asSortedMedia['displayed_id'] = $iIndex + 1; return $asSortedMedias; } private function getPosts() { $asInfo = array( 'select' => array(Db::getFullColumnName(self::POST_TABLE, '*'), 'gravatar'), 'from' => self::POST_TABLE, 'join' => array(User::USER_TABLE => Db::getId(User::USER_TABLE)), 'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()), 'constOpe' => array(Db::getId(Project::PROJ_TABLE) => "=") ); if($this->oProject->getMode()==Project::MODE_HISTO) { $asInfo['constraint']['site_time'] = $this->oProject->getActivePeriod('to'); $asInfo['constOpe']['site_time'] = "<="; } $asPosts = $this->oDb->selectRows($asInfo); foreach($asPosts as &$asPost) { $iUnixTimeStamp = strtotime($asPost['site_time']); //assumes site timezone $asPost['formatted_name'] = Toolbox::mb_ucwords($asPost['name']); unset($asPost[Db::getId(User::USER_TABLE)]); $this->addTimeStamp($asPost, $iUnixTimeStamp, $asPost['timezone']); } usort($asPosts, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); return $asPosts; } private function addTimeStamp(&$asData, $iTime, $sTimeZone='') { $asData['unix_time'] = (int) $iTime; $asData['relative_time'] = Toolbox::getDateTimeDesc($iTime, $this->oLang->getLanguage()); $asData['formatted_time'] = $this->getTimeFormat($iTime); if($sTimeZone != '') $asData['formatted_time_local'] = $this->getTimeFormat($iTime, $sTimeZone); } public function getNewsFeed($iChunk=0, $bInternal=false) { $bHistoMode = ($this->oProject->getMode() == Project::MODE_HISTO); $asFeeds = array(); $asFeedTypes = array( 'message' => array( 'table' => Feed::MSG_TABLE, 'feed' => $this->getSpotMessages(), 'priority' => 0 ), 'media' => array( 'table' => Media::MEDIA_TABLE, 'feed' => $this->getMedias('posted_on'), 'priority' => 1 ), 'post' => array( 'table' => self::POST_TABLE, 'feed' => $this->getPosts(), 'priority' => 2 ) ); foreach($asFeedTypes as $sFeedType=>$asFeedTypeInfo) { foreach($asFeedTypeInfo['feed'] as $asFeed) { $sPriority = $asFeedTypeInfo['priority']; if($bHistoMode) $sPriority = count($asFeedTypes) - 1 - $asFeedTypeInfo['priority']; $iTableId = $asFeed[Db::getId($asFeedTypeInfo['table'])]; $iId = ($asFeed['unix_time'] * -1).'.'.$sPriority.$iTableId; $asFeeds[$iId] = $asFeed; $asFeeds[$iId]['type'] = $sFeedType; $asFeeds[$iId]['id'] = $iTableId; } } //Sort by key if($bHistoMode) krsort($asFeeds); else ksort($asFeeds); //Split chunks $asFeeds = array_slice($asFeeds, $iChunk * self::FEED_CHUNK_SIZE, self::FEED_CHUNK_SIZE); return $bInternal?$asFeeds:self::getJsonResult(true, '', $asFeeds); } public function syncMedias() { return $this->oMedia->syncFileFolder(); } public function addPost($sName, $sPost) { $asData = array( Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(), 'name' => mb_strtolower(trim($sName)), 'content' => trim($sPost), 'site_time' => date(Db::TIMESTAMP_FORMAT), //Now in Site Time 'timezone' => date_default_timezone_get() //Site Time Zone ); if($this->oUser->getUserId() > 0) $asData[Db::getId(User::USER_TABLE)] = $this->oUser->getUserId(); $iPostId = $this->oDb->insertRow(self::POST_TABLE, $asData); $this->oUser->updateNickname($sName); return self::getJsonResult(($iPostId > 0), ''); } public function upload() { $oUploader = new Uploader($this->oMedia, $this->oLang); return $oUploader->sBody; } public function addComment($iMediaId, $sComment) { $oMedia = new Media($this->oDb, $this->oProject, $iMediaId); $asResult = $oMedia->setComment($sComment); return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']); } public function getAdminSettings($sType='') { $oFeed = new Feed($this->oDb); $asData = array( 'project' => $this->oProject->getProjects(), 'feed' => $oFeed->getFeeds(), 'spot' => $oFeed->getSpots(), 'user' => $this->oUser->getActiveUsersInfo() ); foreach($asData['project'] as &$asProject) { $asProject['active_from'] = substr($asProject['active_from'], 0, 10); $asProject['active_to'] = substr($asProject['active_to'], 0, 10); } foreach($asData['user'] as &$asUser) $asUser['id'] = $asUser[Db::getId(User::USER_TABLE)]; return self::getJsonResult(true, '', $asData); } public function setAdminSettings($sType, $iId, $sField, $sValue) { $bSuccess = false; $sDesc = ''; $asResult = array(); switch($sType) { case 'project': $oProject = new Project($this->oDb, $iId); switch($sField) { case 'name': $bSuccess = $oProject->setProjectName($sValue); break; case 'codename': $bSuccess = $oProject->setProjectCodeName($sValue); break; case 'active_from': $bSuccess = $oProject->setActivePeriod($sValue.' 00:00:00', 'from'); break; case 'active_to': $bSuccess = $oProject->setActivePeriod($sValue.' 23:59:59', 'to'); break; default: $sDesc = $this->oLang->getTranslation('unknown_field', $sField); } $asResult = $oProject->getProject(); $asResult['active_from'] = substr($asResult['active_from'], 0, 10); $asResult['active_to'] = substr($asResult['active_to'], 0, 10); break; case 'feed': $oFeed = new Feed($this->oDb, $iId); switch($sField) { case 'ref_feed_id': $bSuccess = $oFeed->setRefFeedId($sValue); break; case 'id_spot': $bSuccess = $oFeed->setSpotId($sValue); break; case 'id_project': $bSuccess = $oFeed->setProjectId($sValue); break; default: $sDesc = $this->oLang->getTranslation('unknown_field', $sField); } $asResult = $oFeed->getFeed(); break; } if(!$bSuccess && $sDesc=='') $sDesc = Mask::LANG_PREFIX.'error_commit_db'; return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult))); } public function delAdminSettings($sType, $iId) { $bSuccess = false; $sDesc = ''; switch($sType) { case 'project': $oProject = new Project($this->oDb, $iId); $asResult = $oProject->delete(); $sDesc = $asResult['project'][0]['desc']; break; case 'feed': $oFeed = new Feed($this->oDb, $iId); $asResult = array('feed'=>array($oFeed->delete())); $sDesc = $asResult['feed'][0]['desc']; break; } $bSuccess = ($sDesc==''); return self::getJsonResult($bSuccess, $sDesc, $asResult); } public function createProject() { $oProject = new Project($this->oDb); $iNewProjectId = $oProject->createProjectId(); $oFeed = new Feed($this->oDb); $oFeed->createFeedId($iNewProjectId); return self::getJsonResult($iNewProjectId>0, '', array( 'project' => array($oProject->getProject()), 'feed' => array($oFeed->getFeed()) )); } public static function decToDms($dValue, $sType) { if($sType=='lat') $sDirection = ($dValue >= 0)?'N':'S'; //Latitude else $sDirection = ($dValue >= 0)?'E':'W'; //Longitude $dLeft = abs($dValue); //Degrees $iDegree = floor($dLeft); $dLeft -= $iDegree; //Minutes $iMinute = floor($dLeft * 60); $dLeft -= $iMinute / 60; //Seconds $fSecond = round($dLeft * 3600, 1); return $iDegree.'°'. self::getNumberWithLeadingZeros($iMinute, 2, 0)."'". self::getNumberWithLeadingZeros($fSecond, 2, 1).'"'. $sDirection; } public static function getNumberWithLeadingZeros($fValue, $iNbLeadingZeros, $iNbDigits){ $sDecimalSeparator = "."; if($iNbDigits > 0) $iNbLeadingZeros += mb_strlen($sDecimalSeparator) + $iNbDigits; $sPattern = '%0'.$iNbLeadingZeros.$sDecimalSeparator.$iNbDigits.'f'; return sprintf($sPattern, $fValue); } public function getTimeFormat($iTime, $sTimeZone='') { if($sTimeZone=='') $sTimeZone = date_default_timezone_get(); $oDate = new \DateTime('@'.$iTime); $oDate->setTimezone(new \DateTimeZone($sTimeZone)); $sDate = $oDate->format('d/m/Y'); $sTime = $oDate->format('H:i'); return $this->oLang->getTranslation('date_time', array($sDate, $sTime)); } public static function getTimeZoneFromDate($sDate) { $sTimeZone = null; preg_match('/(?(\+|\-)\d{2}:?(\d{2}|))$/', $sDate, $asMatch); if(array_key_exists('timezone', $asMatch)) { $sTimeZone = $asMatch['timezone']; //Complete short form: +12 => +1200 if(strlen($sTimeZone) == 3) $sTimeZone .= '00'; //Add colon: +1200 => +12:00 if(!strpos($sTimeZone, ':')) $sTimeZone = substr_replace($sTimeZone, ':', 3, 0); } return $sTimeZone; } }