diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index 2c8cd839c..91736f51a 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -24,12 +24,14 @@ require_once "FileIO.php"; require_once "OsPath.php"; require_once "Database.php"; require_once "ProvisioningHelper.php"; +require_once "GoogleAnalytics.php"; require_once "Timezone.php"; require_once "Auth.php"; require_once __DIR__.'/forms/helpers/ValidationTypes.php'; require_once __DIR__.'/forms/helpers/CustomDecorators.php'; require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php'; require_once __DIR__.'/controllers/plugins/Maintenance.php'; +require_once __DIR__.'/controllers/plugins/ConversionTracking.php'; require_once __DIR__.'/modules/rest/controllers/ShowImageController.php'; require_once __DIR__.'/modules/rest/controllers/MediaController.php'; @@ -37,7 +39,7 @@ require_once (APPLICATION_PATH."/logging/Logging.php"); Logging::setLogPath('/var/log/airtime/zendphp.log'); // We need to manually route because we can't load Zend without the database being initialized first. -if (array_key_exists("REQUEST_URI", $_SERVER) && (strpos($_SERVER["REQUEST_URI"], "/provisioning/create") !== false)) { +if (array_key_exists("REQUEST_URI", $_SERVER) && (stripos($_SERVER["REQUEST_URI"], "/provisioning/create") !== false)) { $provisioningHelper = new ProvisioningHelper($CC_CONFIG["apiKey"][0]); $provisioningHelper->createAction(); die(); @@ -52,6 +54,8 @@ Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance()); $front = Zend_Controller_Front::getInstance(); $front->registerPlugin(new RabbitMqPlugin()); +$front->registerPlugin(new Zend_Controller_Plugin_ConversionTracking()); +$front->throwExceptions(false); //localization configuration Application_Model_Locale::configureLocalization(); diff --git a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php index e2b6a1ba6..270015ae0 100644 --- a/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php +++ b/airtime_mvc/application/cloud_storage/Amazon_S3StorageBackend.php @@ -9,30 +9,76 @@ class Amazon_S3StorageBackend extends StorageBackend { private $s3Client; - + private $proxyHost; + public function Amazon_S3StorageBackend($securityCredentials) { $this->setBucket($securityCredentials['bucket']); $this->setAccessKey($securityCredentials['api_key']); $this->setSecretKey($securityCredentials['api_key_secret']); - $this->s3Client = S3Client::factory(array( + $s3Options = array( 'key' => $securityCredentials['api_key'], 'secret' => $securityCredentials['api_key_secret'], 'region' => $securityCredentials['region'] - )); - } + ); + if (array_key_exists("proxy_host", $securityCredentials)) { + $s3Options = array_merge($s3Options, array( + //'base_url' => "http://" . $securityCredentials['proxy_host'], + 'base_url' => "http://s3.amazonaws.com", + 'scheme' => "http", + //'force_path_style' => true, + 'signature' => 'v4' + )); + $this->proxyHost = $securityCredentials['proxy_host']; + } + $this->s3Client = S3Client::factory($s3Options); + } + public function getAbsoluteFilePath($resourceId) { return $this->s3Client->getObjectUrl($this->getBucket(), $resourceId); } - public function getSignedURL($resourceId) + /** Returns a signed download URL from Amazon S3, expiring in 60 minutes */ + public function getDownloadURLs($resourceId) { - return $this->s3Client->getObjectUrl($this->getBucket(), $resourceId, '+60 minutes'); + $urls = array(); + + $signedS3Url = $this->s3Client->getObjectUrl($this->getBucket(), $resourceId, '+60 minutes'); + + + //If we're using the proxy cache, we need to modify the request URL after it has + //been generated by the above. (The request signature must be for the amazonaws.com, + //not our proxy, since the proxy translates the host back to amazonaws.com) + if ($this->proxyHost) { + $p = parse_url($signedS3Url); + $p["host"] = $this->getBucket() . "." . $this->proxyHost; + $p["scheme"] = "http"; + //If the path contains the bucket name (which is the case with HTTPS requests to Amazon), + //we need to strip that part out, since we're forcing everything to HTTP. The Amazon S3 + //URL convention for HTTP is to prepend the bucket name to the hostname instead of having + //it in the path. + //eg. http://bucket.s3.amazonaws.com/ instead of https://s3.amazonaws.com/bucket/ + if (strpos($p["path"], $this->getBucket()) == 1) { + $p["path"] = substr($p["path"], 1 + strlen($this->getBucket())); + } + $proxyUrl = $p["scheme"] . "://" . $p["host"] . $p["path"] . "?" . $p["query"]; + //Add this proxy cache URL to the list of download URLs. + array_push($urls, $proxyUrl); + } + + //Add the direct S3 URL to the list (as a fallback) + array_push($urls, $signedS3Url); + + //http_build_url() would be nice to use but it requires pecl_http :-( + + //Logging::info($url); + + return $urls; } - + public function deletePhysicalFile($resourceId) { $bucket = $this->getBucket(); @@ -53,14 +99,30 @@ class Amazon_S3StorageBackend extends StorageBackend // Records in the database will remain in case we have to restore the files. public function deleteAllCloudFileObjects() { - $this->s3Client->deleteMatchingObjects( - $bucket = $this->getBucket(), - $prefix = $this->getFilePrefix()); + $bucket = $this->getBucket(); + $prefix = $this->getFilePrefix(); + + //Add a trailing slash in for safety + //(so that deleting /13/413 doesn't delete /13/41313 !) + $prefix = $prefix . "/"; + + //Do a bunch of safety checks to ensure we don't delete more than we intended. + //An valid prefix is like "12/4312" for instance 4312. + $slashPos = strpos($prefix, "/"); + if (($slashPos === FALSE) || //Slash must exist + ($slashPos != 2) || //Slash must be the third character + (strlen($prefix) <= $slashPos) || //String must have something after the first slash + (substr_count($prefix, "/") != 2)) //String must have two slashes + { + throw new Exception("Invalid file prefix in " . __FUNCTION__); } - + $this->s3Client->deleteMatchingObjects($bucket, $prefix); + } + public function getFilePrefix() { $hostingId = Billing::getClientInstanceId(); - return substr($hostingId, -2)."/".$hostingId; + $filePrefix = substr($hostingId, -2)."/".$hostingId; + return $filePrefix; } } diff --git a/airtime_mvc/application/cloud_storage/FileStorageBackend.php b/airtime_mvc/application/cloud_storage/FileStorageBackend.php index 81effde71..e7a87147e 100644 --- a/airtime_mvc/application/cloud_storage/FileStorageBackend.php +++ b/airtime_mvc/application/cloud_storage/FileStorageBackend.php @@ -13,7 +13,7 @@ class FileStorageBackend extends StorageBackend return $resourceId; } - public function getSignedURL($resourceId) + public function getDownloadURLs($resourceId) { return ""; } diff --git a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php index 4df1e19f7..d99c62eef 100644 --- a/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php +++ b/airtime_mvc/application/cloud_storage/ProxyStorageBackend.php @@ -38,9 +38,9 @@ class ProxyStorageBackend extends StorageBackend return $this->storageBackend->getAbsoluteFilePath($resourceId); } - public function getSignedURL($resourceId) + public function getDownloadURLs($resourceId) { - return $this->storageBackend->getSignedURL($resourceId); + return $this->storageBackend->getDownloadURLs($resourceId); } public function deletePhysicalFile($resourceId) diff --git a/airtime_mvc/application/cloud_storage/StorageBackend.php b/airtime_mvc/application/cloud_storage/StorageBackend.php index 028534e61..f0d58ba42 100644 --- a/airtime_mvc/application/cloud_storage/StorageBackend.php +++ b/airtime_mvc/application/cloud_storage/StorageBackend.php @@ -15,7 +15,7 @@ abstract class StorageBackend /** Returns the file object's signed URL. The URL must be signed since they * privately stored on the storage backend. */ - abstract public function getSignedURL($resourceId); + abstract public function getDownloadURLs($resourceId); /** Deletes the file from the storage backend. */ abstract public function deletePhysicalFile($resourceId); diff --git a/airtime_mvc/application/common/FileIO.php b/airtime_mvc/application/common/FileIO.php index 2c7724757..e921ca3c1 100644 --- a/airtime_mvc/application/common/FileIO.php +++ b/airtime_mvc/application/common/FileIO.php @@ -10,7 +10,7 @@ class Application_Common_FileIO * * This HTTP_RANGE compatible read file function is necessary for allowing streaming media to be skipped around in. * - * @param string $filePath - the full filepath pointing to the location of the file + * @param string $filePath - the full filepath or URL pointing to the location of the file * @param string $mimeType - the file's mime type. Defaults to 'audio/mp3' * @param integer $size - the file size, in bytes * @return void @@ -22,8 +22,7 @@ class Application_Common_FileIO { $fm = @fopen($filePath, 'rb'); if (!$fm) { - header ("HTTP/1.1 505 Internal server error"); - return; + throw new FileNotFoundException($filePath); } //Note that $size is allowed to be zero. If that's the case, it means we don't @@ -36,6 +35,8 @@ class Application_Common_FileIO $begin = 0; $end = $size - 1; + ob_start(); //Must start a buffer here for these header() functions + if (isset($_SERVER['HTTP_RANGE'])) { if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) { $begin = intval($matches[1]); @@ -51,6 +52,7 @@ class Application_Common_FileIO header('HTTP/1.1 200 OK'); } header("Content-Type: $mimeType"); + header("Content-Transfer-Encoding: binary"); header('Cache-Control: public, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Accept-Ranges: bytes'); @@ -60,12 +62,13 @@ class Application_Common_FileIO header("Content-Range: bytes $begin-$end/$size"); } } - header("Content-Transfer-Encoding: binary"); //We can have multiple levels of output buffering. Need to //keep looping until all have been disabled!!! //http://www.php.net/manual/en/function.ob-end-flush.php - while (@ob_end_flush()); + while (ob_get_level() > 0) { + ob_end_flush(); + } // NOTE: We can't use fseek here because it does not work with streams // (a.k.a. Files stored in the cloud) diff --git a/airtime_mvc/application/common/GoogleAnalytics.php b/airtime_mvc/application/common/GoogleAnalytics.php new file mode 100644 index 000000000..00a95a51e --- /dev/null +++ b/airtime_mvc/application/common/GoogleAnalytics.php @@ -0,0 +1,92 @@ +sub($trialDuration); + $interval = $today->diff($accountCreationDate); + $accountDuration = $interval->days; + } + + $code = "$( document ).ready(function() { + dataLayer.push({ + 'UserID': '" . $clientId . "', + 'Customer': 'Customer', + 'PlanType': '" . $plan . "', + 'Trial': '" . $isTrial . "', + 'AccountDuration': '" . strval($accountDuration) . "' + }); + });"; + //No longer sending these variables because we used to make a query to WHMCS + //to fetch them, which was slow. + // 'ZipCode': '" . $postcode . "', + // 'Country': '" . $country . "', + + } catch (Exception $e) { + Logging::error($e); + return ""; + } + return $code; + } + + /** Generate the JavaScript snippet that logs a trial to paid conversion */ + public static function generateConversionTrackingJavaScript() + { + $newPlan = Application_Model_Preference::GetPlanLevel(); + $oldPlan = Application_Model_Preference::GetOldPlanLevel(); + + $code = "dataLayer.push({'event': 'Conversion', + 'Conversion': 'Trial to Paid', + 'Old Plan' : '$oldPlan', + 'New Plan' : '$newPlan'});"; + return $code; + } + + /** Return true if the user used to be on a trial plan and was just converted to a paid plan. */ + public static function didPaidConversionOccur($request) + { + $userInfo = Zend_Auth::getInstance()->getStorage()->read(); + if ($userInfo) { + $user = new Application_Model_User($userInfo->id); + } else { + return; + } + + $oldPlan = Application_Model_Preference::GetOldPlanLevel(); + + if ($user->isSuperAdmin() && + !$user->isSourcefabricAdmin() && + $request->getControllerKey() !== "thank-you") + { + //Only tracking trial->paid conversions for now. + if ($oldPlan == "trial") + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/airtime_mvc/application/common/HTTPHelper.php b/airtime_mvc/application/common/HTTPHelper.php index 177892e1f..d6f1d6820 100644 --- a/airtime_mvc/application/common/HTTPHelper.php +++ b/airtime_mvc/application/common/HTTPHelper.php @@ -27,7 +27,7 @@ class Application_Common_HTTPHelper if (empty($baseDir)) { $baseDir = "/"; } - if ($baseDir[0] != "") { + if ($baseDir[0] != "/") { $baseDir = "/" . $baseDir; } diff --git a/airtime_mvc/application/common/ProvisioningHelper.php b/airtime_mvc/application/common/ProvisioningHelper.php index 48bbddc8c..580c20e52 100644 --- a/airtime_mvc/application/common/ProvisioningHelper.php +++ b/airtime_mvc/application/common/ProvisioningHelper.php @@ -10,6 +10,7 @@ class ProvisioningHelper // Parameter values private $dbuser, $dbpass, $dbname, $dbhost, $dbowner, $apikey; private $instanceId; + private $station_name, $description; public function __construct($apikey) { @@ -34,34 +35,37 @@ class ProvisioningHelper try { $this->parsePostParams(); - + //For security, the Airtime Pro provisioning system creates the database for the user. - $this->setNewDatabaseConnection(); + if ($this->dbhost && !empty($this->dbhost)) { + $this->setNewDatabaseConnection(); - //if ($this->checkDatabaseExists()) { - // throw new Exception("ERROR: Airtime database already exists"); - //} + //if ($this->checkDatabaseExists()) { + // throw new Exception("ERROR: Airtime database already exists"); + //} - if (!$this->checkDatabaseExists()) { - throw new Exception("ERROR: $this->dbname database does not exist."); - } + if (!$this->checkDatabaseExists()) { + throw new Exception("ERROR: $this->dbname database does not exist."); + } - //We really want to do this check because all the Propel-generated SQL starts with "DROP TABLE IF EXISTS". - //If we don't check, then a second call to this API endpoint would wipe all the tables! - if ($this->checkTablesExist()) { - throw new Exception("ERROR: airtime tables already exists"); + //We really want to do this check because all the Propel-generated SQL starts with "DROP TABLE IF EXISTS". + //If we don't check, then a second call to this API endpoint would wipe all the tables! + if ($this->checkTablesExist()) { + throw new Exception("ERROR: airtime tables already exists"); + } + + $this->createDatabaseTables(); + $this->initializeMusicDirsTable($this->instanceId); } //$this->createDatabase(); //All we need to do is create the database tables. - $this->createDatabaseTables(); - $this->initializeMusicDirsTable($this->instanceId); + $this->initializePrefs(); } catch (Exception $e) { http_response_code(400); - Logging::error($e->getMessage() - ); + Logging::error($e->getMessage()); echo $e->getMessage() . PHP_EOL; return; } @@ -94,6 +98,7 @@ class ProvisioningHelper // Result is either boolean FALSE (no table found) or PDOStatement Object (table found) return $result !== FALSE; } + private function parsePostParams() { $this->dbuser = $_POST['dbuser']; @@ -102,6 +107,9 @@ class ProvisioningHelper $this->dbhost = $_POST['dbhost']; $this->dbowner = $_POST['dbowner']; $this->instanceId = $_POST['instanceid']; + + $this->station_name = $_POST['station_name']; + $this->description = $_POST['description']; } /** @@ -111,9 +119,9 @@ class ProvisioningHelper private function setNewDatabaseConnection() { self::$dbh = new PDO("pgsql:host=" . $this->dbhost - . ";dbname=" . $this->dbname - . ";port=5432" . ";user=" . $this->dbuser - . ";password=" . $this->dbpass); + . ";dbname=" . $this->dbname + . ";port=5432" . ";user=" . $this->dbuser + . ";password=" . $this->dbpass); //Turn on PDO exceptions because they're off by default. //self::$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $err = self::$dbh->errorInfo(); @@ -130,8 +138,8 @@ class ProvisioningHelper { Logging::info("Creating database..."); $statement = self::$dbh->prepare("CREATE DATABASE " . pg_escape_string($this->dbname) - . " WITH ENCODING 'UTF8' TEMPLATE template0" - . " OWNER " . pg_escape_string($this->dbowner)); + . " WITH ENCODING 'UTF8' TEMPLATE template0" + . " OWNER " . pg_escape_string($this->dbowner)); if (!$statement->execute()) { throw new Exception("ERROR: Failed to create Airtime database"); } @@ -182,5 +190,16 @@ class ProvisioningHelper $musicDir->save(); } + /** + * Initialize preference values passed from the dashboard (if any exist) + */ + private function initializePrefs() { + if ($this->station_name) { + Application_Model_Preference::SetStationName($this->station_name); + } + if ($this->description) { + Application_Model_Preference::SetStationDescription($this->description); + } + } } diff --git a/airtime_mvc/application/configs/ACL.php b/airtime_mvc/application/configs/ACL.php index 8043a7e76..883656fd7 100644 --- a/airtime_mvc/application/configs/ACL.php +++ b/airtime_mvc/application/configs/ACL.php @@ -36,6 +36,7 @@ $ccAcl->add(new Zend_Acl_Resource('library')) ->add(new Zend_Acl_Resource('rest:media')) ->add(new Zend_Acl_Resource('rest:show-image')) ->add(new Zend_Acl_Resource('billing')) + ->add(new Zend_Acl_Resource('thank-you')) ->add(new Zend_Acl_Resource('provisioning')); /** Creating permissions */ @@ -69,6 +70,7 @@ $ccAcl->allow('G', 'index') ->allow('A', 'user') ->allow('A', 'systemstatus') ->allow('A', 'preference') + ->allow('S', 'thank-you') ->allow('S', 'billing'); diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index eae99d778..f89a5472e 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -44,6 +44,13 @@ class Config { $CC_CONFIG['dev_env'] = 'production'; } + //Backported static_base_dir default value into saas for now. + if (array_key_exists('static_base_dir', $values['general'])) { + $CC_CONFIG['staticBaseDir'] = $values['general']['static_base_dir']; + } else { + $CC_CONFIG['staticBaseDir'] = '/'; + } + // Parse separate conf file for cloud storage values $cloudStorageConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/cloud_storage.conf"; if (!file_exists($cloudStorageConfig)) { diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index c90b41c22..a233a0089 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -1073,7 +1073,9 @@ class ApiController extends Zend_Controller_Action $dir->getId(),$all=false); foreach ($files as $f) { // if the file is from this mount - if (substr($f->getFilePath(), 0, strlen($rd)) === $rd) { + $filePaths = $f->getFilePaths(); + $filePath = $filePaths[0]; + if (substr($filePath, 0, strlen($rd)) === $rd) { $f->delete(); } } diff --git a/airtime_mvc/application/controllers/ErrorController.php b/airtime_mvc/application/controllers/ErrorController.php index 70829db63..8a62d9ea6 100644 --- a/airtime_mvc/application/controllers/ErrorController.php +++ b/airtime_mvc/application/controllers/ErrorController.php @@ -1,26 +1,40 @@ view->layout()->disableLayout(); + $this->setupCSS(); + + } + + public function errorAction() { $errors = $this->_getParam('error_handler'); - switch ($errors->type) { - case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE: - case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER: - case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION: + if ($errors) { + // log error message and stack trace + Logging::error($errors->exception->getMessage()); + Logging::error($errors->exception->getTraceAsString()); - // 404 error -- controller or action not found - $this->getResponse()->setHttpResponseCode(404); - $this->view->message = _('Page not found'); - break; - default: - // application error - $this->getResponse()->setHttpResponseCode(500); - $this->view->message = _('Application error'); - break; + switch ($errors->type) { + case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE : + case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER : + $this->error404Action(); + break; + case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION : + $this->error400Action(); + break; + default : + $this->error500Action(); + break; + } + } else { + $exceptions = $this->_getAllParams(); + Logging::error($exceptions); + $this->error500Action(); + return; } // Log exception, if logger available @@ -33,11 +47,17 @@ class ErrorController extends Zend_Controller_Action $this->view->exception = $errors->exception; } - $this->view->request = $errors->request; + $this->view->request = $errors->request; } - public function getLog() + private function setupCSS() { + $CC_CONFIG = Config::getConfig(); + $staticBaseDir = Application_Common_OsPath::formatDirectoryWithDirectorySeparators($CC_CONFIG['staticBaseDir']); + $this->view->headLink()->appendStylesheet($staticBaseDir . 'css/styles.css?' . $CC_CONFIG['airtime_version']); + } + + public function getLog() { $bootstrap = $this->getInvokeArg('bootstrap'); if (!$bootstrap->hasPluginResource('Log')) { return false; @@ -47,9 +67,43 @@ class ErrorController extends Zend_Controller_Action return $log; } - public function deniedAction() - { - // action body + /** + * 404 error - route or controller + */ + public function error404Action() { + $this->_helper->viewRenderer('error-404'); + $this->getResponse()->setHttpResponseCode(404); + $this->view->message = _('Page not found.'); } + /** + * 400 error - no such action + */ + public function error400Action() { + $this->_helper->viewRenderer('error-400'); + $this->getResponse()->setHttpResponseCode(400); + $this->view->message = _('The requested action is not supported.'); + + } + + /** + * 403 error - permission denied + */ + public function error403Action() { + + $this->_helper->viewRenderer('error-403'); + $this->getResponse()->setHttpResponseCode(403); + $this->view->message = _('You do not have permission to access this resource.'); + } + + /** + * 500 error - internal server error + */ + public function error500Action() { + + $this->_helper->viewRenderer('error-500'); + + $this->getResponse()->setHttpResponseCode(500); + $this->view->message = _('An internal application error has occurred.'); + } } diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index 5bafc1438..a42170d6d 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -356,6 +356,8 @@ class LibraryController extends Zend_Controller_Action $res = $file->delete(); } catch (FileNoPermissionException $e) { $message = $noPermissionMsg; + } catch (DeleteScheduledFileException $e) { + $message = _("Could not delete file because it is scheduled in the future."); } catch (Exception $e) { //could throw a scheduled in future exception. $message = _("Could not delete file(s)."); diff --git a/airtime_mvc/application/controllers/ProvisioningController.php b/airtime_mvc/application/controllers/ProvisioningController.php index fc0c28cbb..4aa02fa39 100644 --- a/airtime_mvc/application/controllers/ProvisioningController.php +++ b/airtime_mvc/application/controllers/ProvisioningController.php @@ -18,6 +18,40 @@ class ProvisioningController extends Zend_Controller_Action * */ + /** + * Endpoint to change Airtime preferences remotely. + * Mainly for use with the dashboard right now. + */ + public function changeAction() { + $this->view->layout()->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); + + if (!RestAuth::verifyAuth(true, false, $this)) { + return; + } + + try { + // This is hacky and should be genericized + if ($_POST['station_name']) { + Application_Model_Preference::SetStationName($_POST['station_name']); + } + if ($_POST['description']) { + Application_Model_Preference::SetStationDescription($_POST['description']); + } + } catch (Exception $e) { + $this->getResponse() + ->setHttpResponseCode(400) + ->appendBody("ERROR: " . $e->getMessage()); + Logging::error($e->getMessage()); + echo $e->getMessage() . PHP_EOL; + return; + } + + $this->getResponse() + ->setHttpResponseCode(200) + ->appendBody("OK"); + } + /** * Delete the Airtime Pro station's files from Amazon S3 */ @@ -31,12 +65,12 @@ class ProvisioningController extends Zend_Controller_Action } $CC_CONFIG = Config::getConfig(); - + foreach ($CC_CONFIG["supportedStorageBackends"] as $storageBackend) { $proxyStorageBackend = new ProxyStorageBackend($storageBackend); $proxyStorageBackend->deleteAllCloudFileObjects(); } - + $this->getResponse() ->setHttpResponseCode(200) ->appendBody("OK"); diff --git a/airtime_mvc/application/controllers/ShowbuilderController.php b/airtime_mvc/application/controllers/ShowbuilderController.php index 65c2bb355..075281761 100644 --- a/airtime_mvc/application/controllers/ShowbuilderController.php +++ b/airtime_mvc/application/controllers/ShowbuilderController.php @@ -35,7 +35,7 @@ class ShowbuilderController extends Zend_Controller_Action $user = Application_Model_User::GetCurrentUser(); $userType = $user->getType(); $this->view->headScript()->appendScript("localStorage.setItem( 'user-type', '$userType' );"); - $this->view->headScript()->appendScript($this->generateGoogleTagManagerDataLayerJavaScript()); + $this->view->headScript()->appendScript(Application_Common_GoogleAnalytics::generateGoogleTagManagerDataLayerJavaScript()); $this->view->headScript()->appendFile($baseUrl.'js/contextmenu/jquery.contextMenu.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/datatables/js/jquery.dataTables.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); @@ -369,104 +369,5 @@ class ShowbuilderController extends Zend_Controller_Action throw new Exception("this controller is/was a no-op please fix your code"); } - - /** Returns a string containing the JavaScript code to pass some billing account info - * into Google Tag Manager / Google Analytics, so we can track things like the plan type. - */ - private static function generateGoogleTagManagerDataLayerJavaScript() - { - $code = ""; - - try - { - $accessKey = $_SERVER["WHMCS_ACCESS_KEY"]; - $username = $_SERVER["WHMCS_USERNAME"]; - $password = $_SERVER["WHMCS_PASSWORD"]; - $url = "https://account.sourcefabric.com/includes/api.php?accesskey=" . $accessKey; # URL to WHMCS API file goes here - - $postfields = array(); - $postfields["username"] = $username; - $postfields["password"] = md5($password); - $postfields["action"] = "getclientsdetails"; - $postfields["stats"] = true; - $postfields["clientid"] = Application_Model_Preference::GetClientId(); - $postfields["responsetype"] = "json"; - - $query_string = ""; - foreach ($postfields AS $k=>$v) $query_string .= "$k=".urlencode($v)."&"; - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 ); // WHMCS IP whitelist doesn't support IPv6 - curl_setopt($ch, CURLOPT_TIMEOUT, 5); //Aggressive 5 second timeout - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $query_string); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - $jsondata = curl_exec($ch); - if (curl_error($ch)) { - //die("Connection Error: ".curl_errno($ch).' - '.curl_error($ch)); - throw new Exception("WHMCS server down or invalid request."); - } - curl_close($ch); - - $arr = json_decode($jsondata); # Decode JSON String - - if ($arr->result !== "success") { - Logging::warn("WHMCS API call failed in " . __FUNCTION__); - return; - } - - $client = $arr->client; - $stats = $arr->stats; - $currencyCode = $client->currency_code; - //$incomeCents = NumberFormatter::parseCurrency($stats->income, $currencyCode); - - $isTrial = true; - if (strpos($stats->income, "0.00") === FALSE) { - $isTrial = false; - } - /* - if ($incomeCents > 0) { - $isTrial = false; - }*/ - $plan = Application_Model_Preference::GetPlanLevel(); - $country = $client->country; - $postcode = $client->postcode; - - //Figure out how long the customer has been around using a mega hack. - //(I'm avoiding another round trip to WHMCS for now...) - //We calculate it based on the trial end date... - $trialEndDateStr = Application_Model_Preference::GetTrialEndingDate(); - if ($trialEndDateStr == '') { - $accountDuration = 0; - } else { - $today = new DateTime(); - $trialEndDate = new DateTime($trialEndDateStr); - $trialDuration = new DateInterval("P30D"); //30 day trial duration - $accountCreationDate = $trialEndDate->sub($trialDuration); - $interval = $today->diff($accountCreationDate); - $accountDuration = $interval->days; - } - - $code = "$( document ).ready(function() { - dataLayer.push({ - 'ZipCode': '" . $postcode . "', - 'UserID': '" . $client->id . "', - 'Customer': 'Customer', - 'PlanType': '" . $plan . "', - 'Trial': '" . $isTrial . "', - 'Country': '" . $country . "', - 'AccountDuration': '" . strval($accountDuration) . "' - }); - });"; - - } - catch (Exception $e) - { - return ""; - } - return $code; - } + } diff --git a/airtime_mvc/application/controllers/ThankYouController.php b/airtime_mvc/application/controllers/ThankYouController.php new file mode 100644 index 000000000..86a57a54c --- /dev/null +++ b/airtime_mvc/application/controllers/ThankYouController.php @@ -0,0 +1,48 @@ +view->stationUrl = Application_Common_HTTPHelper::getStationUrl(); + $this->view->conversionUrl = Application_Common_HTTPHelper::getStationUrl() . 'thank-you/confirm-conversion'; + $this->view->gaEventTrackingJsCode = ""; //Google Analytics event tracking code that logs an event. + + // Embed the Google Analytics conversion tracking code if the + // user is a super admin and old plan level is set to trial. + if (Application_Common_GoogleAnalytics::didPaidConversionOccur($this->getRequest())) { + $this->view->gaEventTrackingJsCode = Application_Common_GoogleAnalytics::generateConversionTrackingJavaScript(); + } + + $csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); + $csrf_element = new Zend_Form_Element_Hidden('csrf'); + $csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label'); + $csrf_form = new Zend_Form(); + $csrf_form->addElement($csrf_element); + $this->view->form = $csrf_form; + } + + /** Confirm that a conversion was tracked. */ + public function confirmConversionAction() + { + $this->view->layout()->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); + + $current_namespace = new Zend_Session_Namespace('csrf_namespace'); + $observed_csrf_token = $this->_getParam('csrf_token'); + $expected_csrf_token = $current_namespace->authtoken; + + if($observed_csrf_token != $expected_csrf_token) { + Logging::info("Invalid CSRF token"); + return; + } + + if ($this->getRequest()->isPost()) { + Logging::info("Goal conversion from trial to paid."); + // Clear old plan level so we prevent duplicate events. + // This should only be called from AJAX. See thank-you/index.phtml + Application_Model_Preference::ClearOldPlanLevel(); + } + } +} \ No newline at end of file diff --git a/airtime_mvc/application/controllers/UpgradeController.php b/airtime_mvc/application/controllers/UpgradeController.php index 9af3dc1e9..518e173ac 100644 --- a/airtime_mvc/application/controllers/UpgradeController.php +++ b/airtime_mvc/application/controllers/UpgradeController.php @@ -20,7 +20,8 @@ class UpgradeController extends Zend_Controller_Action array_push($upgraders, new AirtimeUpgrader259()); array_push($upgraders, new AirtimeUpgrader2510()); array_push($upgraders, new AirtimeUpgrader2511()); - + array_push($upgraders, new AirtimeUpgrader2512()); + $didWePerformAnUpgrade = false; try { diff --git a/airtime_mvc/application/controllers/plugins/Acl_plugin.php b/airtime_mvc/application/controllers/plugins/Acl_plugin.php index 10910fb73..7ea1336d0 100644 --- a/airtime_mvc/application/controllers/plugins/Acl_plugin.php +++ b/airtime_mvc/application/controllers/plugins/Acl_plugin.php @@ -28,7 +28,7 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract { $this->_errorPage = array('module' => 'default', 'controller' => 'error', - 'action' => 'denied'); + 'action' => 'error'); $this->_roleName = $roleName; @@ -111,7 +111,16 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract $controller = strtolower($request->getControllerName()); Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance()); - if (in_array($controller, array("api", "auth", "locale", "upgrade", 'whmcs-login', "provisioning"))) { + if (in_array($controller, array( + "api", + "auth", + "error", + "locale", + "upgrade", + 'whmcs-login', + "provisioning" + ))) + { $this->setRoleName("G"); } elseif (!Zend_Auth::getInstance()->hasIdentity()) { diff --git a/airtime_mvc/application/controllers/plugins/ConversionTracking.php b/airtime_mvc/application/controllers/plugins/ConversionTracking.php new file mode 100644 index 000000000..09904828d --- /dev/null +++ b/airtime_mvc/application/controllers/plugins/ConversionTracking.php @@ -0,0 +1,21 @@ +getControllerName() != 'thank-you') + { + $request->setModuleName('default') + ->setControllerName('thank-you') + ->setActionName('index') + ->setDispatched(true); + } + } + } + +} \ No newline at end of file diff --git a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.12/upgrade.sql b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.12/upgrade.sql new file mode 100644 index 000000000..92ca30ade --- /dev/null +++ b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.12/upgrade.sql @@ -0,0 +1,2 @@ +ALTER TABLE cc_show ALTER COLUMN description TYPE varchar(8192); +ALTER TABLE cc_show_instances ALTER COLUMN description TYPE varchar(8192); diff --git a/airtime_mvc/application/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml index 709d32b26..93c758c28 100644 --- a/airtime_mvc/application/layouts/scripts/layout.phtml +++ b/airtime_mvc/application/layouts/scripts/layout.phtml @@ -38,7 +38,6 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= partial('partialviews/trialBox.phtml', array("is_trial"=>$this->isTrial(), "trial_remain"=> $this->trialRemaining())) ?>