diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index 2ff1eb72f..1f4c66ba2 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -25,6 +25,7 @@ require_once "OsPath.php"; require_once "Database.php"; require_once "ProvisioningHelper.php"; require_once "SecurityHelper.php"; +require_once "SessionHelper.php"; require_once "GoogleAnalytics.php"; require_once "Timezone.php"; require_once "Auth.php"; @@ -36,6 +37,7 @@ require_once __DIR__.'/services/CeleryService.php'; require_once __DIR__.'/services/SoundcloudService.php'; require_once __DIR__.'/forms/helpers/ValidationTypes.php'; require_once __DIR__.'/forms/helpers/CustomDecorators.php'; +require_once __DIR__.'/controllers/plugins/PageLayoutInitPlugin.php'; require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php'; require_once __DIR__.'/controllers/plugins/Maintenance.php'; require_once __DIR__.'/controllers/plugins/ConversionTracking.php'; @@ -53,20 +55,21 @@ if (array_key_exists("REQUEST_URI", $_SERVER) && (stripos($_SERVER["REQUEST_URI" die(); } +Zend_Session::setOptions(array('strict' => true)); + + Config::setAirtimeVersion(); require_once (CONFIG_PATH . 'navigation.php'); Zend_Validate::setDefaultNamespaces("Zend"); -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(); + + /* The bootstrap class should only be used to initialize actions that return a view. Actions that return JSON will not use the bootstrap class! */ @@ -79,64 +82,6 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap $view->doctype('XHTML1_STRICT'); } - protected function _initGlobals() - { - $view = $this->getResource('view'); - $baseUrl = Application_Common_OsPath::getBaseDir(); - - $view->headScript()->appendScript("var baseUrl = '$baseUrl';"); - $this->_initTranslationGlobals($view); - - $user = Application_Model_User::GetCurrentUser(); - if (!is_null($user)) { - $userType = $user->getType(); - } else { - $userType = ""; - } - $view->headScript()->appendScript("var userType = '$userType';"); - - // Dropzone also accept file extensions and doesn't correctly extract certain mimetypes (eg. FLAC - try it), - // so we append the file extensions to the list of mimetypes and that makes it work. - $mimeTypes = FileDataHelper::getAudioMimeTypeArray(); - $fileExtensions = array_values($mimeTypes); - foreach($fileExtensions as &$extension) { - $extension = '.' . $extension; - } - $view->headScript()->appendScript("var acceptedMimeTypes = " . json_encode(array_merge(array_keys($mimeTypes), $fileExtensions)) . ";"); - } - - /** - * Create a global namespace to hold a session token for CSRF prevention - */ - protected function _initCsrfNamespace() - { - $csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); - // Check if the token exists - if (!$csrf_namespace->authtoken) { - // If we don't have a token, regenerate it and set a 1 week timeout - // Should we log the user out here if the token is expired? - $csrf_namespace->authtoken = sha1(uniqid(rand(), 1)); - $csrf_namespace->setExpirationSeconds(168 * 60 * 60); - } - - //Here we are closing the session for writing because otherwise no requests - //in this session will be handled in parallel. This gives a major boost to the perceived performance - //of the application (page load times are more consistent, no lock contention). - session_write_close(); - } - - /** - * Ideally, globals should be written to a single js file once - * from a php init function. This will save us from having to - * reinitialize them every request - */ - private function _initTranslationGlobals() - { - $view = $this->getResource('view'); - $view->headScript()->appendScript("var PRODUCT_NAME = '" . PRODUCT_NAME . "';"); - $view->headScript()->appendScript("var USER_MANUAL_URL = '" . USER_MANUAL_URL . "';"); - $view->headScript()->appendScript("var COMPANY_NAME = '" . COMPANY_NAME . "';"); - } protected function _initTasks() { /* We need to wrap this here so that we aren't checking when we're running the unit test suite @@ -149,115 +94,7 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap } } - protected function _initHeadLink() - { - $CC_CONFIG = Config::getConfig(); - $view = $this->getResource('view'); - - $baseUrl = Application_Common_OsPath::getBaseDir(); - - $view->headLink(array('rel' => 'icon', 'href' => $baseUrl . 'favicon.ico?' . $CC_CONFIG['airtime_version'], 'type' => 'image/x-icon'), 'PREPEND') - ->appendStylesheet($baseUrl . 'css/bootstrap.css?' . $CC_CONFIG['airtime_version']) - ->appendStylesheet($baseUrl . 'css/redmond/jquery-ui-1.8.8.custom.css?' . $CC_CONFIG['airtime_version']) - ->appendStylesheet($baseUrl . 'css/pro_dropdown_3.css?' . $CC_CONFIG['airtime_version']) - ->appendStylesheet($baseUrl . 'css/qtip/jquery.qtip.min.css?' . $CC_CONFIG['airtime_version']) - ->appendStylesheet($baseUrl . 'css/styles.css?' . $CC_CONFIG['airtime_version']) - ->appendStylesheet($baseUrl . 'css/masterpanel.css?' . $CC_CONFIG['airtime_version']) - ->appendStylesheet($baseUrl . 'css/tipsy/jquery.tipsy.css?' . $CC_CONFIG['airtime_version']); - } - - protected function _initHeadScript() - { - $CC_CONFIG = Config::getConfig(); - - $view = $this->getResource('view'); - - $baseUrl = Application_Common_OsPath::getBaseDir(); - - $view->headScript()->appendFile($baseUrl . 'js/libs/jquery-1.8.3.min.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/libs/jquery-ui-1.8.24.min.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/bootstrap/bootstrap.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/libs/underscore-min.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - - // ->appendFile($baseUrl . 'js/libs/jquery.stickyPanel.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/qtip/jquery.qtip.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/jplayer/jquery.jplayer.min.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/sprintf/sprintf-0.7-beta1.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/cookie/jquery.cookie.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/i18n/jquery.i18n.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'locale/general-translation-table?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'locale/datatables-translation-table?' . $CC_CONFIG['airtime_version'], 'text/javascript') - - ->appendScript("$.i18n.setDictionary(general_dict)") - ->appendScript("var baseUrl='$baseUrl'"); - - //These timezones are needed to adjust javascript Date objects on the client to make sense to the user's set timezone - //or the server's set timezone. - $serverTimeZone = new DateTimeZone(Application_Model_Preference::GetDefaultTimezone()); - $now = new DateTime("now", $serverTimeZone); - $offset = $now->format("Z") * -1; - $view->headScript()->appendScript("var serverTimezoneOffset = {$offset}; //in seconds"); - - if (class_exists("Zend_Auth", false) && Zend_Auth::getInstance()->hasIdentity()) { - $userTimeZone = new DateTimeZone(Application_Model_Preference::GetUserTimezone()); - $now = new DateTime("now", $userTimeZone); - $offset = $now->format("Z") * -1; - $view->headScript()->appendScript("var userTimezoneOffset = {$offset}; //in seconds"); - } - - //scripts for now playing bar - $view->headScript()->appendFile($baseUrl . 'js/airtime/airtime_bootstrap.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/airtime/dashboard/helperfunctions.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/airtime/dashboard/dashboard.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/airtime/dashboard/versiontooltip.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/tipsy/jquery.tipsy.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - - ->appendFile($baseUrl . 'js/airtime/common/common.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') - ->appendFile($baseUrl . 'js/airtime/common/audioplaytest.js?' . $CC_CONFIG['airtime_version'], 'text/javascript'); - - $user = Application_Model_User::getCurrentUser(); - if (!is_null($user)) { - $userType = $user->getType(); - } else { - $userType = ""; - } - - $view->headScript()->appendScript("var userType = '$userType';"); - if (array_key_exists('REQUEST_URI', $_SERVER) //Doesn't exist for unit tests - && strpos($_SERVER['REQUEST_URI'], 'Dashboard/stream-player') === false - && strpos($_SERVER['REQUEST_URI'], 'audiopreview') === false - && $_SERVER['REQUEST_URI'] != "/") { - $plan_level = strval(Application_Model_Preference::GetPlanLevel()); - // Since the Hobbyist plan doesn't come with Live Chat support, don't enable it - if (Application_Model_Preference::GetLiveChatEnabled() && $plan_level !== 'hobbyist') { - $client_id = strval(Application_Model_Preference::GetClientId()); - $station_url = $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI']; - $view->headScript()->appendScript("var livechat_client_id = '$client_id';\n" . - "var livechat_plan_type = '$plan_level';\n" . - "var livechat_station_url = 'http://$station_url';"); - $view->headScript()->appendFile($baseUrl . 'js/airtime/common/livechat.js?' . $CC_CONFIG['airtime_version'], 'text/javascript'); - } - } - - /* - if (isset($CC_CONFIG['demo']) && $CC_CONFIG['demo'] == 1) { - $view->headScript()->appendFile($baseUrl.'js/libs/google-analytics.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); - }*/ - } - - protected function _initViewHelpers() - { - $view = $this->getResource('view'); - $view->addHelperPath(APPLICATION_PATH . 'views/helpers', 'Airtime_View_Helper'); - $view->assign('suspended', (Application_Model_Preference::getProvisioningStatus() == PROVISIONING_STATUS_SUSPENDED)); - } - - protected function _initTitle() - { - $view = $this->getResource('view'); - $view->headTitle(Application_Model_Preference::GetHeadTitle()); - } protected function _initZFDebug() { @@ -303,6 +140,7 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap { $front = Zend_Controller_Front::getInstance(); $front->registerPlugin(new Zend_Controller_Plugin_Maintenance()); + $front->registerPlugin(new PageLayoutInitPlugin($this)); } } diff --git a/airtime_mvc/application/common/SecurityHelper.php b/airtime_mvc/application/common/SecurityHelper.php index 95353407e..f36ae638a 100644 --- a/airtime_mvc/application/common/SecurityHelper.php +++ b/airtime_mvc/application/common/SecurityHelper.php @@ -12,4 +12,12 @@ class SecurityHelper { } return $arr; } + + public static function verifyCSRFToken($observedToken) { + $current_namespace = new Zend_Session_Namespace('csrf_namespace'); + $observed_csrf_token = $observedToken; + $expected_csrf_token = $current_namespace->authtoken; + + return ($observed_csrf_token == $expected_csrf_token); + } } \ No newline at end of file diff --git a/airtime_mvc/application/common/SessionHelper.php b/airtime_mvc/application/common/SessionHelper.php new file mode 100644 index 000000000..badfa1924 --- /dev/null +++ b/airtime_mvc/application/common/SessionHelper.php @@ -0,0 +1,13 @@ +getStorage()->read(); return !empty($data); diff --git a/airtime_mvc/application/configs/constants.php b/airtime_mvc/application/configs/constants.php index dd421a9d9..41b5cd21a 100644 --- a/airtime_mvc/application/configs/constants.php +++ b/airtime_mvc/application/configs/constants.php @@ -18,7 +18,7 @@ define('TERMS_AND_CONDITIONS_URL' , 'http://www.sourcefabric.org/en/about/poli define('PRIVACY_POLICY_URL' , 'http://www.sourcefabric.org/en/about/policy/'); define('USER_MANUAL_URL' , 'http://sourcefabric.booktype.pro/airtime-pro-for-broadcasters'); define('ABOUT_AIRTIME_URL' , 'https://www.airtime.pro/support/'); -define('AIRTIME_TRANSIFEX_URL' , 'https://www.transifex.com/projects/p/airtime/'); +define('AIRTIME_TRANSIFEX_URL' , 'https://www.transifex.com/sourcefabric/airtime/'); define('WHMCS_PASSWORD_RESET_URL' , 'https://account.sourcefabric.com/pwreset.php'); define('SUPPORT_TICKET_URL' , 'https://sourcefabricberlin.zendesk.com/hc/en-us/requests/new'); define('UI_REVAMP_EMBED_URL' , 'https://www.youtube.com/embed/nqpNnCKGluY'); diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index d30013d8d..ca0d182ef 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -11,6 +11,8 @@ class ApiController extends Zend_Controller_Action public function init() { + + //Ignore API key and session authentication for these APIs: $ignoreAuth = array("live-info", "live-info-v2", "week-info", @@ -25,6 +27,11 @@ class ApiController extends Zend_Controller_Action "show-logo" ); + if (Zend_Session::isStarted()) { + Logging::error("Session already started for an API request. Check your code because + this will negatively impact performance."); + } + $params = $this->getRequest()->getParams(); if (!in_array($params['action'], $ignoreAuth)) { $this->checkAuth(); @@ -73,13 +80,23 @@ class ApiController extends Zend_Controller_Action $CC_CONFIG = Config::getConfig(); $api_key = $this->_getParam('api_key'); - if (!in_array($api_key, $CC_CONFIG["apiKey"]) && - is_null(Zend_Auth::getInstance()->getStorage()->read())) { - header('HTTP/1.0 401 Unauthorized'); - print _('You are not allowed to access this resource.'); - exit; + if (in_array($api_key, $CC_CONFIG["apiKey"])) { + return true; } - return true; + + //Start the session so the authentication is + //enforced by the ACL plugin. + Zend_Session::start(); + $authAdapter = Zend_Auth::getInstance(); + Application_Model_Auth::pinSessionToClient($authAdapter); + + if ((Zend_Auth::getInstance()->hasIdentity())) { + return true; + } + + header('HTTP/1.0 401 Unauthorized'); + print _('You are not allowed to access this resource.'); + exit(); } public function versionAction() diff --git a/airtime_mvc/application/controllers/BillingController.php b/airtime_mvc/application/controllers/BillingController.php index 0e135e436..4cfbd4f77 100644 --- a/airtime_mvc/application/controllers/BillingController.php +++ b/airtime_mvc/application/controllers/BillingController.php @@ -28,7 +28,10 @@ class BillingController extends Zend_Controller_Action { $baseUrl = Application_Common_OsPath::getBaseDir(); $this->view->headLink()->appendStylesheet($baseUrl.'css/billing.css?'.$CC_CONFIG['airtime_version']); Billing::ensureClientIdIsValid(); - + + //Zend's CSRF token element requires the session to be open for writing + SessionHelper::reopenSessionForWriting(); + $request = $this->getRequest(); $form = new Application_Form_BillingUpgradeDowngrade(); @@ -224,7 +227,10 @@ class BillingController extends Zend_Controller_Action { $CC_CONFIG = Config::getConfig(); $baseUrl = Application_Common_OsPath::getBaseDir(); $this->view->headLink()->appendStylesheet($baseUrl.'css/billing.css?'.$CC_CONFIG['airtime_version']); - + + //Zend's CSRF token element requires the session to be open for writing + SessionHelper::reopenSessionForWriting(); + $request = $this->getRequest(); $form = new Application_Form_BillingClient(); Billing::ensureClientIdIsValid(); diff --git a/airtime_mvc/application/controllers/LoginController.php b/airtime_mvc/application/controllers/LoginController.php index 708f9ac16..ab021385f 100644 --- a/airtime_mvc/application/controllers/LoginController.php +++ b/airtime_mvc/application/controllers/LoginController.php @@ -8,8 +8,14 @@ class LoginController extends Zend_Controller_Action public function init() { - //Open the session for writing, because we close it for writing by default in Bootstrap.php as an optimization. - session_start(); + $CC_CONFIG = Config::getConfig(); + $baseUrl = Application_Common_OsPath::getBaseDir(); + + $this->view->headLink(array('rel' => 'icon', 'href' => $baseUrl . 'favicon.ico?' . $CC_CONFIG['airtime_version'], 'type' => 'image/x-icon'), 'PREPEND') + ->appendStylesheet($baseUrl . 'css/bootstrap.css?' . $CC_CONFIG['airtime_version']) + ->appendStylesheet($baseUrl . 'css/redmond/jquery-ui-1.8.8.custom.css?' . $CC_CONFIG['airtime_version']) + ->appendStylesheet($baseUrl . 'css/styles.css?' . $CC_CONFIG['airtime_version']); + } public function indexAction() @@ -22,13 +28,21 @@ class LoginController extends Zend_Controller_Action //Enable AJAX requests from www.airtime.pro for the sign-in process. CORSHelper::enableATProCrossOriginRequests($request, $response); - + Application_Model_Locale::configureLocalization($request->getcookie('airtime_locale', $stationLocale)); - $auth = Zend_Auth::getInstance(); - - if ($auth->hasIdentity()) { - $this->_redirect('showbuilder'); + + if (Zend_Session::isStarted()) { + + //Open the session for writing, because we close it for writing by default in Bootstrap.php as an optimization. + SessionHelper::reopenSessionForWriting(); + + $auth = Zend_Auth::getInstance(); + $auth->getStorage(); + + if ($auth->hasIdentity()) { + $this->_redirect('showbuilder'); + } } //uses separate layout without a navigation. @@ -43,6 +57,10 @@ class LoginController extends Zend_Controller_Action $message = _("Please enter your username and password."); if ($request->isPost()) { + + //Open the session for writing, because we close it for writing by default in Bootstrap.php as an optimization. + //session_start(); + // if the post contains recaptcha field, which means form had recaptcha field. // Hence add the element for validation. if (array_key_exists('recaptcha_response_field', $request->getPost())) { @@ -117,6 +135,9 @@ class LoginController extends Zend_Controller_Action public function logoutAction() { + //Open the session for writing, because we close it for writing by default in Bootstrap.php as an optimization. + SessionHelper::reopenSessionForWriting(); + $auth = Zend_Auth::getInstance(); $auth->clearIdentity(); // Unset all session variables relating to CSRF prevention on logout diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php index 7bb425a7e..c1fa52d17 100644 --- a/airtime_mvc/application/controllers/PlaylistController.php +++ b/airtime_mvc/application/controllers/PlaylistController.php @@ -34,7 +34,7 @@ class PlaylistController extends Zend_Controller_Action ->initContext(); //This controller writes to the session all over the place, so we're going to reopen it for writing here. - session_start(); //Reopen the session for writing + SessionHelper::reopenSessionForWriting(); } private function getPlaylist($p_type) diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php index 359050510..4229aca98 100644 --- a/airtime_mvc/application/controllers/PreferenceController.php +++ b/airtime_mvc/application/controllers/PreferenceController.php @@ -34,7 +34,7 @@ class PreferenceController extends Zend_Controller_Action $form = new Application_Form_Preferences(); $values = array(); - session_start(); //Open session for writing. + SessionHelper::reopenSessionForWriting(); if ($request->isPost()) { $values = $request->getPost(); @@ -94,7 +94,7 @@ class PreferenceController extends Zend_Controller_Action $this->view->headScript()->appendFile($baseUrl.'js/airtime/preferences/support-setting.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->statusMsg = ""; - session_start(); //Open session for writing. + SessionHelper::reopenSessionForWriting(); $form = new Application_Form_SupportSettings(); if ($request->isPost()) { @@ -130,12 +130,18 @@ class PreferenceController extends Zend_Controller_Action public function removeLogoAction() { - session_start(); //Open session for writing. + SessionHelper::reopenSessionForWriting(); $this->view->layout()->disableLayout(); // Remove reliance on .phtml files to render requests $this->_helper->viewRenderer->setNoRender(true); + if (!SecurityHelper::verifyCSRFToken($this->_getParam('csrf_token'))) { + Logging::error(__FILE__ . ': Invalid CSRF token'); + $this->_helper->json->sendJson(array("jsonrpc" => "2.0", "valid" => false, "error" => "CSRF token did not match.")); + return; + } + Application_Model_Preference::SetStationLogo(""); } @@ -151,7 +157,7 @@ class PreferenceController extends Zend_Controller_Action $this->view->headScript()->appendFile($baseUrl.'js/airtime/preferences/streamsetting.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); - session_start(); //Open session for writing. + SessionHelper::reopenSessionForWriting(); $name_map = array( 'ogg' => 'Ogg Vorbis', @@ -445,7 +451,7 @@ class PreferenceController extends Zend_Controller_Action public function setSourceConnectionUrlAction() { - session_start(); //Open session for writing. + SessionHelper::reopenSessionForWriting(); $request = $this->getRequest(); $type = $request->getParam("type", null); @@ -465,7 +471,7 @@ class PreferenceController extends Zend_Controller_Action public function getAdminPasswordStatusAction() { - session_start(); //Open session for writing. + SessionHelper::reopenSessionForWriting(); $out = array(); $num_of_stream = intval(Application_Model_Preference::GetNumOfStreams()); @@ -483,6 +489,12 @@ class PreferenceController extends Zend_Controller_Action { $this->view->layout()->disableLayout(); $this->_helper->viewRenderer->setNoRender(true); + + if (!SecurityHelper::verifyCSRFToken($this->_getParam('csrf_token'))) { + Logging::error(__FILE__ . ': Invalid CSRF token'); + $this->_helper->json->sendJson(array("jsonrpc" => "2.0", "valid" => false, "error" => "CSRF token did not match.")); + return; + } // Only admin users should get here through ACL permissioning // Only allow POST requests diff --git a/airtime_mvc/application/controllers/UserController.php b/airtime_mvc/application/controllers/UserController.php index d626dcd5f..e4b48a90e 100644 --- a/airtime_mvc/application/controllers/UserController.php +++ b/airtime_mvc/application/controllers/UserController.php @@ -18,7 +18,7 @@ class UserController extends Zend_Controller_Action { // Start the session to re-open write permission to the session so we can // create the namespace for our csrf token verification - session_start(); + SessionHelper::reopenSessionForWriting(); $CC_CONFIG = Config::getConfig(); $request = $this->getRequest(); @@ -126,7 +126,8 @@ class UserController extends Zend_Controller_Action { Zend_Layout::getMvcInstance()->assign('parent_page', 'Settings'); - session_start(); //Reopen session for writing. + SessionHelper::reopenSessionForWriting(); + $request = $this->getRequest(); $form = new Application_Form_EditUser(); if ($request->isPost()) { diff --git a/airtime_mvc/application/controllers/UsersettingsController.php b/airtime_mvc/application/controllers/UsersettingsController.php index 35fbb19e1..519e854b8 100644 --- a/airtime_mvc/application/controllers/UsersettingsController.php +++ b/airtime_mvc/application/controllers/UsersettingsController.php @@ -70,14 +70,14 @@ class UsersettingsController extends Zend_Controller_Action public function remindmeAction() { // unset session - session_start(); //open session for writing again + SessionHelper::reopenSessionForWriting(); Zend_Session::namespaceUnset('referrer'); Application_Model_Preference::SetRemindMeDate(); } public function remindmeNeverAction() { - session_start(); //open session for writing again + SessionHelper::reopenSessionForWriting(); Zend_Session::namespaceUnset('referrer'); //pass in true to indicate 'Remind me never' was clicked Application_Model_Preference::SetRemindMeDate(true); @@ -86,7 +86,7 @@ class UsersettingsController extends Zend_Controller_Action public function donotshowregistrationpopupAction() { // unset session - session_start(); //open session for writing again + SessionHelper::reopenSessionForWriting(); Zend_Session::namespaceUnset('referrer'); } diff --git a/airtime_mvc/application/controllers/plugins/Acl_plugin.php b/airtime_mvc/application/controllers/plugins/Acl_plugin.php index 18f1c69a5..6bfe20d0d 100644 --- a/airtime_mvc/application/controllers/plugins/Acl_plugin.php +++ b/airtime_mvc/application/controllers/plugins/Acl_plugin.php @@ -109,9 +109,10 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract public function preDispatch(Zend_Controller_Request_Abstract $request) { $controller = strtolower($request->getControllerName()); - Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance()); if (in_array($controller, array( + "index", + "login", "api", "auth", "error", @@ -123,7 +124,10 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract ))) { $this->setRoleName("G"); - } elseif (!Zend_Auth::getInstance()->hasIdentity()) { + } + elseif (Zend_Session::isStarted() && !Zend_Auth::getInstance()->hasIdentity()) { + + //The controller uses sessions but we don't have an identity yet. // If we don't have an identity and we're making a RESTful request, // we need to do API key verification @@ -165,6 +169,7 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract } } } else { //We have a session/identity. + // If we have an identity and we're making a RESTful request, // we need to check the CSRF token if ($_SERVER['REQUEST_METHOD'] != "GET" && $request->getModuleName() == "rest") { @@ -223,11 +228,7 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract } private function verifyCSRFToken($token) { - $current_namespace = new Zend_Session_Namespace('csrf_namespace'); - $observed_csrf_token = $token; - $expected_csrf_token = $current_namespace->authtoken; - - return ($observed_csrf_token == $expected_csrf_token); + return SecurityHelper::verifyCSRFToken($token); } private function verifyAPIKey() { diff --git a/airtime_mvc/application/controllers/plugins/ConversionTracking.php b/airtime_mvc/application/controllers/plugins/ConversionTracking.php index 09904828d..ae0fc2781 100644 --- a/airtime_mvc/application/controllers/plugins/ConversionTracking.php +++ b/airtime_mvc/application/controllers/plugins/ConversionTracking.php @@ -4,6 +4,10 @@ class Zend_Controller_Plugin_ConversionTracking extends Zend_Controller_Plugin_A { public function preDispatch(Zend_Controller_Request_Abstract $request) { + if (!Zend_Session::isStarted()) { + return; + } + //If user is a super admin and old plan level is set to trial.... if (Application_Common_GoogleAnalytics::didPaidConversionOccur($request)) { diff --git a/airtime_mvc/application/controllers/plugins/PageLayoutInitPlugin.php b/airtime_mvc/application/controllers/plugins/PageLayoutInitPlugin.php new file mode 100644 index 000000000..dca720028 --- /dev/null +++ b/airtime_mvc/application/controllers/plugins/PageLayoutInitPlugin.php @@ -0,0 +1,241 @@ +_bootstrap = $boostrap; + } + + /** + * Start the session depending on which controller your request is going to. + * We start the session explicitly here so that we can avoid starting sessions + * needlessly for (stateless) requests to the API. + * @param Zend_Controller_Request_Abstract $request + * @throws Zend_Session_Exception + */ + public function routeShutdown(Zend_Controller_Request_Abstract $request) + { + $controller = strtolower($request->getControllerName()); + $action = strtolower($request->getActionName()); + + //List of controllers where we don't need a session, and we don't need + //all the standard HTML / JS boilerplate. + if (!in_array($controller, array( + "index", //Radio Page + "api", + "auth", + "error", + "locale", + "upgrade", + 'whmcs-login', + "provisioning", + "embed" + )) + ) { + //Start the session + Zend_Session::start(); + Application_Model_Auth::pinSessionToClient(Zend_Auth::getInstance()); + + //localization configuration + Application_Model_Locale::configureLocalization(); + + $this->_initGlobals(); + $this->_initCsrfNamespace(); + $this->_initHeadLink(); + $this->_initHeadScript(); + $this->_initTitle(); + $this->_initTranslationGlobals(); + $this->_initViewHelpers(); + } + } + + protected function _initGlobals() + { + if (!Zend_Session::isStarted()) { + return; + } + + $view = $this->_bootstrap->getResource('view'); + $baseUrl = Application_Common_OsPath::getBaseDir(); + + $view->headScript()->appendScript("var baseUrl = '$baseUrl';"); + $this->_initTranslationGlobals($view); + + $user = Application_Model_User::GetCurrentUser(); + if (!is_null($user)) { + $userType = $user->getType(); + } else { + $userType = ""; + } + $view->headScript()->appendScript("var userType = '$userType';"); + + // Dropzone also accept file extensions and doesn't correctly extract certain mimetypes (eg. FLAC - try it), + // so we append the file extensions to the list of mimetypes and that makes it work. + $mimeTypes = FileDataHelper::getAudioMimeTypeArray(); + $fileExtensions = array_values($mimeTypes); + foreach($fileExtensions as &$extension) { + $extension = '.' . $extension; + } + $view->headScript()->appendScript("var acceptedMimeTypes = " . json_encode(array_merge(array_keys($mimeTypes), $fileExtensions)) . ";"); + } + + /** + * Create a global namespace to hold a session token for CSRF prevention + */ + protected function _initCsrfNamespace() + { + /* + if (!Zend_Session::isStarted()) { + return; + }*/ + + $csrf_namespace = new Zend_Session_Namespace('csrf_namespace'); + // Check if the token exists + if (!$csrf_namespace->authtoken) { + // If we don't have a token, regenerate it and set a 1 week timeout + // Should we log the user out here if the token is expired? + $csrf_namespace->authtoken = sha1(uniqid(rand(), 1)); + $csrf_namespace->setExpirationSeconds(168 * 60 * 60); + } + + //Here we are closing the session for writing because otherwise no requests + //in this session will be handled in parallel. This gives a major boost to the perceived performance + //of the application (page load times are more consistent, no lock contention). + session_write_close(); + //Zend_Session::writeClose(true); + } + + /** + * Ideally, globals should be written to a single js file once + * from a php init function. This will save us from having to + * reinitialize them every request + */ + private function _initTranslationGlobals() + { + $view = $this->_bootstrap->getResource('view'); + $view->headScript()->appendScript("var PRODUCT_NAME = '" . PRODUCT_NAME . "';"); + $view->headScript()->appendScript("var USER_MANUAL_URL = '" . USER_MANUAL_URL . "';"); + $view->headScript()->appendScript("var COMPANY_NAME = '" . COMPANY_NAME . "';"); + } + + protected function _initHeadLink() + { + $CC_CONFIG = Config::getConfig(); + + $view = $this->_bootstrap->getResource('view'); + + $baseUrl = Application_Common_OsPath::getBaseDir(); + + $view->headLink(array('rel' => 'icon', 'href' => $baseUrl . 'favicon.ico?' . $CC_CONFIG['airtime_version'], 'type' => 'image/x-icon'), 'PREPEND') + ->appendStylesheet($baseUrl . 'css/bootstrap.css?' . $CC_CONFIG['airtime_version']) + ->appendStylesheet($baseUrl . 'css/redmond/jquery-ui-1.8.8.custom.css?' . $CC_CONFIG['airtime_version']) + ->appendStylesheet($baseUrl . 'css/pro_dropdown_3.css?' . $CC_CONFIG['airtime_version']) + ->appendStylesheet($baseUrl . 'css/qtip/jquery.qtip.min.css?' . $CC_CONFIG['airtime_version']) + ->appendStylesheet($baseUrl . 'css/styles.css?' . $CC_CONFIG['airtime_version']) + ->appendStylesheet($baseUrl . 'css/masterpanel.css?' . $CC_CONFIG['airtime_version']) + ->appendStylesheet($baseUrl . 'css/tipsy/jquery.tipsy.css?' . $CC_CONFIG['airtime_version']); + } + + protected function _initHeadScript() + { + if (!Zend_Session::isStarted()) { + return; + } + + $CC_CONFIG = Config::getConfig(); + + $view = $this->_bootstrap->getResource('view'); + + $baseUrl = Application_Common_OsPath::getBaseDir(); + + $view->headScript()->appendFile($baseUrl . 'js/libs/jquery-1.8.3.min.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/libs/jquery-ui-1.8.24.min.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/bootstrap/bootstrap.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/libs/underscore-min.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + + // ->appendFile($baseUrl . 'js/libs/jquery.stickyPanel.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/qtip/jquery.qtip.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/jplayer/jquery.jplayer.min.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/sprintf/sprintf-0.7-beta1.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/cookie/jquery.cookie.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/i18n/jquery.i18n.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'locale/general-translation-table?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'locale/datatables-translation-table?' . $CC_CONFIG['airtime_version'], 'text/javascript') + + ->appendScript("$.i18n.setDictionary(general_dict)") + ->appendScript("var baseUrl='$baseUrl'"); + + //These timezones are needed to adjust javascript Date objects on the client to make sense to the user's set timezone + //or the server's set timezone. + $serverTimeZone = new DateTimeZone(Application_Model_Preference::GetDefaultTimezone()); + $now = new DateTime("now", $serverTimeZone); + $offset = $now->format("Z") * -1; + $view->headScript()->appendScript("var serverTimezoneOffset = {$offset}; //in seconds"); + + if (class_exists("Zend_Auth", false) && Zend_Auth::getInstance()->hasIdentity()) { + $userTimeZone = new DateTimeZone(Application_Model_Preference::GetUserTimezone()); + $now = new DateTime("now", $userTimeZone); + $offset = $now->format("Z") * -1; + $view->headScript()->appendScript("var userTimezoneOffset = {$offset}; //in seconds"); + } + + //scripts for now playing bar + $view->headScript()->appendFile($baseUrl . 'js/airtime/airtime_bootstrap.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/airtime/dashboard/helperfunctions.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/airtime/dashboard/dashboard.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/airtime/dashboard/versiontooltip.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/tipsy/jquery.tipsy.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + + ->appendFile($baseUrl . 'js/airtime/common/common.js?' . $CC_CONFIG['airtime_version'], 'text/javascript') + ->appendFile($baseUrl . 'js/airtime/common/audioplaytest.js?' . $CC_CONFIG['airtime_version'], 'text/javascript'); + + $user = Application_Model_User::getCurrentUser(); + if (!is_null($user)) { + $userType = $user->getType(); + } else { + $userType = ""; + } + + $view->headScript()->appendScript("var userType = '$userType';"); + if (array_key_exists('REQUEST_URI', $_SERVER) //Doesn't exist for unit tests + && strpos($_SERVER['REQUEST_URI'], 'Dashboard/stream-player') === false + && strpos($_SERVER['REQUEST_URI'], 'audiopreview') === false + && $_SERVER['REQUEST_URI'] != "/") { + $plan_level = strval(Application_Model_Preference::GetPlanLevel()); + // Since the Hobbyist plan doesn't come with Live Chat support, don't enable it + if (Application_Model_Preference::GetLiveChatEnabled() && $plan_level !== 'hobbyist') { + $client_id = strval(Application_Model_Preference::GetClientId()); + $station_url = $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI']; + $view->headScript()->appendScript("var livechat_client_id = '$client_id';\n" . + "var livechat_plan_type = '$plan_level';\n" . + "var livechat_station_url = 'http://$station_url';"); + $view->headScript()->appendFile($baseUrl . 'js/airtime/common/livechat.js?' . $CC_CONFIG['airtime_version'], 'text/javascript'); + } + } + + /* + if (isset($CC_CONFIG['demo']) && $CC_CONFIG['demo'] == 1) { + $view->headScript()->appendFile($baseUrl.'js/libs/google-analytics.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + }*/ + } + + protected function _initViewHelpers() + { + $view = $this->_bootstrap->getResource('view'); + $view->addHelperPath(APPLICATION_PATH . 'views/helpers', 'Airtime_View_Helper'); + $view->assign('suspended', (Application_Model_Preference::getProvisioningStatus() == PROVISIONING_STATUS_SUSPENDED)); + } + + protected function _initTitle() + { + $view = $this->_bootstrap->getResource('view'); + $view->headTitle(Application_Model_Preference::GetHeadTitle()); + } +} diff --git a/airtime_mvc/application/forms/BillingClient.php b/airtime_mvc/application/forms/BillingClient.php index 7cbac545d..098c004a4 100644 --- a/airtime_mvc/application/forms/BillingClient.php +++ b/airtime_mvc/application/forms/BillingClient.php @@ -188,6 +188,10 @@ class Application_Form_BillingClient extends Zend_Form $passwordVerify->addValidator($notEmptyValidator); $this->addElement($passwordVerify); + $this->addElement('hash', 'csrf', array( + 'salt' => 'unique' + )); + $submit = new Zend_Form_Element_Submit("submit"); $submit->setIgnore(true) ->setLabel(_pro("Save")); diff --git a/airtime_mvc/application/forms/BillingUpgradeDowngrade.php b/airtime_mvc/application/forms/BillingUpgradeDowngrade.php index ecf9b4f50..5ff4ff4de 100644 --- a/airtime_mvc/application/forms/BillingUpgradeDowngrade.php +++ b/airtime_mvc/application/forms/BillingUpgradeDowngrade.php @@ -8,6 +8,10 @@ class Application_Form_BillingUpgradeDowngrade extends Zend_Form $csrf_element->setValue($csrf_namespace->authtoken)->setRequired('true')->removeDecorator('HtmlTag')->removeDecorator('Label'); $this->addElement($csrf_element); + $this->addElement('hash', 'csrf', array( + 'salt' => 'unique' + )); + $productPrices = array(); $productTypes = array(); list($productPrices, $productTypes) = Billing::getProductPricesAndTypes(); diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php index e8bf5adf5..5003466ad 100644 --- a/airtime_mvc/application/models/Preference.php +++ b/airtime_mvc/application/models/Preference.php @@ -10,7 +10,7 @@ class Application_Model_Preference { //pass in true so the check is made with the autoloader //we need this check because saas calls this function from outside Zend - if (!class_exists("Zend_Auth", true) || !Zend_Auth::getInstance()->hasIdentity()) { + if (!class_exists("Zend_Session", true) || !Zend_Session::isStarted() || !class_exists("Zend_Auth", true) || !Zend_Auth::getInstance()->hasIdentity()) { $userId = null; } else { $auth = Zend_Auth::getInstance(); @@ -150,10 +150,14 @@ class Application_Model_Preference try { - $userId = self::getUserId(); - - if ($isUserValue && is_null($userId)) - throw new Exception("User id can't be null for a user preference."); + $userId = null; + if ($isUserValue) { + //This is nested in here because so we can still use getValue() when the session hasn't started yet. + $userId = self::getUserId(); + if (is_null($userId)) { + throw new Exception("User id can't be null for a user preference."); + } + } // If the value is already cached, return it $res = $cache->fetch($key, $isUserValue, $userId); @@ -202,7 +206,7 @@ class Application_Model_Preference } catch (Exception $e) { header('HTTP/1.0 503 Service Unavailable'); - Logging::info("Could not connect to database: ".$e->getMessage()); + Logging::info("Could not connect to database: ".$e); exit; } } diff --git a/airtime_mvc/public/js/airtime/preferences/preferences.js b/airtime_mvc/public/js/airtime/preferences/preferences.js index 40e267ecd..8850caa32 100644 --- a/airtime_mvc/public/js/airtime/preferences/preferences.js +++ b/airtime_mvc/public/js/airtime/preferences/preferences.js @@ -114,16 +114,18 @@ function setMsAuthenticationFieldsReadonly(ele) { } function removeLogo() { - $.post(baseUrl+'preference/remove-logo', function(json){}); - // Reload without resubmitting the form - location.href = location.href.replace(location.hash,""); + $.post(baseUrl+'preference/remove-logo', {'csrf_token' : $('#csrf').val()}, function(json){ + // Reload without resubmitting the form + location.href = location.href.replace(location.hash,""); + }); } function deleteAllFiles() { var resp = confirm($.i18n._("Are you sure you want to delete all the tracks in your library?")) if (resp) { - $.post(baseUrl+'preference/delete-all-files', function(json){}); - location.reload(); + $.post(baseUrl+'preference/delete-all-files', {'csrf_token' : $('#csrf').val()}, function(json){ + location.reload(); + }); } }