Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
New feature #8239: Allow for user friendly survey URLS (#2218)
  • Loading branch information
gabrieljenik committed Nov 8, 2022
1 parent 89d3b1b commit 0b5acab
Show file tree
Hide file tree
Showing 18 changed files with 1,699 additions and 10 deletions.
2 changes: 1 addition & 1 deletion application/config/version.php
Expand Up @@ -12,7 +12,7 @@
*/

$config['versionnumber'] = '5.5.0-dev';
$config['dbversionnumber'] = 493;
$config['dbversionnumber'] = 494;
$config['buildnumber'] = '';
$config['updatable'] = true;
$config['templateapiversion'] = 3;
Expand Down
3 changes: 3 additions & 0 deletions application/controllers/LimereplacementfieldsController.php
Expand Up @@ -333,6 +333,7 @@ private function getReplacementFields($fieldtype, $surveyid)
$replFields['ADMINNAME'] = gT("Survey administrator - Name");
$replFields['ADMINEMAIL'] = gT("Survey administrator - Email address");
$replFields['SURVEYURL'] = gT("Survey URL");
$replFields['SURVEYIDURL'] = gT("Survey URL based on survey ID");
$replFields['EXPIRY'] = gT("Survey expiration date");
return array($replFields, false);

Expand All @@ -352,6 +353,7 @@ private function getReplacementFields($fieldtype, $surveyid)
$replFields['ADMINNAME'] = gT("Survey administrator - Name");
$replFields['ADMINEMAIL'] = gT("Survey administrator - Email address");
$replFields['SURVEYURL'] = gT("Survey URL");
$replFields['SURVEYIDURL'] = gT("Survey URL based on survey ID");
$replFields['EXPIRY'] = gT("Survey expiration date");
return array($replFields, false);
} elseif (strpos($fieldtype, 'email_confirmation') !== false) {
Expand All @@ -371,6 +373,7 @@ private function getReplacementFields($fieldtype, $surveyid)
$replFields['ADMINNAME'] = gT("Survey administrator - Name");
$replFields['ADMINEMAIL'] = gT("Survey administrator - Email address");
$replFields['SURVEYURL'] = gT("Survey URL");
$replFields['SURVEYIDURL'] = gT("Survey URL based on survey ID");
$replFields['EXPIRY'] = gT("Survey expiration date");

// email-conf can accept insertans fields for non anonymous surveys
Expand Down
6 changes: 6 additions & 0 deletions application/controllers/SurveyAdministrationController.php
Expand Up @@ -185,6 +185,12 @@ public function actionView()
$aData['display']['menu_bars']['surveysummary'] = 'viewgroup';
}

$surveyUrls = [];
foreach ($survey->allLanguages as $language) {
$surveyUrls[$language] = $survey->getSurveyUrl($language);
}
$aData['surveyUrls'] = $surveyUrls;

$this->surveysummary($aData);

// Display 'Overview' in Green Bar
Expand Down
27 changes: 25 additions & 2 deletions application/controllers/admin/Database.php
Expand Up @@ -577,13 +577,15 @@ private function actionUpdateSurveyLocaleSettings($iSurveyID)

Yii::app()->loadHelper('database');

$hasSurveyLanguageSettingError = false;
if (Permission::model()->hasSurveyPermission($iSurveyID, 'surveylocale', 'update')) {
foreach ($languagelist as $langname) {
if ($langname) {
$data = array();
$sURLDescription = Yii::app()->request->getPost('urldescrip_' . $langname, null);
$sURL = Yii::app()->request->getPost('url_' . $langname, null);
$short_title = Yii::app()->request->getPost('short_title_' . $langname, null);
$alias = Yii::app()->request->getPost('alias_' . $langname, null);
$description = Yii::app()->request->getPost('description_' . $langname, null);
$welcome = Yii::app()->request->getPost('welcome_' . $langname, null);
$endtext = Yii::app()->request->getPost('endtext_' . $langname, null);
Expand All @@ -598,6 +600,9 @@ private function actionUpdateSurveyLocaleSettings($iSurveyID)
$short_title = $this->oFixCKeditor->fixCKeditor($short_title);
$data['surveyls_title'] = $short_title;
}
if ($alias !== null) {
$data['surveyls_alias'] = $alias;
}
if ($description !== null) {
// Fix bug with FCKEditor saving strange BR types
$description = $this->oFixCKeditor->fixCKeditor($description);
Expand Down Expand Up @@ -642,7 +647,21 @@ private function actionUpdateSurveyLocaleSettings($iSurveyID)
if (count($data) > 0) {
$oSurveyLanguageSetting = SurveyLanguageSetting::model()->findByPk(array('surveyls_survey_id' => $iSurveyID, 'surveyls_language' => $langname));
$oSurveyLanguageSetting->setAttributes($data);
$oSurveyLanguageSetting->save(); // save the change to database
if (!$oSurveyLanguageSetting->save()) { // save the change to database
$languageDescription = getLanguageNameFromCode($langname, false);
Yii::app()->setFlashMessage(
Chtml::errorSummary(
$oSurveyLanguageSetting,
Chtml::tag(
"p",
['class' => 'strong'],
sprintf(gT("Texts for %s language could not be updated:"), $languageDescription)
)
),
"error"
);
$hasSurveyLanguageSettingError = true;
}
}
}
}
Expand Down Expand Up @@ -765,7 +784,11 @@ private function actionUpdateSurveyLocaleSettings($iSurveyID)
$aAfterApplyAttributes = $oSurvey->attributes;

if ($oSurvey->save()) {
Yii::app()->setFlashMessage(gT("Survey settings were successfully saved."));
if (!$hasSurveyLanguageSettingError) {
Yii::app()->setFlashMessage(gT("Survey settings were successfully saved."));
} else {
Yii::app()->setFlashMessage(gT("Survey settings were saved, but there where errors with some languages."), "warning");
}
} else {
Yii::app()->setFlashMessage(CHtml::errorSummary($oSurvey, CHtml::tag("p", array('class' => 'strong'), gT("Survey could not be updated, please fix the following error:"))), "error");
}
Expand Down
2 changes: 2 additions & 0 deletions application/controllers/admin/ExpressionValidate.php
Expand Up @@ -198,6 +198,7 @@ public function email($iSurveyId, $lang)
$aReplacement["OPTINURL"] = gT("URL for a respondent to opt-in to this survey");
$aReplacement["GLOBALOPTINURL"] = gT("URL for a respondent to opt-in to the central participant list for this site");
$aReplacement["SURVEYURL"] = gT("Survey URL");
$aReplacement['SURVEYIDURL'] = gT("Survey URL based on survey ID");
foreach ($aAttributes as $sAttribute => $aAttribute) {
$aReplacement['' . strtoupper($sAttribute) . ''] = sprintf(gT("Participant - Attribute: %s"), $aAttribute['description']);
}
Expand All @@ -207,6 +208,7 @@ public function email($iSurveyId, $lang)
$aReplacement["FIRSTNAME"] = gT("Participant - Last name");
$aReplacement["LASTNAME"] = gT("Participant - First name");
$aReplacement["SURVEYURL"] = gT("Survey URL");
$aReplacement['SURVEYIDURL'] = gT("Survey URL without alias");
foreach ($aAttributes as $sAttribute => $aAttribute) {
$aReplacement['' . strtoupper($sAttribute) . ''] = sprintf(gT("Participant - Attribute: %s"), $aAttribute['description']);
}
Expand Down
54 changes: 54 additions & 0 deletions application/core/LSYii_Application.php
Expand Up @@ -102,6 +102,9 @@ public function __construct($aApplicationConfig = null)
if (!isset($aApplicationConfig['components']['assetManager']['basePath'])) {
App()->getAssetManager()->setBasePath($this->config['tempdir'] . '/assets');
}

// Load common helper
$this->loadHelper("common");
}

/* @inheritdoc */
Expand Down Expand Up @@ -508,6 +511,57 @@ public function getTwigCustomExtensionsConfig($sUsertwigextensionrootdir, $aAppl
return $aApplicationConfig;
}

/**
* @inheritdoc
* Special handling for SEO friendly URLs
*/
public function createController($route, $owner=null)
{
$controller = parent::createController($route, $owner);

// If no controller is found by standard ways, check if the route matches
// an existing survey's alias.
if (is_null($controller)) {
$controller = $this->createControllerFromShortUrl($route);
}

return $controller;
}

/**
* Create controller from short url if the route matches a survey alias.
* @param string $route the route of the request.
* @return array<mixed>|null
*/
private function createControllerFromShortUrl($route)
{
// When updating from versions that didn't support short urls, this code runs before the update process,
// so we cannot asume the field exists. We try to retrieve the Survey Language Settings and, if it fails,
// just don't do anything.
try {
$alias = explode("/", $route)[0];
$criteria = new CDbCriteria();
$criteria->addCondition('surveyls_alias = :alias');
$criteria->params[':alias'] = $alias;
$criteria->index = 'surveyls_language';

$languageSettings = SurveyLanguageSetting::model()->find($criteria);
} catch (CDbException $ex) {
// It's probably just because the field doesn't exist, so don't do anything.
}

if (empty($languageSettings)) {
return null;
}

// If no language is specified in the request, add a GET param based on the survey's language for this alias
$language = $this->request->getParam('lang');
if (empty($language)) {
$_GET['lang'] = $languageSettings->surveyls_language;
}
return parent::createController("survey/index/sid/" . $languageSettings->surveyls_survey_id);
}

/**
* Handles specific exception cases, like "user friendly" exceptions and exceptions on ajax requests.
*
Expand Down
2 changes: 1 addition & 1 deletion application/core/LSYii_Controller.php
Expand Up @@ -39,7 +39,7 @@ public function __construct($id, $module = null)
// Deprecated function
$this->loadHelper('globalsettings');
// tracevar function
$this->loadHelper('common');
//$this->loadHelper('common');
$this->loadHelper('expressions.em_manager');
$this->loadHelper('replacements');
$this->customInit();
Expand Down
152 changes: 152 additions & 0 deletions application/core/LSYii_ShortUrlValidator.php
@@ -0,0 +1,152 @@
<?php

if (!defined('BASEPATH')) {
exit('No direct script access allowed');
}
/*
* LimeSurvey
* Copyright (C) 2007-2011 The LimeSurvey Project Team / Carsten Schmitz
* All rights reserved.
* License: GNU/GPL License v2 or later, see LICENSE.php
* LimeSurvey is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
* See COPYRIGHT.php for copyright notices and details.
*
*/
/**
* Validator class for Short URLs (Survey Aliases).
* Compares the alias against basic route rules and existing controllers, trying
* to avoid collisions.
*/
class LSYii_ShortUrlValidator extends CValidator
{
protected $urlManager = null;

public function __construct($urlManager = null)
{
if (!isset($urlManager)) {
$urlManager = Yii::app()->getUrlManager();
}
$this->urlManager = $urlManager;
}

protected function validateAttribute($object, $attribute)
{
if (empty($object->$attribute)) {
return;
}
if (
$this->matchesReservedPath($object->$attribute)
|| $this->matchesExistingRoute($object->$attribute)
|| $this->matchesExistingController($object->$attribute)
) {
$this->addError($object, $attribute, gT('Survey Alias matches an existing route.'));
}
}

/**
* Checks whether a specified route matches a controller
* @param string $route
* @param CModule|null $owner
* @return boolean
*/
protected function matchesExistingController($route, $owner = null)
{
if ($owner === null) {
$owner = Yii::app();
$defaultController = 'surveys';
$controllerPath = Yii::app()->getBasePath() . DIRECTORY_SEPARATOR . 'controllers';
} else {
$defaultController = $owner->defaultController;
$controllerPath = $owner->getControllerPath();
}
if ((array)$route === $route || ($route = trim($route, '/')) === '') {
$route = $defaultController;
}
$caseSensitive = Yii::app()->getUrlManager()->caseSensitive;

$route .= '/';
$controllerID = '';
while (($pos=strpos($route, '/')) !== false) {
$id = substr($route, 0, $pos);
if (!preg_match('/^\w+$/', $id)) {
return null;
}
if (!$caseSensitive) {
$id = strtolower($id);
}
$route = (string)substr($route, $pos+1);
if (!isset($basePath)) { // first segment
if (
($module = $owner->getModule($id)) !== null
&& is_a($module, "CWebModule")
) {
return $this->isRouteValid($route, $module);
}
$basePath = $controllerPath;
$controllerID = '';
} else {
$controllerID .= '/';
}
$className = ucfirst($id) . 'Controller';
$classFile = $basePath . DIRECTORY_SEPARATOR . $className . '.php';

if (isset($owner->controllerNamespace)) {
$className = $owner->controllerNamespace . '\\' . str_replace('/', '\\', $controllerID) . $className;
}

if (is_file($classFile)) {
if (!class_exists($className, false)) {
require_once($classFile);
}
if (class_exists($className, false) && is_subclass_of($className, 'CController')) {
$id[0] = strtolower($id[0]);
return true;
}
return false;
}
$controllerID .= $id;
$basePath .= DIRECTORY_SEPARATOR . $id;
}
}

/**
* Checks whether a specified alias matches any of the configured routes.
* @param string $alias
* @return boolean
*/
protected function matchesExistingRoute($alias)
{
// Since survey aliases can not contain slashes, we only care about the first part of route patterns.
$patterns = array_keys($this->urlManager->rules);
foreach ($patterns as $pattern) {
// We only check against fixed routes (we can't handle routes like '<_controller:\w+>/<_action:\w+>' here)
if (strpos($pattern, "<") === 0) {
continue;
}
$firstPart = explode("/", $pattern)[0];
if ($firstPart == $alias) {
return true;
}
}
}

/**
* Checks whether a specified alias matches any of the reserved words/paths.
* @param string $alias
* @return boolean
*/
protected function matchesReservedPath($alias)
{
$reserved = [
'assets',
'plugins',
'tmp',
'upload'
];
return in_array($alias, $reserved);
}

}
10 changes: 6 additions & 4 deletions application/core/LimeMailer.php
Expand Up @@ -791,9 +791,10 @@ public function getTokenReplacements()
if (empty($this->oToken)) { // Did need to check if sent to token ?
return $aTokenReplacements;
}
$survey = Survey::model()->findByPk($this->surveyId);
$language = Yii::app()->getLanguage();
if (!in_array($language, Survey::model()->findByPk($this->surveyId)->getAllLanguages())) {
$language = Survey::model()->findByPk($this->surveyId)->language;
if (!in_array($language, $survey->getAllLanguages())) {
$language = $survey->language;
}
$token = $this->oToken->token;
if (!empty($this->oToken->language)) {
Expand All @@ -818,9 +819,10 @@ public function getTokenReplacements()
$aTokenReplacements["GLOBALOPTINURL"] = App()->getController()
->createAbsoluteUrl("/optin/participants", array("surveyid" => $this->surveyId, "token" => $token,"langcode" => $language));
$this->addUrlsPlaceholders("GLOBALOPTINURL");
$aTokenReplacements["SURVEYURL"] = App()->getController()
->createAbsoluteUrl("/survey/index", array("sid" => $this->surveyId, "token" => $token,"lang" => $language));
$aTokenReplacements["SURVEYURL"] = $survey->getSurveyUrl($language, ["token" => $token]);
$this->addUrlsPlaceholders("SURVEY");
$aTokenReplacements["SURVEYIDURL"] = $survey->getSurveyUrl($language, ["token" => $token], false);
$this->addUrlsPlaceholders("SURVEYID");
return $aTokenReplacements;
}

Expand Down
14 changes: 14 additions & 0 deletions application/helpers/update/updates/Update_494.php
@@ -0,0 +1,14 @@
<?php

namespace LimeSurvey\Helpers\Update;

class Update_494 extends DatabaseUpdateBase
{
/**
* @inheritDoc
*/
public function up()
{
$this->db->createCommand()->addColumn('{{surveys_languagesettings}}', 'surveyls_alias', 'string(100)');
}
}

0 comments on commit 0b5acab

Please sign in to comment.