Skip to content

Commit

Permalink
Merge pull request #221 from tractorcow/pulls/1.1/fix-https
Browse files Browse the repository at this point in the history
API Add option to specify http / https on subsite domains
  • Loading branch information
dhensby committed Nov 24, 2015
2 parents a4c925b + ce90c21 commit 1876b78
Show file tree
Hide file tree
Showing 9 changed files with 588 additions and 226 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

language: php

sudo: false

php:
- 5.4

Expand Down
17 changes: 11 additions & 6 deletions code/extensions/SiteTreeSubsites.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,17 @@ public function updateCMSFields(FieldList $fields)
// replace readonly link prefix
$subsite = $this->owner->Subsite();
$nested_urls_enabled = Config::inst()->get('SiteTree', 'nested_urls');
if ($subsite && $subsite->ID) {
$baseUrl = Director::protocol() . $subsite->domain() . '/';
$baseLink = Controller::join_links(
$baseUrl,
($nested_urls_enabled && $this->owner->ParentID ? $this->owner->Parent()->RelativeLink(true) : null)
);
if ($subsite && $subsite->exists()) {
// Use baseurl from domain
$baseLink = $subsite->absoluteBaseURL();

// Add parent page if enabled
if($nested_urls_enabled && $this->owner->ParentID) {
$baseLink = Controller::join_links(
$baseLink,
$this->owner->Parent()->RelativeLink(true)
);
}

$urlsegment = $fields->dataFieldByName('URLSegment');
$urlsegment->setURLPrefix($baseLink);
Expand Down
43 changes: 43 additions & 0 deletions code/forms/WildcardDomainField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/**
* A text field that accepts only valid domain names, but allows the wildcard (*) character
*/
class WildcardDomainField extends TextField
{
/**
* Validate this field as a valid hostname
*
* @param Validator $validator
* @return bool
*/
public function validate($validator)
{
if ($this->checkHostname($this->Value())) {
return true;
}

$validator->validationError(
$this->getName(),
_t("DomainNameField.INVALID_DOMAIN", "Invalid domain name"),
"validation"
);
return false;
}

/**
* Check if the given hostname is valid.
*
* @param string $hostname
* @return bool True if this hostname is valid
*/
public function checkHostname($hostname)
{
return (bool)preg_match('/^([a-z0-9\*]+[\-\.])*([a-z0-9\*]+)$/', $hostname);
}

public function Type()
{
return 'text wildcarddomain';
}
}
50 changes: 29 additions & 21 deletions code/model/Subsite.php
Original file line number Diff line number Diff line change
Expand Up @@ -758,26 +758,27 @@ public function onAfterWrite()
*/
public function domain()
{
if ($this->ID) {
$domains = DataObject::get("SubsiteDomain", "\"SubsiteID\" = $this->ID", "\"IsPrimary\" DESC", "", 1);
if ($domains && $domains->Count()>0) {
$domain = $domains->First()->Domain;
// If there are wildcards in the primary domain (not recommended), make some
// educated guesses about what to replace them with:
$domain = preg_replace('/\.\*$/', ".$_SERVER[HTTP_HOST]", $domain);
// Default to "subsite." prefix for first wildcard
// TODO Whats the significance of "subsite" in this context?!
$domain = preg_replace('/^\*\./', "subsite.", $domain);
// *Only* removes "intermediate" subdomains, so 'subdomain.www.domain.com' becomes 'subdomain.domain.com'
$domain = str_replace('.www.', '.', $domain);

return $domain;
}

// SubsiteID = 0 is often used to refer to the main site, just return $_SERVER['HTTP_HOST']
} else {
return $_SERVER['HTTP_HOST'];
// Get best SubsiteDomain object
$domainObject = $this->getPrimarySubsiteDomain();
if ($domainObject) {
return $domainObject->SubstitutedDomain;
}

// If there are no objects, default to the current hostname
return $_SERVER['HTTP_HOST'];
}

/**
* Finds the primary {@see SubsiteDomain} object for this subsite
*
* @return SubsiteDomain
*/
public function getPrimarySubsiteDomain()
{
return $this
->Domains()
->sort('"IsPrimary" DESC')
->first();
}

/**
Expand All @@ -790,12 +791,19 @@ public function getPrimaryDomain()
}

/**
*
* Get the absolute URL for this subsite
* @return string
*/
public function absoluteBaseURL()
{
return "http://" . $this->domain() . Director::baseURL();
// Get best SubsiteDomain object
$domainObject = $this->getPrimarySubsiteDomain();
if ($domainObject) {
return $domainObject->absoluteBaseURL();
}

// Fall back to the current base url
return Director::absoluteBaseURL();
}

/**
Expand Down
156 changes: 143 additions & 13 deletions code/model/SubsiteDomain.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<?php

/**
* @property text Domain domain name of this subsite. Do not include the URL scheme here
* @property bool IsPrimary Is this the primary subdomain?
* @property string $Domain domain name of this subsite. Can include wildcards. Do not include the URL scheme here
* @property string $Protocol Required protocol (http or https) if only one is supported. 'automatic' implies
* that any links to this subsite should use the current protocol, and that both are supported.
* @property string $SubstitutedDomain Domain name with all wildcards filled in
* @property string $FullProtocol Full protocol including ://
* @property bool $IsPrimary Is this the primary subdomain?
*/
class SubsiteDomain extends DataObject
{
Expand All @@ -12,9 +16,35 @@ class SubsiteDomain extends DataObject
*/
private static $db = array(
"Domain" => "Varchar(255)",
"Protocol" => "Enum('http,https,automatic','automatic')",
"IsPrimary" => "Boolean",
);

/**
* Specifies that this subsite is http only
*/
const PROTOCOL_HTTP = 'http';

/**
* Specifies that this subsite is https only
*/
const PROTOCOL_HTTPS = 'https';

/**
* Specifies that this subsite supports both http and https
*/
const PROTOCOL_AUTOMATIC = 'automatic';

/**
* Get the descriptive title for this domain
*
* @return string
*/
public function getTitle()
{
return $this->Domain;
}

/**
*
* @var array
Expand All @@ -24,14 +54,24 @@ class SubsiteDomain extends DataObject
);

/**
*
* @config
* @var array
*/
private static $summary_fields=array(
private static $summary_fields = array(
'Domain',
'IsPrimary',
);

/**
* @config
* @var array
*/
private static $casting = array(
'SubstitutedDomain' => 'Varchar',
'FullProtocol' => 'Varchar',
'AbsoluteLink' => 'Varchar',
);

/**
* Whenever a Subsite Domain is written, rewrite the hostmap
*
Expand All @@ -48,9 +88,29 @@ public function onAfterWrite()
*/
public function getCMSFields()
{
$protocols = array(
self::PROTOCOL_HTTP => _t('SubsiteDomain.PROTOCOL_HTTP', 'http://'),
self::PROTOCOL_HTTPS => _t('SubsiteDomain.PROTOCOL_HTTPS', 'https://'),
self::PROTOCOL_AUTOMATIC => _t('SubsiteDomain.PROTOCOL_AUTOMATIC', 'Automatic')
);

$fields = new FieldList(
new TextField('Domain', $this->fieldLabel('Domain'), null, 255),
new CheckboxField('IsPrimary', $this->fieldLabel('IsPrimary'))
WildcardDomainField::create('Domain', $this->fieldLabel('Domain'), null, 255)
->setDescription(_t(
'SubsiteDomain.DOMAIN_DESCRIPTION',
'Hostname of this subsite (exclude protocol). Allows wildcards (*).'
)),
OptionsetField::create('Protocol', $this->fieldLabel('Protocol'), $protocols)
->setDescription(_t(
'SubsiteDomain.PROTOCOL_DESCRIPTION',
'When generating links to this subsite, use the selected protocol. <br />' .
'Selecting \'Automatic\' means subsite links will default to the current protocol.'
)),
CheckboxField::create('IsPrimary', $this->fieldLabel('IsPrimary'))
->setDescription(_t(
'SubsiteDomain.PROTOCOL_DESCRIPTION',
'Mark this as the default domain for this subsite'
))
);

$this->extend('updateCMSFields', $fields);
Expand All @@ -66,20 +126,90 @@ public function fieldLabels($includerelations = true)
{
$labels = parent::fieldLabels($includerelations);
$labels['Domain'] = _t('SubsiteDomain.DOMAIN', 'Domain');
$labels['IsPrimary'] = _t('SubsiteDomain.IS_PRIMARY', 'Is Primary Domain');
$labels['Protocol'] = _t('SubsiteDomain.Protocol', 'Protocol');
$labels['IsPrimary'] = _t('SubsiteDomain.IS_PRIMARY', 'Is Primary Domain?');

return $labels;
}

/**
* Before writing the Subsite Domain, strip out any HTML the user has entered.
* @return void
* Get the link to this subsite
*
* @return string
*/
public function onBeforeWrite()
public function Link()
{
parent::onBeforeWrite();
return $this->getFullProtocol() . $this->Domain;
}

//strip out any HTML to avoid XSS attacks
$this->Domain = Convert::html2raw($this->Domain);
/**
* Gets the full protocol (including ://) for this domain
*
* @return string
*/
public function getFullProtocol()
{
switch ($this->Protocol) {
case self::PROTOCOL_HTTPS:
{
return 'https://';
}
case self::PROTOCOL_HTTP:
{
return 'http://';
}
default:
{
return Director::protocol();
}
}
}

/**
* Retrieves domain name with wildcards substituted with actual values
*
* @todo Refactor domains into separate wildcards / primary domains
*
* @return string
*/
public function getSubstitutedDomain()
{
$currentHost = $_SERVER['HTTP_HOST'];

// If there are wildcards in the primary domain (not recommended), make some
// educated guesses about what to replace them with:
$domain = preg_replace('/\.\*$/', ".{$currentHost}", $this->Domain);

// Default to "subsite." prefix for first wildcard
// TODO Whats the significance of "subsite" in this context?!
$domain = preg_replace('/^\*\./', "subsite.", $domain);

// *Only* removes "intermediate" subdomains, so 'subdomain.www.domain.com' becomes 'subdomain.domain.com'
$domain = str_replace('.www.', '.', $domain);

return $domain;
}

/**
* Get absolute link for this domain
*
* @return string
*/
public function getAbsoluteLink()
{
return $this->getFullProtocol() . $this->getSubstitutedDomain();
}

/**
* Get absolute baseURL for this domain
*
* @return string
*/
public function absoluteBaseURL()
{
return Controller::join_links(
$this->getAbsoluteLink(),
Director::baseURL()
);
}
}
20 changes: 17 additions & 3 deletions tests/FileSubsitesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
class FileSubsitesTest extends BaseSubsiteTest
{
public static $fixture_file = 'subsites/tests/SubsiteTest.yml';

/**
* Disable other file extensions
*
* @var array
*/
protected $illegalExtensions = array(
'File' => array(
'SecureFileExtension',
'VersionedFileExtension'
)
);

public function testTrivialFeatures()
{
Expand Down Expand Up @@ -68,12 +80,14 @@ public function testSubsitesFolderDropdown()

$this->assertEquals(array(
'Main site',
'Template',
'Subsite1 Template',
'Subsite2 Template',
'Template',
'Test 1',
'Test 2',
'Test 3'
), $source);
'Test 3',
'Test Non-SSL',
'Test SSL',
), array_values($source));
}
}
Loading

0 comments on commit 1876b78

Please sign in to comment.