Merge pull request #221 from tractorcow/pulls/1.1/fix-https

API Add option to specify http / https on subsite domains
This commit is contained in:
Daniel Hensby 2015-11-24 09:50:39 +00:00
commit 1876b78204
9 changed files with 588 additions and 226 deletions

View File

@ -2,6 +2,8 @@
language: php language: php
sudo: false
php: php:
- 5.4 - 5.4

View File

@ -104,12 +104,17 @@ class SiteTreeSubsites extends DataExtension
// replace readonly link prefix // replace readonly link prefix
$subsite = $this->owner->Subsite(); $subsite = $this->owner->Subsite();
$nested_urls_enabled = Config::inst()->get('SiteTree', 'nested_urls'); $nested_urls_enabled = Config::inst()->get('SiteTree', 'nested_urls');
if ($subsite && $subsite->ID) { if ($subsite && $subsite->exists()) {
$baseUrl = Director::protocol() . $subsite->domain() . '/'; // Use baseurl from domain
$baseLink = $subsite->absoluteBaseURL();
// Add parent page if enabled
if($nested_urls_enabled && $this->owner->ParentID) {
$baseLink = Controller::join_links( $baseLink = Controller::join_links(
$baseUrl, $baseLink,
($nested_urls_enabled && $this->owner->ParentID ? $this->owner->Parent()->RelativeLink(true) : null) $this->owner->Parent()->RelativeLink(true)
); );
}
$urlsegment = $fields->dataFieldByName('URLSegment'); $urlsegment = $fields->dataFieldByName('URLSegment');
$urlsegment->setURLPrefix($baseLink); $urlsegment->setURLPrefix($baseLink);

View File

@ -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';
}
}

View File

@ -758,26 +758,27 @@ class Subsite extends DataObject
*/ */
public function domain() public function domain()
{ {
if ($this->ID) { // Get best SubsiteDomain object
$domains = DataObject::get("SubsiteDomain", "\"SubsiteID\" = $this->ID", "\"IsPrimary\" DESC", "", 1); $domainObject = $this->getPrimarySubsiteDomain();
if ($domains && $domains->Count()>0) { if ($domainObject) {
$domain = $domains->First()->Domain; return $domainObject->SubstitutedDomain;
// 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'] // If there are no objects, default to the current hostname
} else {
return $_SERVER['HTTP_HOST']; 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();
} }
/** /**
@ -790,12 +791,19 @@ class Subsite extends DataObject
} }
/** /**
* * Get the absolute URL for this subsite
* @return string * @return string
*/ */
public function absoluteBaseURL() 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();
} }
/** /**

View File

@ -1,8 +1,12 @@
<?php <?php
/** /**
* @property text Domain domain name of this subsite. Do not include the URL scheme here * @property string $Domain domain name of this subsite. Can include wildcards. Do not include the URL scheme here
* @property bool IsPrimary Is this the primary subdomain? * @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 class SubsiteDomain extends DataObject
{ {
@ -12,9 +16,35 @@ class SubsiteDomain extends DataObject
*/ */
private static $db = array( private static $db = array(
"Domain" => "Varchar(255)", "Domain" => "Varchar(255)",
"Protocol" => "Enum('http,https,automatic','automatic')",
"IsPrimary" => "Boolean", "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 * @var array
@ -24,7 +54,7 @@ class SubsiteDomain extends DataObject
); );
/** /**
* * @config
* @var array * @var array
*/ */
private static $summary_fields = array( private static $summary_fields = array(
@ -32,6 +62,16 @@ class SubsiteDomain extends DataObject
'IsPrimary', 'IsPrimary',
); );
/**
* @config
* @var array
*/
private static $casting = array(
'SubstitutedDomain' => 'Varchar',
'FullProtocol' => 'Varchar',
'AbsoluteLink' => 'Varchar',
);
/** /**
* Whenever a Subsite Domain is written, rewrite the hostmap * Whenever a Subsite Domain is written, rewrite the hostmap
* *
@ -48,9 +88,29 @@ class SubsiteDomain extends DataObject
*/ */
public function getCMSFields() 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( $fields = new FieldList(
new TextField('Domain', $this->fieldLabel('Domain'), null, 255), WildcardDomainField::create('Domain', $this->fieldLabel('Domain'), null, 255)
new CheckboxField('IsPrimary', $this->fieldLabel('IsPrimary')) ->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); $this->extend('updateCMSFields', $fields);
@ -66,20 +126,90 @@ class SubsiteDomain extends DataObject
{ {
$labels = parent::fieldLabels($includerelations); $labels = parent::fieldLabels($includerelations);
$labels['Domain'] = _t('SubsiteDomain.DOMAIN', 'Domain'); $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; return $labels;
} }
/** /**
* Before writing the Subsite Domain, strip out any HTML the user has entered. * Get the link to this subsite
* @return void *
* @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()
);
} }
} }

View File

@ -4,6 +4,18 @@ class FileSubsitesTest extends BaseSubsiteTest
{ {
public static $fixture_file = 'subsites/tests/SubsiteTest.yml'; public static $fixture_file = 'subsites/tests/SubsiteTest.yml';
/**
* Disable other file extensions
*
* @var array
*/
protected $illegalExtensions = array(
'File' => array(
'SecureFileExtension',
'VersionedFileExtension'
)
);
public function testTrivialFeatures() public function testTrivialFeatures()
{ {
$this->assertTrue(is_array(singleton('FileSubsites')->extraStatics())); $this->assertTrue(is_array(singleton('FileSubsites')->extraStatics()));
@ -68,12 +80,14 @@ class FileSubsitesTest extends BaseSubsiteTest
$this->assertEquals(array( $this->assertEquals(array(
'Main site', 'Main site',
'Template',
'Subsite1 Template', 'Subsite1 Template',
'Subsite2 Template', 'Subsite2 Template',
'Template',
'Test 1', 'Test 1',
'Test 2', 'Test 2',
'Test 3' 'Test 3',
), $source); 'Test Non-SSL',
'Test SSL',
), array_values($source));
} }
} }

View File

@ -4,19 +4,36 @@ class SubsiteTest extends BaseSubsiteTest
{ {
public static $fixture_file = 'subsites/tests/SubsiteTest.yml'; public static $fixture_file = 'subsites/tests/SubsiteTest.yml';
/**
* Original value of {@see SubSite::$strict_subdomain_matching}
*
* @var bool
*/
protected $origStrictSubdomainMatching = null;
/**
* Original value of $_REQUEST
*
* @var array
*/
protected $origServer = array();
public function setUp() public function setUp()
{ {
parent::setUp(); parent::setUp();
Config::inst()->update('Director', 'alternate_base_url', '/');
$this->origStrictSubdomainMatching = Subsite::$strict_subdomain_matching; $this->origStrictSubdomainMatching = Subsite::$strict_subdomain_matching;
$this->origServer = $_SERVER;
Subsite::$strict_subdomain_matching = false; Subsite::$strict_subdomain_matching = false;
} }
public function tearDown() public function tearDown()
{ {
parent::tearDown(); $_SERVER = $this->origServer;
Subsite::$strict_subdomain_matching = $this->origStrictSubdomainMatching; Subsite::$strict_subdomain_matching = $this->origStrictSubdomainMatching;
parent::tearDown();
} }
/** /**
@ -239,8 +256,6 @@ class SubsiteTest extends BaseSubsiteTest
$this->assertEquals('two.mysite.com', $this->assertEquals('two.mysite.com',
$this->objFromFixture('Subsite', 'domaintest2')->domain()); $this->objFromFixture('Subsite', 'domaintest2')->domain());
$originalHTTPHost = $_SERVER['HTTP_HOST'];
$_SERVER['HTTP_HOST'] = "www.example.org"; $_SERVER['HTTP_HOST'] = "www.example.org";
$this->assertEquals('three.example.org', $this->assertEquals('three.example.org',
$this->objFromFixture('Subsite', 'domaintest3')->domain()); $this->objFromFixture('Subsite', 'domaintest3')->domain());
@ -251,8 +266,51 @@ class SubsiteTest extends BaseSubsiteTest
$this->assertEquals($_SERVER['HTTP_HOST'], singleton('Subsite')->PrimaryDomain); $this->assertEquals($_SERVER['HTTP_HOST'], singleton('Subsite')->PrimaryDomain);
$this->assertEquals('http://'.$_SERVER['HTTP_HOST'].Director::baseURL(), singleton('Subsite')->absoluteBaseURL()); $this->assertEquals('http://'.$_SERVER['HTTP_HOST'].Director::baseURL(), singleton('Subsite')->absoluteBaseURL());
}
$_SERVER['HTTP_HOST'] = $originalHTTPHost; /**
* Tests that Subsite and SubsiteDomain both respect http protocol correctly
*/
public function testDomainProtocol() {
// domaintest2 has 'protocol'
$subsite2 = $this->objFromFixture('Subsite', 'domaintest2');
$domain2a = $this->objFromFixture('SubsiteDomain', 'dt2a');
$domain2b = $this->objFromFixture('SubsiteDomain', 'dt2b');
// domaintest4 is 'https' (primary only)
$subsite4 = $this->objFromFixture('Subsite', 'domaintest4');
$domain4a = $this->objFromFixture('SubsiteDomain', 'dt4a');
$domain4b = $this->objFromFixture('SubsiteDomain', 'dt4b'); // secondary domain is http only though
// domaintest5 is 'http'
$subsite5 = $this->objFromFixture('Subsite', 'domaintest5');
$domain5a = $this->objFromFixture('SubsiteDomain', 'dt5');
// Check protocol when current protocol is http://
$_SERVER['HTTP_HOST'] = 'www.mysite.com';
$_SERVER['HTTPS'] = '';
$this->assertEquals('http://two.mysite.com/', $subsite2->absoluteBaseURL());
$this->assertEquals('http://two.mysite.com/', $domain2a->absoluteBaseURL());
$this->assertEquals('http://subsite.mysite.com/', $domain2b->absoluteBaseURL());
$this->assertEquals('https://www.primary.com/', $subsite4->absoluteBaseURL());
$this->assertEquals('https://www.primary.com/', $domain4a->absoluteBaseURL());
$this->assertEquals('http://www.secondary.com/', $domain4b->absoluteBaseURL());
$this->assertEquals('http://www.tertiary.com/', $subsite5->absoluteBaseURL());
$this->assertEquals('http://www.tertiary.com/', $domain5a->absoluteBaseURL());
// Check protocol when current protocol is https://
$_SERVER['HTTP_HOST'] = 'www.mysite.com';
$_SERVER['HTTPS'] = 'ON';
$this->assertEquals('https://two.mysite.com/', $subsite2->absoluteBaseURL());
$this->assertEquals('https://two.mysite.com/', $domain2a->absoluteBaseURL());
$this->assertEquals('https://subsite.mysite.com/', $domain2b->absoluteBaseURL());
$this->assertEquals('https://www.primary.com/', $subsite4->absoluteBaseURL());
$this->assertEquals('https://www.primary.com/', $domain4a->absoluteBaseURL());
$this->assertEquals('http://www.secondary.com/', $domain4b->absoluteBaseURL());
$this->assertEquals('http://www.tertiary.com/', $subsite5->absoluteBaseURL());
$this->assertEquals('http://www.tertiary.com/', $domain5a->absoluteBaseURL());
} }
public function testAllSites() public function testAllSites()
@ -265,7 +323,9 @@ class SubsiteTest extends BaseSubsiteTest
array('Title' =>'Subsite2 Template'), array('Title' =>'Subsite2 Template'),
array('Title' =>'Test 1'), array('Title' =>'Test 1'),
array('Title' =>'Test 2'), array('Title' =>'Test 2'),
array('Title' =>'Test 3') array('Title' =>'Test 3'),
array('Title' => 'Test Non-SSL'),
array('Title' => 'Test SSL')
), $subsites, 'Lists all subsites'); ), $subsites, 'Lists all subsites');
} }
@ -301,10 +361,14 @@ class SubsiteTest extends BaseSubsiteTest
'Test 1', 'Test 1',
'Test 2', 'Test 2',
'Test 3', 'Test 3',
), $adminSiteTitles); 'Test Non-SSL',
'Test SSL'
), array_values($adminSiteTitles));
$member2Sites = Subsite::accessible_sites("CMS_ACCESS_CMSMain", false, null, $member2Sites = Subsite::accessible_sites(
$this->objFromFixture('Member', 'subsite1member2')); "CMS_ACCESS_CMSMain", false, null,
$this->objFromFixture('Member', 'subsite1member2')
);
$member2SiteTitles = $member2Sites->column("Title"); $member2SiteTitles = $member2Sites->column("Title");
sort($member2SiteTitles); sort($member2SiteTitles);
$this->assertEquals('Subsite1 Template', $member2SiteTitles[0], 'Member can get to subsite via a group role'); $this->assertEquals('Subsite1 Template', $member2SiteTitles[0], 'Member can get to subsite via a group role');

View File

@ -11,30 +11,54 @@ Subsite:
Title: Test 2 Title: Test 2
domaintest3: domaintest3:
Title: Test 3 Title: Test 3
domaintest4:
Title: 'Test SSL'
domaintest5:
Title: 'Test Non-SSL'
SubsiteDomain: SubsiteDomain:
subsite1: subsite1:
SubsiteID: =>Subsite.subsite1 SubsiteID: =>Subsite.subsite1
Domain: subsite1.* Domain: subsite1.*
Protocol: automatic
subsite2: subsite2:
SubsiteID: =>Subsite.subsite2 SubsiteID: =>Subsite.subsite2
Domain: subsite2.* Domain: subsite2.*
Protocol: automatic
dt1a: dt1a:
SubsiteID: =>Subsite.domaintest1 SubsiteID: =>Subsite.domaintest1
Domain: one.example.org Domain: one.example.org
Protocol: automatic
IsPrimary: 1 IsPrimary: 1
dt1b: dt1b:
SubsiteID: =>Subsite.domaintest1 SubsiteID: =>Subsite.domaintest1
Domain: one.* Domain: one.*
Protocol: automatic
dt2a: dt2a:
SubsiteID: =>Subsite.domaintest2 SubsiteID: =>Subsite.domaintest2
Domain: two.mysite.com Domain: two.mysite.com
Protocol: automatic
IsPrimary: 1 IsPrimary: 1
dt2b: dt2b:
SubsiteID: =>Subsite.domaintest2 SubsiteID: =>Subsite.domaintest2
Domain: *.mysite.com Domain: *.mysite.com
Protocol: automatic
dt3: dt3:
SubsiteID: =>Subsite.domaintest3 SubsiteID: =>Subsite.domaintest3
Domain: three.* Domain: three.*
Protocol: automatic
IsPrimary: 1
dt4a:
SubsiteID: =>Subsite.domaintest4
Domain: www.primary.com
Protocol: https
dt4b:
SubsiteID: =>Subsite.domaintest4
Domain: www.secondary.com
Protocol: http
dt5:
SubsiteID: =>Subsite.domaintest5
Domain: www.tertiary.com
Protocol: http
IsPrimary: 1 IsPrimary: 1
Page: Page:
mainSubsitePage: mainSubsitePage:

View File

@ -0,0 +1,72 @@
<?php
/**
* Tests {@see WildcardDomainField}
*/
class WildcardDomainFieldTest extends SapphireTest {
/**
* Check that valid domains are accepted
*
* @dataProvider validDomains
*/
public function testValidDomains($domain) {
$field = new WildcardDomainField('DomainField');
$this->assertTrue($field->checkHostname($domain), "Validate that {$domain} is a valid domain name");
}
/**
* Check that valid domains are accepted
*
* @dataProvider invalidDomains
*/
public function testInvalidDomains($domain) {
$field = new WildcardDomainField('DomainField');
$this->assertFalse($field->checkHostname($domain), "Validate that {$domain} is an invalid domain name");
}
/**
* Check that valid domains are accepted
*
* @dataProvider validWildcards
*/
public function testValidWildcards($domain) {
$field = new WildcardDomainField('DomainField');
$this->assertTrue($field->checkHostname($domain), "Validate that {$domain} is a valid domain wildcard");
}
public function validDomains() {
return array(
array('www.mysite.com'),
array('domain7'),
array('mysite.co.n-z'),
array('subdomain.my-site.com'),
array('subdomain.mysite')
);
}
public function invalidDomains() {
return array(
array('-mysite'),
array('.mysite'),
array('mys..ite'),
array('mysite-'),
array('mysite.'),
array('-mysite.*'),
array('.mysite.*'),
array('mys..ite.*'),
array('*.mysite-'),
array('*.mysite.')
);
}
public function validWildcards() {
return array(
array('*.mysite.com'),
array('mys*ite.com'),
array('*.my-site.*'),
array('*')
);
}
}