vanilla-application-yaga/controllers/class.yagacontroller.php

575 lines
16 KiB
PHP

<?php if(!defined('APPLICATION')) exit();
/* Copyright 2013 Zachary Doll */
/**
* Manage the yaga application including configuration and import/export
*
* @since 1.0
* @package Yaga
*/
class YagaController extends DashboardController {
/**
* @var array These objects will be created on instantiation and available via
* $this->ObjectName
*/
public $Uses = array('Form');
/**
* Make this look like a dashboard page and add the resources
*
* @since 1.0
* @access public
*/
public function Initialize() {
parent::Initialize();
$this->Application = 'Yaga';
Gdn_Theme::Section('Dashboard');
if($this->Menu) {
$this->Menu->HighlightRoute('/yaga');
}
$this->AddSideMenu('yaga/settings');
$this->AddCssFile('yaga.css');
$this->removeCssFile('magnific-popup.css');
}
/**
* Redirect to settings by default
*
* @since 1.0
*/
public function Index() {
$this->Settings();
}
/**
* This handles all the core settings for the gamification application.
*
* @since 1.0
*/
public function Settings() {
$this->Permission('Garden.Settings.Manage');
$this->Title(T('Yaga.Settings'));
// Get list of actions from the model and pass to the view
$ConfigModule = new ConfigurationModule($this);
$ConfigModule->Initialize(array(
'Yaga.Reactions.Enabled' => array(
'LabelCode' => 'Yaga.Reactions.Use',
'Control' => 'Checkbox'
),
'Yaga.Badges.Enabled' => array(
'LabelCode' => 'Yaga.Badges.Use',
'Control' => 'Checkbox'
),
'Yaga.Ranks.Enabled' => array(
'LabelCode' => 'Yaga.Ranks.Use',
'Control' => 'Checkbox'
),
'Yaga.MenuLinks.Show' => array(
'LabelCode' => 'Yaga.MenuLinks.Show',
'Control' => 'Checkbox'
),
'Yaga.LeaderBoard.Enabled' => array(
'LabelCode' => 'Yaga.LeaderBoard.Use',
'Control' => 'Checkbox'
),
'Yaga.LeaderBoard.Limit' => array(
'LabelCode' => 'Yaga.LeaderBoard.Max',
'Control' => 'Textbox',
'Options' => array(
'Size' => 45,
'class' => 'SmallInput'
)
)
));
$this->ConfigurationModule = $ConfigModule;
$this->Render('settings');
}
/**
* Performs the necessary functions to change a backend controller into a
* frontend controller
*
* @since 1.1
*/
private function FrontendStyle() {
$this->RemoveCssFile('admin.css');
unset($this->Assets['Panel']['SideMenuModule']);
$this->AddCssFile('style.css');
$this->MasterView = 'default';
$WeeklyModule = new LeaderBoardModule();
$WeeklyModule->SlotType = 'w';
$this->AddModule($WeeklyModule);
$AllTimeModule = new LeaderBoardModule();
$this->AddModule($AllTimeModule);
}
/**
* Displays a summary of ranks currently configured on a frontend page to help
* users understand what is valued in this community
*
* @since 1.1
*/
public function Ranks() {
$this->permission('Yaga.Ranks.View');
$this->FrontendStyle();
$this->AddCssFile('ranks.css');
$this->Title(T('Yaga.Ranks.All'));
// Get list of ranks from the model and pass to the view
$this->SetData('Ranks', Yaga::RankModel()->Get());
$this->Render('ranks');
}
/**
* Displays a summary of badges currently configured on a frontend page to
* help users understand what is valued in this community.
*
* Also provides a convenience redirect to badge details
*
* @param int $BadgeID The badge ID you want to see details
* @param string $Slug The badge slug you want to see details
* @since 1.1
*/
public function Badges($BadgeID = FALSE, $Slug = NULL) {
$this->permission('Yaga.Badges.View');
$this->FrontendStyle();
$this->AddCssFile('badges.css');
$this->AddModule('BadgesModule');
if(is_numeric($BadgeID)) {
return $this->BadgeDetail($BadgeID, $Slug);
}
$this->Title(T('Yaga.Badges.All'));
// Get list of badges from the model and pass to the view
$UserID = Gdn::Session()->UserID;
$AllBadges = Yaga::BadgeModel()->GetWithEarned($UserID);
$this->SetData('Badges', $AllBadges);
$this->Render('badges');
}
/**
* Displays information about the specified badge including recent recipients
* of the badge.
*
* @param int $BadgeID
* @param string $Slug
*/
public function BadgeDetail($BadgeID, $Slug = NULL) {
$this->permission('Yaga.Badges.View');
$Badge = Yaga::BadgeModel()->GetByID($BadgeID);
if(!$Badge) {
throw NotFoundException('Badge');
}
$UserID = Gdn::Session()->UserID;
$BadgeAwardModel = Yaga::BadgeAwardModel();
$AwardCount = $BadgeAwardModel->GetCount($BadgeID);
$UserBadgeAward = $BadgeAwardModel->Exists($UserID, $BadgeID);
$RecentAwards = $BadgeAwardModel->GetRecent($BadgeID);
$this->SetData('AwardCount', $AwardCount);
$this->SetData('RecentAwards', $RecentAwards);
$this->SetData('UserBadgeAward', $UserBadgeAward);
$this->SetData('Badge', $Badge);
$this->Title(T('Yaga.Badge.View') . $Badge->Name);
$this->Render('badgedetail');
}
/**
* Import a Yaga transport file
*
* @since 1.0
*/
public function Import() {
$this->Title(T('Yaga.Import'));
$this->SetData('TransportType', 'Import');
if(!class_exists('ZipArchive')) {
$this->Form->AddError(T('Yaga.Error.TransportRequirements'));
}
if($this->Form->IsPostBack() == TRUE) {
// Handle the file upload
$Upload = new Gdn_Upload();
$TmpZip = $Upload->ValidateUpload('FileUpload', FALSE);
$ZipFile = FALSE;
if($TmpZip) {
// Generate the target name
$TargetFile = $Upload->GenerateTargetName(PATH_UPLOADS, 'zip');
$BaseName = pathinfo($TargetFile, PATHINFO_BASENAME);
// Save the uploaded zip
$Parts = $Upload->SaveAs($TmpZip, $BaseName);
$ZipFile = PATH_UPLOADS . DS . $Parts['SaveName'];
$this->SetData('TransportPath', $ZipFile);
}
$Include = $this->_FindIncludes();
if(count($Include)) {
$Info = $this->_ExtractZip($ZipFile);
$this->_ImportData($Info, $Include);
Gdn_FileSystem::RemoveFolder(PATH_UPLOADS . DS . 'import' . DS . 'yaga');
}
else {
$this->Form->AddError(T('Yaga.Error.Includes'));
}
}
if($this->Form->ErrorCount() == 0 && $this->Form->IsPostBack()) {
$this->Render('transport-success');
}
else {
$this->Render();
}
}
/**
* Create a Yaga transport file
*
* @since 1.0
*/
public function Export() {
$this->Title(T('Yaga.Export'));
$this->SetData('TransportType', 'Export');
if(!class_exists('ZipArchive')) {
$this->Form->AddError(T('Yaga.Error.TransportRequirements'));
}
if($this->Form->IsPostBack()) {
$Include = $this->_FindIncludes();
if(count($Include)) {
$Filename = $this->_ExportData($Include);
$this->SetData('TransportPath', $Filename);
}
else {
$this->Form->AddError(T('Yaga.Error.Includes'));
}
}
if($this->Form->ErrorCount() == 0 && $this->Form->IsPostBack()) {
$this->Render('transport-success');
}
else {
$this->Render();
}
}
/**
* This searches through the submitted checkboxes and constructs an array of
* Yaga sections to be included in the transport file.
*
* @return array
* @since 1.0
*/
protected function _FindIncludes() {
$FormValues = $this->Form->FormValues();
$Sections = $FormValues['Checkboxes'];
// Figure out which boxes were checked
$Include = array();
foreach($Sections as $Section) {
$Include[$Section] = $FormValues[$Section];
}
return $Include;
}
/**
* Creates a transport file for easily transferring Yaga configurations across
* installs
*
* @param array An array containing the config areas to transfer
* @param string Where to save the transport file
* @return mixed False on failure, the path to the transport file on success
* @since 1.0
*/
protected function _ExportData($Include = array(), $Path = NULL) {
$StartTime = microtime(TRUE);
$Info = new stdClass();
$Info->Version = C('Yaga.Version', '?.?');
$Info->StartDate = date('Y-m-d H:i:s');
if(is_null($Path)) {
$Path = PATH_UPLOADS . DS . 'export' . date('Y-m-d-His') . '.yaga.zip';
}
$FH = new ZipArchive();
$Images = array();
$Hashes = array();
if($FH->open($Path, ZipArchive::CREATE) !== TRUE) {
$this->Form->AddError(sprintf(T('Yaga.Error.ArchiveCreate'), $FH->getStatusString()));
return FALSE;
}
// Add configuration items
$Info->Config = 'configs.yaga';
$Configs = Gdn::Config('Yaga', array());
unset($Configs['Version']);
$ConfigData = serialize($Configs);
$FH->addFromString('configs.yaga', $ConfigData);
$Hashes[] = md5($ConfigData);
// Add actions
if($Include['Action']) {
$Info->Action = 'actions.yaga';
$Actions = Yaga::ActionModel()->Get('Sort', 'asc');
$this->SetData('ActionCount', count($Actions));
$ActionData = serialize($Actions);
$FH->addFromString('actions.yaga', $ActionData);
$Hashes[] = md5($ActionData);
}
// Add ranks and associated image
if($Include['Rank']) {
$Info->Rank = 'ranks.yaga';
$Ranks = Yaga::RankModel()->Get('Level', 'asc');
$this->SetData('RankCount', count($Ranks));
$RankData = serialize($Ranks);
$FH->addFromString('ranks.yaga', $RankData);
array_push($Images, C('Yaga.Ranks.Photo'), NULL);
$Hashes[] = md5($RankData);
}
// Add badges and associated images
if($Include['Badge']) {
$Info->Badge = 'badges.yaga';
$Badges = Yaga::BadgeModel()->Get();
$this->SetData('BadgeCount', count($Badges));
$BadgeData = serialize($Badges);
$FH->addFromString('badges.yaga', $BadgeData);
$Hashes[] = md5($BadgeData);
foreach($Badges as $Badge) {
array_push($Images, $Badge->Photo);
}
}
// Add in any images
$FilteredImages = array_filter($Images);
$ImageCount = count($FilteredImages);
$this->SetData('ImageCount', $ImageCount);
if($ImageCount > 0) {
$FH->addEmptyDir('images');
}
foreach($FilteredImages as $Image) {
if($FH->addFile('.' . $Image, 'images/' . $Image) === FALSE) {
$this->Form->AddError(sprintf(T('Yaga.Error.AddFile'), $FH->getStatusString()));
//return FALSE;
}
$Hashes[] = md5_file('.' . $Image);
}
// Save all the hashes
sort($Hashes);
$Info->MD5 = md5(implode(',', $Hashes));
$Info->EndDate = date('Y-m-d H:i:s');
$EndTime = microtime(TRUE);
$TotalTime = $EndTime - $StartTime;
$m = floor($TotalTime / 60);
$s = $TotalTime - ($m * 60);
$Info->ElapsedTime = sprintf('%02d:%02.2f', $m, $s);
$FH->setArchiveComment(serialize($Info));
if($FH->close()) {
return $Path;
}
else {
$this->Form->AddError(sprintf(T('Yaga.Error.ArchiveSave'), $FH->getStatusString()));
return FALSE;
}
}
/**
* Extract the transport file and validate
*
* @param string The transport file path
* @return boolean Whether or not the transport file was extracted successfully
* @since 1.0
*/
protected function _ExtractZip($Filename) {
if(!file_exists($Filename)) {
$this->Form->AddError(T('Yaga.Error.FileDNE'));
return FALSE;
}
$ZipFile = new ZipArchive();
$Result = $ZipFile->open($Filename);
if($Result !== TRUE) {
$this->Form->AddError(T('Yaga.Error.ArchiveOpen'));
return FALSE;
}
// Get the metadata from the comment
$Comment = $ZipFile->comment;
$MetaData = unserialize($Comment);
$Result = $ZipFile->extractTo(PATH_UPLOADS . DS . 'import' . DS . 'yaga');
if($Result !== TRUE) {
$this->Form->AddError(T('Yaga.Error.ArchiveExtract'));
return FALSE;
}
$ZipFile->close();
// Validate checksum
if($this->_ValidateChecksum($MetaData) === TRUE) {
return $MetaData;
}
else {
$this->Form->AddError(T('Yaga.Error.ArchiveChecksum'));
return FALSE;
}
}
/**
* Overwrites Yaga configurations, dumps Yaga db tables, inserts data via the
* model, and copies uploaded files to the server
*
* @param stdClass The info object read in from the archive
* @param array Which tables should be overwritten
* @return bool Pass/Fail on the import being executed. Errors can exist on the
* form with a passing return value.
* @since 1.0
*/
protected function _ImportData($Info, $Include) {
if(!$Info) {
return FALSE;
}
// Import Configs
$Configs = unserialize(file_get_contents(PATH_UPLOADS . DS . 'import' . DS . 'yaga' . DS . $Info->Config));
$Configurations = $this->_NestedToDotNotation($Configs, 'Yaga');
foreach($Configurations as $Name => $Value) {
SaveToConfig($Name, $Value);
}
// Import model data
foreach($Include as $Key => $Value) {
if($Value) {
$Data = unserialize(file_get_contents(PATH_UPLOADS . DS . 'import' . DS . 'yaga' . DS . $Info->$Key));
Gdn::SQL()->EmptyTable($Key);
$ModelName = $Key . 'Model';
$Model = Yaga::$ModelName();
foreach($Data as $Datum) {
$Model->Insert((array)$Datum);
}
$this->SetData($Key . 'Count', $Model->GetCount());
}
}
// Import uploaded files
if(Gdn_FileSystem::Copy(PATH_UPLOADS . DS . 'import' . DS . 'yaga' . DS . 'images' . DS . 'uploads' . DS, PATH_UPLOADS . DS) === FALSE) {
$this->Form->AddError(T('Yaga.Error.TransportCopy'));
}
return TRUE;
}
/**
* Converted a nest config array into an array where indexes are the configuration
* strings and the value is the value
*
* @param array The nested array
* @param string What should the configuration strings be prefixed with
* @return array
* @since 1.0
*/
protected function _NestedToDotNotation($Configs, $Prefix = '') {
$ConfigStrings = array();
foreach($Configs as $Name => $Value) {
if(is_array($Value)) {
$ConfigStrings = array_merge($ConfigStrings, $this->_NestedToDotNotation($Value, "$Prefix.$Name"));
}
else {
$ConfigStrings["$Prefix.$Name"] = $Value;
}
}
return $ConfigStrings;
}
/**
* Inspects the Yaga transport files and calculates a checksum
*
* @param stdClass The metadata object read in from the transport file
* @return boolean Whether or not the checksum is valid
* @since 1.0
*/
protected function _ValidateChecksum($MetaData) {
$Hashes = array();
// Hash the config file
$Hashes[] = md5_file(PATH_UPLOADS . DS . 'import' . DS . 'yaga' . DS . $MetaData->Config);
// Hash the data files
if(property_exists($MetaData, 'Action')) {
$Hashes[] = md5_file(PATH_UPLOADS . DS . 'import' . DS . 'yaga' . DS . $MetaData->Action);
}
if(property_exists($MetaData, 'Badge')) {
$Hashes[] = md5_file(PATH_UPLOADS . DS . 'import' . DS . 'yaga' . DS . $MetaData->Badge);
}
if(property_exists($MetaData, 'Rank')) {
$Hashes[] = md5_file(PATH_UPLOADS . DS . 'import' . DS . 'yaga' . DS . $MetaData->Rank);
}
// Hash the image files
$Files = $this->_GetFiles(PATH_UPLOADS . DS . 'import' . DS . 'yaga' . DS . 'images');
$this->SetData('ImageCount', count($Files));
foreach($Files as $File) {
$Hashes[] = md5_file($File);
}
sort($Hashes);
$CalculatedChecksum = md5(implode(',', $Hashes));
if($CalculatedChecksum != $MetaData->MD5) {
return FALSE;
}
else {
return TRUE;
}
}
/**
* Returns a list of all files in a directory, recursively (Thanks @businessdad)
*
* @param string Directory The directory to scan for files
* @return array A list of Files and, optionally, Directories.
* @since 1.0
*/
protected function _GetFiles($Directory) {
$Files = array_diff(scandir($Directory), array('.', '..'));
$Result = array();
foreach($Files as $File) {
$FileName = $Directory . '/' . $File;
if(is_dir($FileName)) {
$Result = array_merge($Result, $this->_GetFiles($FileName));
continue;
}
$Result[] = $FileName;
}
return $Result;
}
}