'";
+ //query .= " WHERE `id` = '22' AND `alias` = 'discover'";
+ //$database->setQuery($query);
+ //$database->query();
+
+ //$this->output->addLine('Updating content page id (32)');
+ //$query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/gettingstarted.css\"}\r\n\r\n
\r\n
\r\n
Getting To Know Your Hub
\r\n\r\n
\r\n
\r\n
\r\n
Utilize
\r\n\r\n
View tutorials and read about how to use the numerous features of a HUB. We show you how to add content, customize your dashboard, create groups, and more.
Read the manual for managing the content and functionality of a HUB. It progresses step-by-step through various common tasks and familiarizes you with the administrative interface.
Build your own extensions and discover how to extend or tailor the existing ones to your needs. We try to guide you through the creation steps and provide examples for download.
We\'ve provided a default set of content with a menu of commonly used extensions and pages but there is still some setup and spots needing filled in that we, unfortunately, couldn\'t do for you.
\r\n\r\n
\r\n
\r\n
To-Do List
\r\n
\r\n
\r\n
About You
\r\n
Here\'s a page for telling your story and letting your visitors know all about you. You just have to fill in the details!
\r\n
\r\n
\r\n
How to Contact
\r\n
Sometimes your visitors will need to get ahold of you. We\'ve provided a page to detail contact information to help make that happen.\"
\r\n
\r\n
\r\n
Terms of Use
\r\n
Although we provided a generic Terms of Use, it will need some information filled in and further refinement for your hub.
\r\n
\r\n
\r\n
\r\n
\r\n
Recommended
\r\n
\r\n
\r\n
Facebook, Google, LinkedIn
\r\n
Every hub comes with the ability for users to authenticate with popular services like Facebook, Google, or LinkedIn. Turn one or all of these on with a click of a button!
\r\n
\r\n
\r\n
Set up Analytics
\r\n
We highly recommend setting up Google Analytics on your hub. Luckily, we provide a module for doing just that.
\r\n
\r\n
\r\n
Use ReCAPTCHA
\r\n
While we provide basic image and text CAPTCHAs to help guard against spam bots, we recommend ReCAPTCHA for stronger protection and larger feature set.
\r\n
\r\n\r\n
\r\n
Email
Hubs use outgoing email for account setup and a couple other things. Outgoing email from an Amazon hosted server often gets marked as SPAM by many email providers. It is often necessary to setup your hub to use external email services (such as Mandrill) to ensure that email makes it to users\' inboxes.
We try to make using and configuring a hub a simple, smooth process. Sometimes, however, you may have questions or issues not easily answered by the available documentation. If you need help or support while developing your hub, feel free to contact us.
'";
+ $query .= " WHERE `id` = '22' AND `alias` = 'discover'";
+ $database->setQuery($query);
+ $database->query();
+
+ $this->output->addLine('Updating content page id (32)');
+ $query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/gettingstarted.css\"}\r\n\r\n
\r\n
\r\n
Getting To Know Your Hub
\r\n\r\n
\r\n
\r\n
\r\n
Utilize
\r\n\r\n
View tutorials and read about how to use the numerous features of a HUB. We show you how to add content, customize your dashboard, create groups, and more.
Read the manual for managing the content and functionality of a HUB. It progresses step-by-step through various common tasks and familiarizes you with the administrative interface.
Build your own extensions and discover how to extend or tailor the existing ones to your needs. We try to guide you through the creation steps and provide examples for download.
We\'ve provided a default set of content with a menu of commonly used extensions and pages but there is still some setup and spots needing filled in that we, unfortunately, couldn\'t do for you.
\r\n\r\n
\r\n
\r\n
To-Do List
\r\n
\r\n
\r\n
About You
\r\n
Here\'s a page for telling your story and letting your visitors know all about you. You just have to fill in the details!
\r\n
\r\n
\r\n
How to Contact
\r\n
Sometimes your visitors will need to get ahold of you. We\'ve provided a page to detail contact information to help make that happen.\"
\r\n
\r\n
\r\n
Terms of Use
\r\n
Although we provided a generic Terms of Use, it will need some information filled in and further refinement for your hub.
\r\n
\r\n
\r\n
\r\n
\r\n
Recommended
\r\n
\r\n
\r\n
Facebook, Google, LinkedIn
\r\n
Every hub comes with the ability for users to authenticate with popular services like Facebook, Google, or LinkedIn. Turn one or all of these on with a click of a button!
\r\n
\r\n
\r\n
Set up Analytics
\r\n
We highly recommend setting up Google Analytics on your hub. Luckily, we provide a module for doing just that.
\r\n
\r\n
\r\n
Use ReCAPTCHA
\r\n
While we provide basic image and text CAPTCHAs to help guard against spam bots, we recommend ReCAPTCHA for stronger protection and larger feature set.
\r\n
\r\n\r\n
\r\n
Email
Hubs use outgoing email for account setup and a couple other things. Outgoing email from an Amazon hosted server often gets marked as SPAM by many email providers. It is often necessary to setup your hub to use external email services (such as Mandrill) to ensure that email makes it to users\' inboxes.
We try to make using and configuring a hub a simple, smooth process. Sometimes, however, you may have questions or issues not easily answered by the available documentation. If you need help or support while developing your hub, feel free to contact us.
'";
+ $query .= " WHERE `id` = '22' AND `alias` = 'discover'";
+ $database->setQuery($query);
+ $database->query();
+
+ $this->output->addLine('Updating content page id (32)');
+ $query = "UPDATE `#__content` SET `introtext` = '{xhub:include type=\"stylesheet\" filename=\"pages/gettingstarted.css\"}\r\n\r\n
\r\n
\r\n
Getting To Know Your Hub
\r\n\r\n
\r\n
\r\n
\r\n
Utilize
\r\n\r\n
View tutorials and read about how to use the numerous features of a HUB. We show you how to add content, customize your dashboard, create groups, and more.
Read the manual for managing the content and functionality of a HUB. It progresses step-by-step through various common tasks and familiarizes you with the administrative interface.
Build your own extensions and discover how to extend or tailor the existing ones to your needs. We try to guide you through the creation steps and provide examples for download.
We\'ve provided a default set of content with a menu of commonly used extensions and pages but there is still some setup and spots needing filled in that we, unfortunately, couldn\'t do for you.
\r\n\r\n
\r\n
\r\n
To-Do List
\r\n
\r\n
\r\n
About You
\r\n
Here\'s a page for telling your story and letting your visitors know all about you. You just have to fill in the details!
\r\n
\r\n
\r\n
How to Contact
\r\n
Sometimes your visitors will need to get ahold of you. We\'ve provided a page to detail contact information to help make that happen.\"
\r\n
\r\n
\r\n
Terms of Use
\r\n
Although we provided a generic Terms of Use, it will need some information filled in and further refinement for your hub.
\r\n
\r\n
\r\n
\r\n
\r\n
Recommended
\r\n
\r\n
\r\n
Facebook, Google, LinkedIn
\r\n
Every hub comes with the ability for users to authenticate with popular services like Facebook, Google, or LinkedIn. Turn one or all of these on with a click of a button!
\r\n
\r\n
\r\n
Set up Analytics
\r\n
We highly recommend setting up Google Analytics on your hub. Luckily, we provide a module for doing just that.
\r\n
\r\n
\r\n
Use ReCAPTCHA
\r\n
While we provide basic image and text CAPTCHAs to help guard against spam bots, we recommend ReCAPTCHA for stronger protection and larger feature set.
We try to make using and configuring a hub a simple, smooth process. Sometimes, however, you may have questions or issues not easily answered by the available documentation. If you need help or support while developing your hub, feel free to contact us.
+ id
+ user_id
+ content
+
+ 1
+ 1
+ This is why I want to be a teacher.
+
+
+ 2
+ 1
+ This is why I want to be a computer programmer.
+
+
+ 3
+ 2
+ This is why I don't have Facebook.
+
+
+
+ id
+ name
+
+ 1
+ Fix Stuff
+
+
+ 2
+ Break Stuff
+
+
+ 3
+ Make new Stuff
+
+
+
+ id
+ name
+
+ 1
+ fun stuff
+
+
+ 2
+ smart stuff
+
+
+ 3
+ important stuff
+
+
+
+ id
+ name
+ email
+
+ 1
+ Test User
+ testuser@gmail.com
+
+
+ 2
+ Admin
+ admin@hub.com
+
+
+ 3
+ You
+ you@you.com
+
+
+ 4
+ Me
+ me@me.com
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/test.sqlite3 b/core/libraries/Hubzero/Database/Tests/Fixtures/test.sqlite3
new file mode 100644
index 00000000000..2c622fcc9d4
Binary files /dev/null and b/core/libraries/Hubzero/Database/Tests/Fixtures/test.sqlite3 differ
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussions.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussions.xml
new file mode 100644
index 00000000000..c4caff9ef3f
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussions.xml
@@ -0,0 +1,14 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDelete.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDelete.xml
new file mode 100644
index 00000000000..6abcada5be2
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDelete.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDeleteParent.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDeleteParent.xml
new file mode 100644
index 00000000000..ee40d481b31
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsAfterDeleteParent.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNode.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNode.xml
new file mode 100644
index 00000000000..fc993b0956a
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNode.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNodeByParentId.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNodeByParentId.xml
new file mode 100644
index 00000000000..b32aca30615
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewChildNodeByParentId.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewFirstChild.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewFirstChild.xml
new file mode 100644
index 00000000000..2b6d579dab0
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewFirstChild.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewRootNode.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewRootNode.xml
new file mode 100644
index 00000000000..f6be387c032
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedDiscussionsWithNewRootNode.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Fixtures/updatedUsers.xml b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedUsers.xml
new file mode 100644
index 00000000000..baac409fd66
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Fixtures/updatedUsers.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Bio.php b/core/libraries/Hubzero/Database/Tests/Mock/Bio.php
new file mode 100644
index 00000000000..35af1a8904e
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Mock/Bio.php
@@ -0,0 +1,19 @@
+oneShiftsToMany('Member');
+ }
+
+ /**
+ * Many shifts to many relationship with permissions
+ *
+ * @return \Hubzero\Database\Relationship\ManyShiftsToMany
+ **/
+ public function permissions()
+ {
+ return $this->manyShiftsToMany('Permission');
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Member.php b/core/libraries/Hubzero/Database/Tests/Mock/Member.php
new file mode 100644
index 00000000000..11a99e1572d
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Mock/Member.php
@@ -0,0 +1,38 @@
+shifter();
+ }
+
+ /**
+ * Many shifts to many relationship with permissions
+ *
+ * @return \Hubzero\Database\Relationship\ManyShiftsToMany
+ **/
+ public function permissions()
+ {
+ return $this->manyShiftsToMany('Permission');
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Permission.php b/core/libraries/Hubzero/Database/Tests/Mock/Permission.php
new file mode 100644
index 00000000000..4bedeb2162c
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Mock/Permission.php
@@ -0,0 +1,19 @@
+belongsToOne('Hubzero\Database\Tests\Mock\User');
+ }
+
+ /**
+ * Many to many relationship with tags
+ *
+ * @return \Hubzero\Database\Relationship\ManyToMany
+ **/
+ public function tags()
+ {
+ return $this->manyToMany('Tag');
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Project.php b/core/libraries/Hubzero/Database/Tests/Mock/Project.php
new file mode 100644
index 00000000000..64c183b7c96
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Mock/Project.php
@@ -0,0 +1,38 @@
+oneShiftsToMany('Member');
+ }
+
+ /**
+ * Many shifts to many relationship with permissions
+ *
+ * @return \Hubzero\Database\Relationship\ManyShiftsToMany
+ **/
+ public function permissions()
+ {
+ return $this->manyShiftsToMany('Permission');
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Tests/Mock/Tag.php b/core/libraries/Hubzero/Database/Tests/Mock/Tag.php
new file mode 100644
index 00000000000..ec552f060a7
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/Mock/Tag.php
@@ -0,0 +1,19 @@
+name, ' ')) ? explode(' ', $this->name)[0] : $this->name;
+ }
+
+ /**
+ * Transforms name to a silly nickname
+ *
+ * @return string
+ **/
+ public function transformNickname()
+ {
+ return $this->getFirstName() . 'er';
+ }
+
+ /**
+ * One to many relationship with posts
+ *
+ * @return \Hubzero\Database\Relationship\OneToMany
+ **/
+ public function posts()
+ {
+ return $this->oneToMany('Post');
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Tests/NestedTest.php b/core/libraries/Hubzero/Database/Tests/NestedTest.php
new file mode 100644
index 00000000000..03e50c8cfed
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/NestedTest.php
@@ -0,0 +1,253 @@
+getMockDriver());
+ }
+
+ /**
+ * Tests object construction and variable initialization
+ *
+ * @return void
+ **/
+ public function testConstruct()
+ {
+ $model = Discussion::blank();
+
+ $this->assertInstanceOf('\Hubzero\Database\Nested', $model, 'Model is not an instance of \Hubzero\Database\Nested');
+ $this->assertEquals($model->getModelName(), 'Discussion', 'Model should have a model name of "Discussion"');
+ }
+
+ /**
+ * Tests to make sure we can add a new generic child node
+ *
+ * @return void
+ **/
+ public function testCanAddNewChildNodeFromParentModel()
+ {
+ $parent = Discussion::oneOrFail(1);
+
+ $new = Discussion::blank()->set([
+ 'user_id' => 3,
+ 'title' => 'Testing Stuff',
+ 'content' => 'This is a test additional child node'
+ ]);
+
+ $new->saveAsChildOf($parent);
+
+ // Get the current state of the database
+ $queryTable = $this->getConnection()->createQueryTable(
+ 'discussions', 'SELECT * FROM discussions'
+ );
+
+ // Get our expected state of the database
+ $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsWithNewChildNode.xml')
+ ->getTable('discussions');
+
+ // Now assert that updated and expected are the same
+ $this->assertTablesEqual($expectedTable, $queryTable);
+ }
+
+ /**
+ * Tests to make sure we can add a new generic child node
+ *
+ * @return void
+ **/
+ public function testCanAddNewChildNodeFromParentId()
+ {
+ $new = Discussion::blank()->set([
+ 'user_id' => 3,
+ 'title' => 'More Testing',
+ 'content' => 'This is a node added by parent id'
+ ]);
+
+ $new->saveAsChildOf(3);
+
+ // Get the current state of the database
+ $queryTable = $this->getConnection()->createQueryTable(
+ 'discussions', 'SELECT * FROM discussions'
+ );
+
+ // Get our expected state of the database
+ $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsWithNewChildNodeByParentId.xml')
+ ->getTable('discussions');
+
+ // Now assert that updated and expected are the same
+ $this->assertTablesEqual($expectedTable, $queryTable);
+ }
+
+ /**
+ * Tests to make sure we can add a new first child node
+ *
+ * @return void
+ **/
+ public function testCanAddNewFirstChildNode()
+ {
+ $new = Discussion::blank()->set([
+ 'user_id' => 3,
+ 'title' => 'Left node',
+ 'content' => 'This should be located as the first child of the parent'
+ ]);
+
+ $new->saveAsFirstChildOf(1);
+
+ // Get the current state of the database
+ $queryTable = $this->getConnection()->createQueryTable(
+ 'discussions', 'SELECT * FROM discussions'
+ );
+
+ // Get our expected state of the database
+ $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsWithNewFirstChild.xml')
+ ->getTable('discussions');
+
+ // Now assert that updated and expected are the same
+ $this->assertTablesEqual($expectedTable, $queryTable);
+ }
+
+ /**
+ * Tests to see if we can add a new root node
+ *
+ * @return void
+ **/
+ public function testCanAddNewRootNode()
+ {
+ Discussion::blank()->set([
+ 'user_id' => 3,
+ 'title' => 'This is a new discussion',
+ 'content' => 'Tell me about life',
+ 'scope' => 'group',
+ 'scope_id' => 2
+ ])->saveAsRoot();
+
+ // Get the current state of the database
+ $queryTable = $this->getConnection()->createQueryTable(
+ 'discussions', 'SELECT * FROM discussions'
+ );
+
+ // Get our expected state of the database
+ $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsWithNewRootNode.xml')
+ ->getTable('discussions');
+
+ // Now assert that updated and expected are the same
+ $this->assertTablesEqual($expectedTable, $queryTable);
+ }
+
+ /**
+ * Tests to see if we can get the children of a given parent
+ *
+ * @return void
+ **/
+ public function testCanGetChildren()
+ {
+ // Clear that cache here from our earlier request for the same model
+ \Hubzero\Database\Query::purgeCache();
+
+ $discussion = Discussion::oneOrFail(1);
+ $children = $discussion->getChildren()->raw();
+
+ $this->assertCount(3, $children, 'Discussion 1 should have had 3 children');
+
+ foreach ([2, 3, 5] as $expected)
+ {
+ $this->assertArrayHasKey($expected, $children, "Expected a discussion with id {$expected}");
+ }
+ }
+
+ /**
+ * Tests to see if we can get all the descendants of a given parent
+ *
+ * @return void
+ **/
+ public function testCanGetDescendants()
+ {
+ $discussion = Discussion::oneOrFail(1);
+ $descendants = $discussion->getDescendants()->raw();
+
+ $this->assertCount(4, $descendants, 'Discussion 1 should have had 4 descendants');
+
+ foreach ([2, 3, 4, 5] as $expected)
+ {
+ $this->assertArrayHasKey($expected, $descendants, "Expected a discussion with id {$expected}");
+ }
+ }
+
+ /**
+ * Tests to see if we can get a limited set of the descendants of a given parent
+ *
+ * @return void
+ **/
+ public function testCanGetLimitedDescendants()
+ {
+ $discussion = Discussion::oneOrFail(1);
+ $descendants = $discussion->descendants()->limit(2)->rows()->raw();
+
+ foreach ([2, 5] as $expected)
+ {
+ $this->assertArrayHasKey($expected, $descendants, "Expected a discussion with id {$expected}");
+ }
+ }
+
+ /**
+ * Tests to make sure we can delete a low/bottom level child node (i.e. with no children)
+ *
+ * @return void
+ **/
+ public function testCanDeleteLeafNode()
+ {
+ $discussion = Discussion::oneOrFail(5)->destroy();
+
+ // Get the current state of the database
+ $queryTable = $this->getConnection()->createQueryTable(
+ 'discussions', 'SELECT * FROM discussions'
+ );
+
+ // Get our expected state of the database
+ $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsAfterDelete.xml')
+ ->getTable('discussions');
+
+ // Now assert that updated and expected are the same
+ $this->assertTablesEqual($expectedTable, $queryTable);
+ }
+
+ /**
+ * Tests to make sure we can delete a parent node and cascade the delete as appropriate
+ *
+ * @return void
+ **/
+ public function testCanDeleteParentNode()
+ {
+ $discussion = Discussion::oneOrFail(3)->destroy();
+
+ // Get the current state of the database
+ $queryTable = $this->getConnection()->createQueryTable(
+ 'discussions', 'SELECT * FROM discussions'
+ );
+
+ // Get our expected state of the database
+ $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedDiscussionsAfterDeleteParent.xml')
+ ->getTable('discussions');
+
+ // Now assert that updated and expected are the same
+ $this->assertTablesEqual($expectedTable, $queryTable);
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Tests/QueryTest.php b/core/libraries/Hubzero/Database/Tests/QueryTest.php
new file mode 100644
index 00000000000..e6bcb5cb656
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/QueryTest.php
@@ -0,0 +1,820 @@
+getMockDriver();
+ $query = new Query($dbo);
+
+ // Try to actually fetch some rows
+ $rows = $query->select('*')
+ ->from('users')
+ ->whereEquals('id', '1')
+ ->fetch();
+
+ // Basically, as long as we don't get false here, we're good
+ $this->assertCount(1, $rows, 'Query should have returned one result');
+ }
+
+ /**
+ * Test to make sure we can run a basic insert statement
+ *
+ * @return void
+ **/
+ public function testBasicPush()
+ {
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ // Try to add a new row
+ $query->push('users', [
+ 'name' => 'new user',
+ 'email' => 'newuser@gmail.com'
+ ]);
+
+ // There are 4 default users in the seed data, and adding a new one should a rowcount of 5
+ $this->assertEquals(5, $this->getConnection()->getRowCount('users'), 'Push did not return the expected row count of 5');
+ }
+
+ /**
+ * Test to make sure we can run a basic update statement
+ *
+ * @return void
+ **/
+ public function testBasicAlter()
+ {
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ // Try to update an existing row
+ $query->alter('users', 'id', 1, [
+ 'name' => 'Updated User',
+ 'email' => 'updateduser@gmail.com'
+ ]);
+
+ // Get the current state of the database
+ $queryTable = $this->getConnection()->createQueryTable(
+ 'users', 'SELECT * FROM users'
+ );
+
+ // Get our expected state of the database
+ $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedUsers.xml')
+ ->getTable('users');
+
+ // Now assert that updated and expected are the same
+ $this->assertTablesEqual($expectedTable, $queryTable);
+ }
+
+ /**
+ * Test to make sure we can set a basic value on update
+ *
+ * @return void
+ **/
+ public function testSetWithBasicValue()
+ {
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ // Try to update an existing row
+ $query->alter('users', 'id', 1, [
+ 'name' => new Basic('Updated User'),
+ 'email' => new Basic('updateduser@gmail.com')
+ ]);
+
+ // Get the current state of the database
+ $queryTable = $this->getConnection()->createQueryTable(
+ 'users', 'SELECT * FROM users'
+ );
+
+ // Get our expected state of the database
+ $expectedTable = $this->createFlatXmlDataSet(dirname(__FILE__) . DS . 'Fixtures' . DS . 'updatedUsers.xml')
+ ->getTable('users');
+
+ // Now assert that updated and expected are the same
+ $this->assertTablesEqual($expectedTable, $queryTable);
+ }
+
+ /**
+ * Test to make sure we can run a basic delete statement
+ *
+ * @return void
+ **/
+ public function testBasicRemove()
+ {
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $result = $query->remove('users', null, null);
+ $this->assertFalse($result);
+
+ // Try to update an existing row
+ $query->remove('users', 'id', 1);
+
+ $this->assertEquals(3, $this->getConnection()->getRowCount('users'), 'Remove did not return the expected row count of 3');
+ }
+
+ /**
+ * Test to make sure we can build a query with aliased from statements
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithAliasedFrom()
+ {
+ // Here's the query we're trying to write...
+ $expected = "SELECT * FROM `users` AS `u` WHERE `u`.`name` = 'awesome'";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users', 'u')
+ ->whereEquals('u.name', 'awesome');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build a query with a raw WHERE statement
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithRawWhere()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` WHERE LOWER(`name`)='awesome'";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereRaw('LOWER(`name`)=?', ['awesome']);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $expected = "SELECT * FROM `users` WHERE `name` = 'foo' OR LOWER(`name`)='awesome'";
+
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereEquals('name', 'foo')
+ ->orWhereRaw('LOWER(`name`)=?', ['awesome']);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build a query with where like statements
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithWhereLike()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` WHERE `name` LIKE '%awesome%'";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereLike('name', 'awesome');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $expected = "SELECT * FROM `users` WHERE `name` LIKE '%awesome%' OR `name` LIKE '%amazing%'";
+
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereLike('name', 'awesome')
+ ->orWhereLike('name', 'amazing');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build a query with where IS NULL statements
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithWhereIsNull()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` WHERE `name` IS NULL";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereIsNull('name');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $expected = "SELECT * FROM `users` WHERE `name` = '' OR `name` IS NULL";
+
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereEquals('name', '')
+ ->orWhereIsNull('name');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build a query with where IS NULL statements
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithWhereIsNotNull()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` WHERE `name` IS NOT NULL";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereIsNotNull('name');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $expected = "SELECT * FROM `users` WHERE `name` = 'bar' OR `name` IS NOT NULL";
+
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereEquals('name', 'bar')
+ ->orWhereIsNotNull('name');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build a query with where IS NULL statements
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithWhereIn()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` WHERE `name` IN ('one','two','three')";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereIn('name', ['one', 'two', 'three']);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $expected = "SELECT * FROM `users` WHERE `name` = 'amazing' OR `name` IN ('one','two','three')";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereEquals('name', 'amazing')
+ ->orWhereIn('name', ['one', 'two', 'three']);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $expected = "SELECT * FROM `users` WHERE `name` NOT IN ('one','two','three')";
+
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereNotIn('name', ['one', 'two', 'three']);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $expected = "SELECT * FROM `users` WHERE `name` = 'amazing' OR `name` NOT IN ('one','two','three')";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereEquals('name', 'amazing')
+ ->orWhereNotIn('name', ['one', 'two', 'three']);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build a query with complex nested where statements
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithNestedWheres()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` WHERE (`name` = 'a' OR `name` = 'b' ) AND (`email` = 'c' OR `email` = 'd' )";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->whereEquals('name', 'a', 1)
+ ->orWhereEquals('name', 'b', 1)
+ ->resetDepth(0)
+ ->whereEquals('email', 'c', 1)
+ ->orWhereEquals('email', 'd', 1);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build a query with a raw JOIN statement
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithJoinClause()
+ {
+ $dbo = $this->getMockDriver();
+
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` INNER JOIN posts ON `users`.id = `posts`.user_id";
+
+ $query = new Query($dbo);
+ $query->select('*')
+ ->from('users')
+ ->join('posts', '`users`.id', '`posts`.user_id');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'join Query did not build the expected result');
+
+ $query = new Query($dbo);
+ $query->select('*')
+ ->from('users')
+ ->innerJoin('posts', '`users`.id', '`posts`.user_id');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'innerJoin Query did not build the expected result');
+
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` LEFT JOIN posts ON `users`.id = `posts`.user_id";
+
+ $query = new Query($dbo);
+ $query->select('*')
+ ->from('users')
+ ->leftJoin('posts', '`users`.id', '`posts`.user_id');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'leftJoin Query did not build the expected result');
+
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` RIGHT JOIN posts ON `users`.id = `posts`.user_id";
+
+ $query = new Query($dbo);
+
+ if ($dbo->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'sqlite')
+ {
+ $this->setExpectedException('\Hubzero\Database\Exception\UnsupportedSyntaxException');
+
+ $query->select('*')
+ ->from('users')
+ ->rightJoin('posts', '`users`.id', '`posts`.user_id');
+ }
+ else
+ {
+ $query->select('*')
+ ->from('users')
+ ->rightJoin('posts', '`users`.id', '`posts`.user_id');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'rightJoin Query did not build the expected result');
+ }
+
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` RIGHT JOIN posts ON `users`.id = `posts`.user_id";
+
+ $query = new Query($dbo);
+ $query->select('*')
+ ->from('users')
+ ->rightJoin('posts', '`users`.id', '`posts`.user_id');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'rightJoin Query did not build the expected result');
+
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` FULL JOIN posts ON `users`.id = `posts`.user_id";
+
+ $query = new Query($dbo);
+
+ if ($dbo->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'sqlite')
+ {
+ $this->setExpectedException('\Hubzero\Database\Exception\UnsupportedSyntaxException');
+
+ $query->select('*')
+ ->from('users')
+ ->fullJoin('posts', '`users`.id', '`posts`.user_id');
+ }
+ else
+ {
+ $query->select('*')
+ ->from('users')
+ ->fullJoin('posts', '`users`.id', '`posts`.user_id');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'fullJoin Query did not build the expected result');
+ }
+ }
+
+ /**
+ * Test to make sure we can build a query with a raw JOIN statement
+ *
+ * @return void
+ **/
+ public function testBuildQueryWithRawJoinClause()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `users` INNER JOIN posts ON `users`.id = `posts`.user_id AND `users`.id > 1";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->joinRaw('posts', '`users`.id = `posts`.user_id AND `users`.id > 1');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build an INSERT statement
+ *
+ * @return void
+ **/
+ public function testBuildInsert()
+ {
+ // Here's the query we're trying to write...
+ $expected = "INSERT INTO `users` (`name`) VALUES ('danger')";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->insert('users')
+ ->values(array(
+ 'name' => 'danger'
+ ));
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ if ($dbo->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'sqlite')
+ {
+ $expected = "INSERT OR IGNORE INTO `users` (`name`) VALUES ('awesome')";
+ }
+ else
+ {
+ $expected = "INSERT IGNORE INTO `users` (`name`) VALUES ('awesome')";
+ }
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->insert('users', true)
+ ->values(array(
+ 'name' => 'awesome'
+ ));
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure that fetch properly caches a query
+ *
+ * @return void
+ **/
+ public function testFetchCachesQueriesByDefault()
+ {
+ // Mock a database driver
+ $dbo = $this->getMockDriver();
+
+ // Mock the query builder and tell it we only want to override the query method
+ $query = $this->getMockBuilder('Hubzero\Database\Query')
+ ->setConstructorArgs([$dbo])
+ ->setMethods(['query'])
+ ->getMock();
+
+ // Now set that we should only be calling the query method one time
+ // We also tell it to return something from the query method, otherwise
+ // the cache will fail.
+ $query->expects($this->once())
+ ->method('query')
+ ->willReturn('foo');
+
+ // The query itself here is irrelavent, we just need to make sure
+ // that calling the same query twice doesn't hit the driver twice
+ $query->fetch();
+ $query->fetch();
+ }
+
+ /**
+ * Test to make sure that fetch properly caches a query
+ *
+ * @return void
+ **/
+ public function testFetchDoesNotCacheQueries()
+ {
+ // Mock a database driver
+ $dbo = $this->getMockDriver();
+
+ // Mock the query builder and tell it we only want to override the query method
+ $query = $this->getMockBuilder('Hubzero\Database\Query')
+ ->setConstructorArgs([$dbo])
+ ->setMethods(['query'])
+ ->getMock();
+
+ // Now set that we should be calling the query exactly 2 times
+ // We also tell it to return something from the query method, otherwise
+ // the cache will fail and we could get a false positive.
+ $query->expects($this->exactly(2))
+ ->method('query')
+ ->willReturn('foo');
+
+ // The query itself here is irrelavent, we just need to make sure
+ // that calling fetch results in a call to the query method.
+ // We call it twice to ensure that the result is in the cache.
+ // If the result were not in the cache, we could get a false positive.
+ $query->fetch('rows', true);
+ $query->fetch('rows', true);
+ }
+
+ /**
+ * Test to make sure we can build a query with where IS NULL statements
+ *
+ * @return void
+ **/
+ public function testBuildQueryClear()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `groups`";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('users')
+ ->join('groups', 'created_by', 'id', 'inner')
+ ->whereIsNotNull('name')
+ ->group('cn', 'id')
+ ->having('foo', '=', 3)
+ ->clear('from')
+ ->clear('where')
+ ->clear('join')
+ ->clear('group')
+ ->clear('having')
+ ->from('groups');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ // Here's the query we're try to write...
+ $expected = "SELECT id FROM `users` WHERE `name` IS NOT NULL";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('name')
+ ->from('users')
+ ->whereIsNotNull('name')
+ ->deselect()
+ ->select('id');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ // Here's the query we're try to write...
+ $expected = "INSERT INTO `users` (`name`) VALUES ('Jimmy')";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->insert('groups')
+ ->values(array('cn' => 'lorem'))
+ ->clear('insert')
+ ->clear('values')
+ ->insert('users')
+ ->values(array('name' => 'Jimmy'));
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ // Here's the query we're try to write...
+ $expected = "DELETE FROM `users` WHERE `name` = 'Frank'";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->delete('groups')
+ ->whereEquals('cn', 'lorem')
+ ->clear('delete')
+ ->clear('where')
+ ->delete('users')
+ ->whereEquals('name', 'Frank');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ // Here's the query we're try to write...
+ $expected = "UPDATE `users` SET `name` = 'Frank' WHERE `id` = '2'";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->update('groups')
+ ->set(array('cn' => 'lorem'))
+ ->whereEquals('cn', 'lorem')
+ ->clear('update')
+ ->clear('where')
+ ->update('users')
+ ->set(array('name' => 'Frank'))
+ ->whereEquals('id', 2);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test to make sure we can build a query with LIMIT
+ *
+ * @return void
+ **/
+ public function testBuildQueryLimitStart()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `groups` LIMIT 10";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('groups')
+ ->limit(10);
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $query->start(5);
+
+ $expected = "SELECT * FROM `groups` LIMIT 5,10";
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+ }
+
+ /**
+ * Test that START is an integer
+ *
+ * @return void
+ **/
+ public function testNonNumericStart()
+ {
+ $dbo = $this->getMockDriver();
+
+ $query = new Query($dbo);
+ $query->select('*')
+ ->from('groups')
+ ->start('beginning');
+
+ $expected = "SELECT * FROM `groups`";
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ // NOTE: We directly test the Syntax class as the `start()` method on
+ // the Query class casts values as integers.
+ $syntax = '\\Hubzero\\Database\\Syntax\\' . ucfirst($dbo->getSyntax());
+ $syntax = new $syntax($dbo);
+
+ $this->setExpectedException('InvalidArgumentException');
+
+ $syntax->setStart('beginning');
+ }
+
+ /**
+ * Test that START is greater than or equal to zero
+ *
+ * @return void
+ **/
+ public function testNegativeStart()
+ {
+ $dbo = $this->getMockDriver();
+
+ $query = new Query($dbo);
+
+ $this->setExpectedException('InvalidArgumentException');
+
+ $query->select('*')
+ ->from('groups')
+ ->start(-50);
+
+ /*
+ $syntax = '\\Hubzero\\Database\\Syntax\\' . ucfirst($dbo->getSyntax());
+ $syntax = new $syntax($dbo);
+
+ $this->setExpectedException('InvalidArgumentException');
+
+ $syntax->setStart(-50);
+ */
+ }
+
+ /**
+ * Test that LIMIT is an integer
+ *
+ * @return void
+ **/
+ public function testNonNumericLimit()
+ {
+ $dbo = $this->getMockDriver();
+
+ $query = new Query($dbo);
+ $query->select('*')
+ ->from('groups')
+ ->limit('all');
+
+ $expected = "SELECT * FROM `groups`";
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ // NOTE: We directly test the Syntax class as the `limit()` method on
+ // the Query class casts values as integers.
+ $syntax = '\\Hubzero\\Database\\Syntax\\' . ucfirst($dbo->getSyntax());
+ $syntax = new $syntax($dbo);
+
+ $this->setExpectedException('InvalidArgumentException');
+
+ $syntax->setLimit('all');
+ }
+
+ /**
+ * Test that LIMIT is greater than or equal to zero
+ *
+ * @return void
+ **/
+ public function testNegativeLimit()
+ {
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $this->setExpectedException('InvalidArgumentException');
+
+ $query->select('*')
+ ->from('groups')
+ ->limit(-50);
+ }
+
+ /**
+ * Test to make sure we can build a query with ORDER BY
+ *
+ * @return void
+ **/
+ public function testBuildQueryOrder()
+ {
+ // Here's the query we're try to write...
+ $expected = "SELECT * FROM `groups` ORDER BY `id` ASC";
+
+ $dbo = $this->getMockDriver();
+ $query = new Query($dbo);
+
+ $query->select('*')
+ ->from('groups')
+ ->order('id', 'asc');
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ $query->unorder()
+ ->order('name', 'desc');
+
+ $expected = "SELECT * FROM `groups` ORDER BY `name` DESC";
+
+ $this->assertEquals($expected, str_replace("\n", ' ', $query->toString()), 'Query did not build the expected result');
+
+ // Test that an exception is thrown if order is not asc or desc
+ $this->setExpectedException('InvalidArgumentException');
+
+ $query = new Query($dbo);
+ $query->select('*')
+ ->from('groups')
+ ->order('id', 'foo');
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Tests/RelationalTest.php b/core/libraries/Hubzero/Database/Tests/RelationalTest.php
new file mode 100644
index 00000000000..f0c45c5bf06
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/RelationalTest.php
@@ -0,0 +1,556 @@
+getMockDriver());
+ }
+
+ /**
+ * Tests object construction and variable initialization
+ *
+ * @return void
+ **/
+ public function testConstruct()
+ {
+ $model = User::blank();
+
+ $this->assertInstanceOf('\Hubzero\Database\Relational', $model, 'Model is not an instance of \Hubzero\Database\Relational');
+ $this->assertEquals($model->getModelName(), 'User', 'Model should have a model name of "User"');
+ }
+
+ /**
+ * Tests to make sure a call to a helper function actually finds the function
+ *
+ * @return void
+ **/
+ public function testCallHelper()
+ {
+ $this->assertEquals('Test', User::one(1)->getFirstName(), 'Model should have returned a first name of "Test"');
+ }
+
+ /**
+ * Tests to make sure a call to a transformer actually finds the transformer
+ *
+ * @return void
+ **/
+ public function testCallTransformer()
+ {
+ $this->assertEquals('Tester', User::one(1)->nickname, 'Model should have returned a nickname of "Tester"');
+ }
+
+ /**
+ * Tests to make sure that a result can be retrieved
+ *
+ * @return void
+ **/
+ public function testOneReturnsResult()
+ {
+ $this->assertEquals(1, User::one(1)->id, 'Model should have returned an instance with id of 1');
+ }
+
+ /**
+ * Tests that a call for a non-existant row via oneOrFail method throws an exception
+ *
+ * @expectedException RuntimeException
+ * @return void
+ **/
+ public function testOneOrFailThrowsException()
+ {
+ User::oneOrFail(0);
+ }
+
+ /**
+ * Tests that a request for a non-existant row via oneOrNew method returns new model
+ *
+ * @return void
+ **/
+ public function testOneOrNewCreatesNew()
+ {
+ $this->assertTrue(User::oneOrNew(0)->isNew(), 'Model should have stated that it was new');
+ }
+
+ /**
+ * Tests that a belongsToOne relationship properly grabs the related side of the relationship
+ *
+ * @return void
+ **/
+ public function testBelongsToOneReturnsRelationship()
+ {
+ $this->assertEquals(1, Post::oneOrFail(1)->user->id, 'Model should have returned a user id of 1');
+ }
+
+ /**
+ * Tests that the belongs to one relationship can properly constrain the belongs to side
+ *
+ * @return void
+ **/
+ public function testBelongsToOneCanBeConstrained()
+ {
+ // Get all users that have 2 or more posts - this should return 1 result
+ $posts = Post::all()->whereRelatedHas('user', function($user)
+ {
+ $user->whereEquals('name', 'Test User');
+ })->rows();
+
+ $this->assertCount(2, $posts, 'Model should have returned a count of 2 posts for the user by the name of "Test User"');
+ }
+
+ /**
+ * Tests that a oneToMany relationship properly grabs the many side of the relationship
+ *
+ * @return void
+ **/
+ public function testOneToManyReturnsRelationship()
+ {
+ $this->assertCount(2, User::oneOrFail(1)->posts, 'Model should have returned a count of 2 posts for user 1');
+ }
+
+ /**
+ * Tests that the one side of the relationship can be properly constrained by the many side
+ *
+ * @return void
+ **/
+ public function testOneToManyCanBeConstrainedByCount()
+ {
+ // Get all users that have 2 or more posts - this should return 1 result
+ $users = User::all()->whereRelatedHasCount('posts', 2)->rows();
+
+ $this->assertCount(1, $users, 'Model should have returned a count of 1 user with 2 or more posts');
+ }
+
+ /**
+ * Tests that a manyToMany relationship properly grabs the many side of the relationship
+ *
+ * @return void
+ **/
+ public function testManyToManyReturnsRelationship()
+ {
+ $this->assertCount(3, Post::oneOrFail(1)->tags, 'Model should have returned a count of 3 tags for post 1');
+ }
+
+ /**
+ * Tests that the local/left side of the m2m relationship can be properly constrained by the related/right side
+ *
+ * @return void
+ **/
+ public function testManyToManyCanBeConstrainedByCount()
+ {
+ $posts = Post::all()->whereRelatedHasCount('tags', 3)->rows();
+
+ $this->assertCount(1, $posts, 'Model should have returned a count of 1 post with 3 or more tags');
+ }
+
+ /**
+ * Tests that the local/left side of the m2m relationship can be properly constrained by the related/right side
+ *
+ * @return void
+ **/
+ public function testManyToManyCanBeConstrained()
+ {
+ $posts = Post::all()->whereRelatedHas('tags', function($tags)
+ {
+ $tags->whereEquals('name', 'fun stuff');
+ })->rows();
+
+ $this->assertCount(2, $posts, 'Model should have returned a count of 2 post with the tag "fun stuff"');
+ }
+
+ /**
+ * Tests that a oneShiftsToMany relationship properly grabs the many side of the relationship
+ *
+ * @return void
+ **/
+ public function testOneShiftsToManyReturnsRelationship()
+ {
+ $this->assertCount(3, Group::oneOrFail(1)->members, 'Model should have returned a count of 3 members for group 1');
+ }
+
+ /**
+ * Tests that a manyShiftsToMany relationship properly grabs the many (right) side of the relationship
+ *
+ * @return void
+ **/
+ public function testManyShiftsToManyReturnsRelationship()
+ {
+ $this->assertCount(2, Group::oneOrFail(1)->permissions, 'Model should have returned a count of 2 permissions for group 1');
+ }
+
+ /**
+ * Tests that the local/left side of the os2m relationship can be properly constrained by the related/right side
+ *
+ * @return void
+ **/
+ public function testOneShiftsToManyCanBeConstrainedByCount()
+ {
+ $projects = Project::all()->whereRelatedHasCount('members', 3)->rows();
+
+ $this->assertCount(1, $projects, 'Model should have returned a count of 1 project with 3 or more members');
+ }
+
+ /**
+ * Tests that the local/left side of the ms2m relationship can be properly constrained by the related/right side
+ *
+ * @return void
+ **/
+ public function testManyShiftsToManyCanBeConstrainedByCount()
+ {
+ $projects = Project::all()->whereRelatedHasCount('permissions', 2)->rows();
+
+ $this->assertCount(1, $projects, 'Model should have returned a count of 1 project with 2 or more permissions');
+ }
+
+ /**
+ * Tests that the local/left side of the os2m relationship can be properly constrained by the related/right side
+ *
+ * @return void
+ **/
+ public function testOneShiftsToManyCanBeConstrained()
+ {
+ $projects = Project::all()->whereRelatedHas('members', function($members)
+ {
+ $members->whereEquals('user_id', 3);
+ })->rows();
+
+ $this->assertCount(1, $projects, 'Model should have returned a count of 1 project with a member whose user_id is 3');
+ }
+
+ /**
+ * Tests that the local/left side of the ms2m relationship can be properly constrained by the related/right side
+ *
+ * @return void
+ **/
+ public function testManyShiftsToManyCanBeConstrained()
+ {
+ $projects = Project::all()->whereRelatedHas('permissions', function($permissions)
+ {
+ $permissions->whereEquals('name', 'read');
+ })->rows();
+
+ $this->assertCount(2, $projects, 'Model should have returned a count of 2 project with read permissions');
+ }
+
+ /**
+ * Tests that an including call can properly preload a simple one to many relationship
+ *
+ * @return void
+ **/
+ public function testIncludingOneToManyPreloadsRelationship()
+ {
+ $users = User::all()->including('posts')->rows()->first();
+
+ $this->assertNotNull($users->getRelationship('posts'), 'Model should have had a relationship named posts defined');
+ }
+
+ /**
+ * Tests that an including call can properly preload a one shifts to many relationship
+ *
+ * @return void
+ **/
+ public function testIncludingOneShiftsToManyPreloadsRelationship()
+ {
+ $projects = Project::all()->including('members')->rows()->first();
+
+ $this->assertNotNull($projects->getRelationship('members'), 'Model should have had a relationship named members defined');
+ }
+
+ /**
+ * Tests that an including call can properly preload a many shifts to many relationship
+ *
+ * @return void
+ **/
+ public function testIncludingManyShiftsToManyPreloadsRelationship()
+ {
+ $projects = Project::all()->including('permissions')->rows()->first();
+
+ $this->assertNotNull($projects->getRelationship('permissions'), 'Model should have had a relationship named permissions defined');
+ }
+
+ /**
+ * Tests that an including call can properly preload a simple many to many relationship
+ *
+ * @return void
+ **/
+ public function testIncludingManyToManyPreloadsRelationship()
+ {
+ $posts = Post::all()->including('tags')->rows()->first();
+
+ $this->assertNotNull($posts->getRelationship('tags'), 'Model should have had a relationship named tags defined');
+ }
+
+ /**
+ * Tests that an including call can be constrained on a one to many relationship
+ *
+ * @return void
+ **/
+ public function testIncludingOneToManyCanBeConstrained()
+ {
+ $users = User::all()->including(['posts', function($posts)
+ {
+ $posts->where('content', 'LIKE', '%computer%');
+ }])->rows();
+
+ $this->assertCount(1, $users->seek(1)->posts, 'Model should have had 1 post that met the constraint');
+ $this->assertCount(0, $users->seek(2)->posts, 'Model should have had 0 posts that met the constraint');
+ }
+
+ /**
+ * Tests that an including call can be constrained on a one shifts to many relationship
+ *
+ * @return void
+ **/
+ public function testIncludingOneShiftsToManyCanBeConstrained()
+ {
+ $projects = Project::all()->including(['members', function($posts)
+ {
+ $posts->whereEquals('user_id', 1);
+ }])->rows();
+
+ $this->assertCount(1, $projects->seek(1)->members, 'Model should have had 1 member that met the constraint');
+ $this->assertCount(1, $projects->seek(2)->members, 'Model should have had 1 member that met the constraint');
+ }
+
+ /**
+ * Tests that an including call can be constrained on a many shifts to many relationship
+ *
+ * @return void
+ **/
+ public function testIncludingManyShiftsToManyCanBeConstrained()
+ {
+ $projects = Project::all()->including(['permissions', function($permissions)
+ {
+ $permissions->whereEquals('name', 'read');
+ }])->rows();
+
+ $this->assertCount(1, $projects->seek(1)->permissions, 'Model should have had 1 permission that met the constraint');
+ $this->assertCount(0, $projects->seek(3)->permissions, 'Model should have had 0 permissions that met the constraint');
+ }
+
+ /**
+ * Tests to make sure saving a one to many relationship properly sets the associated field on the related side
+ *
+ * @return void
+ **/
+ public function testSaveOneToManyAssociatesRelated()
+ {
+ User::oneOrFail(1)->posts()->save(['content' => 'This is a test post']);
+
+ $this->assertArrayHasKey('user_id', User::oneOrFail(1)->posts->last(), 'Saved item should have automatically included a user_id');
+ }
+
+ /**
+ * Tests to make sure saving a one shifts to many relationship properly sets the associated fields on the related side
+ *
+ * @return void
+ **/
+ public function testSaveOneShiftsToManyAssociatesRelated()
+ {
+ Project::oneOrFail(1)->members()->save(['user_id' => 2]);
+
+ $this->assertCount(4, Project::oneOrFail(1)->members, 'Saved item should have automatically included a scope and scope_id');
+ }
+
+ /**
+ * Tests to make sure connecting a many to many properly creates the relationship
+ *
+ * @return void
+ **/
+ public function testConnectManyToManyCreatesAssociation()
+ {
+ // Tag post 2 with tag 3
+ Post::oneOrFail(2)->tags()->connect([3]);
+
+ $this->assertCount(2, Post::oneOrFail(2)->tags, 'Post should have had a total of 2 tags');
+ }
+
+ /**
+ * Tests to make sure connecting a many shifts to many properly creates the relationship
+ *
+ * @return void
+ **/
+ public function testConnectManyShiftsToManyCreatesAssociation()
+ {
+ // Tag post 2 with tag 3
+ Member::oneOrFail(1)->permissions()->connect([1]);
+
+ $this->assertCount(3, Member::oneOrFail(1)->permissions, 'Member should have had a total of 3 permissions');
+ }
+
+ /**
+ * Tests to make sure disconnecting a many to many properly destroys the relationship
+ *
+ * @return void
+ **/
+ public function testDisconnectManyToManyDestroysAssociation()
+ {
+ // Tag post 2 with tag 3
+ Post::oneOrFail(2)->tags()->disconnect([3]);
+
+ $this->assertCount(1, Post::oneOrFail(2)->purgeCache()->tags, 'Post should have had a total of 1 tags');
+ }
+
+ /**
+ * Tests to make sure disconnecting a many shifts to many properly destroys the relationship
+ *
+ * @return void
+ **/
+ public function testDisconnectManyShiftsToManyDestroysAssociation()
+ {
+ // Tag post 2 with tag 3
+ Member::oneOrFail(1)->permissions()->disconnect([1]);
+
+ $this->assertCount(2, Member::oneOrFail(1)->purgeCache()->permissions, 'Member should have had a total of 2 permissions');
+ }
+
+ /**
+ * Tests to make sure many to many save automatically connects
+ *
+ * @return void
+ **/
+ public function testManyToManySaveAutomaticallyConnects()
+ {
+ // Tag post 2 with tag 3
+ Post::oneOrFail(2)->tags()->save(['name' => 'automatically created']);
+
+ $this->assertCount(1, Tag::whereEquals('name', 'automatically created')->rows(), 'A tag with the name of "automatically created" should exist');
+ $this->assertCount(2, Post::oneOrFail(2)->purgeCache()->tags, 'Post should have had a total of 2 tags');
+ }
+
+ /**
+ * Tests to make sure many shifts to many save automatically connects
+ *
+ * @return void
+ **/
+ public function testManyShiftsToManySaveAutomaticallyConnects()
+ {
+ // Tag post 2 with tag 3
+ Member::oneOrFail(1)->permissions()->save(['name' => 'do awesome stuff']);
+
+ $this->assertCount(1, Permission::whereEquals('name', 'do awesome stuff')->rows(), 'A permission with the name of "do awesome stuff" should exist');
+ $this->assertCount(3, Member::oneOrFail(1)->purgeCache()->permissions, 'Member should have had a total of 3 permissions');
+ }
+
+ /**
+ * Tests to make sure connecting a many to many can also add additional fields to the intermediary table
+ *
+ * @return void
+ **/
+ public function testConnectManyToManyCanAddAdditionalFields()
+ {
+ // Tag post 2 with tag 4
+ $now = uniqid();
+ Post::oneOrFail(3)->tags()->connect([1 => ['tagged' => $now]]);
+
+ $result = Post::oneOrFail(3)->tags->seek(1);
+
+ $this->assertFalse($result->hasAttribute('tagged'), 'Post should not have had an attributed for "tagged"');
+ $this->assertArrayHasKey('tagged', (array)$result->associated, 'Post should have had an associated key of "tagged"');
+ $this->assertEquals($now, $result->associated->tagged, 'Post tagged date should have equaled ' . $now);
+ }
+
+ /**
+ * Tests to make sure connecting a many shifts to many can also add additional fields to the intermediary table
+ *
+ * @return void
+ **/
+ public function testConnectManyShiftsToManyCanAddAdditionalFields()
+ {
+ $now = uniqid();
+ Group::oneOrFail(1)->permissions()->connect([3 => ['permitted' => $now]]);
+
+ $result = Group::oneOrFail(1)->permissions->seek(3);
+
+ $this->assertFalse($result->hasAttribute('permitted'), 'Group should not have had an attributed for "permitted"');
+ $this->assertArrayHasKey('permitted', (array)$result->associated, 'Group should have had an associated key of "permitted"');
+ $this->assertEquals($now, $result->associated->permitted, 'Group permitted date should have equaled ' . $now);
+ }
+
+ /**
+ * Tests to make sure we can retrieve the shifter of a many to many shifting relationship
+ *
+ * @return void
+ **/
+ public function testOneShiftsToManyCanGetShifter()
+ {
+ $group = Member::oneOrFail(1)->memberable;
+
+ $this->assertEquals($group->id, '1', 'Member should have returned a group association id of 1');
+ }
+
+ /**
+ * Tests to make sure we can get relationships defined on a model
+ *
+ * @return void
+ **/
+ public function testCanIntrospectRelationships()
+ {
+ $this->assertEquals(['posts'], User::introspectRelationships(), 'User should have had one relationships of "posts"');
+ }
+
+ /**
+ * Tests to make sure we can add a dynamic relationship at runtime
+ *
+ * @return void
+ **/
+ public function testCanAddRuntimeRelationship()
+ {
+ User::registerRelationship('bio', function ($model)
+ {
+ return $model->oneToOne('Bio');
+ });
+
+ $this->assertEquals('This is my bio about me.', User::oneOrFail(1)->bio->text, 'Bio call should have returned the bio for user 1');
+ $this->assertEquals('This is my bio about me.', User::oneOrFail(1)->bio()->rows()->text, 'Bio call should have returned the bio for user 1');
+ }
+
+ /**
+ * Tests to make sure we can get relationships defined on a model, including runtime relationships
+ *
+ * @return void
+ **/
+ public function testCanIntrospectRelationshipsIncludingRuntimeRelationships()
+ {
+ // Note that we're expecting the relationship registered in the previous test
+ $this->assertEquals(['posts', 'bio'], User::introspectRelationships(), 'User should have had two relationships of "posts" and "bio"');
+ }
+
+ /**
+ * Tests to make sure we can serialize and unserialize a model
+ *
+ * @return void
+ **/
+ public function testCanDoSimpleSerialization()
+ {
+ $user = User::oneOrFail(1);
+
+ $serialized = serialize($user);
+ $unserialized = unserialize($serialized);
+
+ $this->assertEquals($user, $unserialized, 'Unserialize did not result in the original model');
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Tests/RulesTest.php b/core/libraries/Hubzero/Database/Tests/RulesTest.php
new file mode 100644
index 00000000000..1937ed8dd9b
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Tests/RulesTest.php
@@ -0,0 +1,160 @@
+ 'Me'], ['name' => 'notempty']);
+ $fail = Rules::validate(['name' => ''], ['name' => 'notempty']);
+
+ $this->assertTrue($pass, 'Rule (notempty) should have validated with a name of "Me"');
+ $this->assertCount(1, $fail, 'Rule (notempty) should have returned one error for empty name');
+ }
+
+ /**
+ * Test to make sure a negative number fails validation
+ *
+ * @return void
+ **/
+ public function testPositive()
+ {
+ $pass = Rules::validate(['age' => 25], ['age' => 'positive']);
+ $fail = Rules::validate(['age' => -1], ['age' => 'positive']);
+
+ $this->assertTrue($pass, 'Rule (positive) should have validated with an age of 25');
+ $this->assertCount(1, $fail, 'Rule (positive) should have returned one error for an age of -1');
+ }
+
+ /**
+ * Test to make sure a zero fails validation
+ *
+ * @return void
+ **/
+ public function testNonzero()
+ {
+ $passPos = Rules::validate(['score' => 1], ['score' => 'nonzero']);
+ $passNeg = Rules::validate(['score' => -1], ['score' => 'nonzero']);
+ $fail = Rules::validate(['score' => 0], ['score' => 'nonzero']);
+
+ $this->assertTrue($passPos, 'Rule (nonzero) should have validated with a score of 1');
+ $this->assertTrue($passNeg, 'Rule (nonzero) should have validated with a score of -1');
+ $this->assertCount(1, $fail, 'Rule (nonzero) should have returned one error for a score of 0');
+ }
+
+ /**
+ * Test to make sure a number fails validation
+ *
+ * @return void
+ **/
+ public function testAlpha()
+ {
+ $pass = Rules::validate(['name' => "John Awesome"], ['name' => 'alpha']);
+ $fail = Rules::validate(['name' => "MI6"], ['name' => 'alpha']);
+
+ $this->assertTrue($pass, 'Rule (alpha) should have validated with a name of "John Awesome"');
+ $this->assertCount(1, $fail, 'Rule (alpha) should have returned one error for a name of "MI6"');
+ }
+
+ /**
+ * Test to make sure a bad phone fails validation
+ *
+ * @return void
+ **/
+ public function testPhone()
+ {
+ $pass1 = Rules::validate(['phone' => "765-494-4000"], ['phone' => 'phone']);
+ $pass2 = Rules::validate(['phone' => "(765) 494-4000"], ['phone' => 'phone']);
+ $pass3 = Rules::validate(['phone' => "7654944000"], ['phone' => 'phone']);
+ $fail1 = Rules::validate(['phone' => "12345"], ['phone' => 'phone']);
+ $fail2 = Rules::validate(['phone' => "123-456-7890"], ['phone' => 'phone']);
+
+ $this->assertTrue($pass1, 'Rule (phone) should have validated with a phone of "765-494-4000"');
+ $this->assertTrue($pass2, 'Rule (phone) should have validated with a phone of "(765) 494-4000"');
+ $this->assertTrue($pass3, 'Rule (phone) should have validated with a phone of "7654944000"');
+ $this->assertCount(1, $fail1, 'Rule (phone) should have returned one error for a phone of "12345"');
+ $this->assertCount(1, $fail2, 'Rule (phone) should have returned one error for a phone of "123-456-7890"');
+ }
+
+ /**
+ * Test to make sure an improper email fails validation
+ *
+ * @return void
+ **/
+ public function testEmail()
+ {
+ $pass = Rules::validate(['email' => "you@gmail.com"], ['email' => 'email']);
+ $fail = Rules::validate(['email' => "me.com"], ['email' => 'email']);
+
+ $this->assertTrue($pass, 'Rule (email) should have validated with a email of "you@gmail.com"');
+ $this->assertCount(1, $fail, 'Rule (email) should have returned one error for a email of "me.com"');
+ }
+
+ /**
+ * Test to make sure an empty string fails validation
+ *
+ * @return void
+ **/
+ public function testCompoundRules()
+ {
+ $pass = Rules::validate(['name' => "mr cool"], ['name' => 'notempty|alpha']);
+ $fail = Rules::validate(['name' => ""], ['name' => 'notempty|alpha']);
+
+ $this->assertTrue($pass, 'Rule (notempty|alpha) should have validated with a name of "mr cool"');
+ $this->assertCount(2, $fail, 'Rules (notempty|alpha) should have returned two errors for a name of ""');
+ }
+
+ /**
+ * Test to make sure partial failure still results in failure
+ *
+ * @return void
+ **/
+ public function testPartialFailure()
+ {
+ $fail = Rules::validate(['name' => "Mr. Awesome"], ['name' => 'notempty|alpha']);
+
+ $this->assertCount(1, $fail, 'Rules (notempty|alpha) should have returned one error for a name of "Mr. Awesome"');
+ }
+
+ /**
+ * Test to make sure custom validation rules result in failure
+ *
+ * @return void
+ **/
+ public function testCustomRules()
+ {
+ $endAfterStart = function($data)
+ {
+ return $data['end'] > $data['start'] ? false : 'The end must be after the beginning';
+ };
+
+ $fail = Rules::validate(['start' => '2015-07-01 00:00:00', 'end' => '2015-06-01 00:00:00'], ['end' => $endAfterStart]);
+
+ $beSquare = function($data)
+ {
+ return $data['height'] == $data['width'] ? false : 'It\'s not a square!';
+ };
+
+ $pass = Rules::validate(['height' => 5, 'width' => 5], ['width' => $beSquare]);
+
+ $this->assertCount(1, $fail, 'Rules (custom) should have returned one error for an end date before the beginning date');
+ $this->assertTrue($pass, 'Rule (custom) should have validated with an equal height and width');
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Traits/ErrorBag.php b/core/libraries/Hubzero/Database/Traits/ErrorBag.php
new file mode 100644
index 00000000000..5a3f70b3b63
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Traits/ErrorBag.php
@@ -0,0 +1,69 @@
+errors = $errors;
+ return $this;
+ }
+
+ /**
+ * Adds error to the existing set
+ *
+ * @param string $error The error to add
+ * @return $this
+ * @since 2.0.0
+ **/
+ public function addError($error)
+ {
+ $this->errors[] = $error;
+ return $this;
+ }
+
+ /**
+ * Returns all errors
+ *
+ * @return array
+ * @since 2.0.0
+ **/
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+
+ /**
+ * Returns the first error
+ *
+ * @return string
+ * @since 2.0.0
+ **/
+ public function getError()
+ {
+ return (isset($this->errors[0])) ? $this->errors[0] : '';
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Value/Basic.php b/core/libraries/Hubzero/Database/Value/Basic.php
new file mode 100644
index 00000000000..7ff1426db08
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Value/Basic.php
@@ -0,0 +1,47 @@
+content = $content;
+ }
+
+ /**
+ * Builds the given string representation of the value object
+ *
+ * @param object $syntax The syntax object with which the query is being built
+ * @return string
+ * @since 2.1.0
+ **/
+ public function build($syntax)
+ {
+ $syntax->bind(is_string($this->content) ? trim($this->content) : $this->content);
+
+ return '?';
+ }
+}
diff --git a/core/libraries/Hubzero/Database/Value/Raw.php b/core/libraries/Hubzero/Database/Value/Raw.php
new file mode 100644
index 00000000000..9dfd6078410
--- /dev/null
+++ b/core/libraries/Hubzero/Database/Value/Raw.php
@@ -0,0 +1,26 @@
+content;
+ }
+}
diff --git a/core/libraries/Hubzero/Debug/Dumper.php b/core/libraries/Hubzero/Debug/Dumper.php
new file mode 100644
index 00000000000..e5355f51d95
--- /dev/null
+++ b/core/libraries/Hubzero/Debug/Dumper.php
@@ -0,0 +1,265 @@
+ '',
+ '_' => '',
+ ' ' => '',
+ '\\' => '',
+ '/' => ''
+ );
+
+ /**
+ * Constructor
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ $this->clear();
+ }
+
+ /**
+ * Returns a reference to a global object, only creating it
+ * if it doesn't already exist.
+ *
+ * @return object
+ */
+ public static function &getInstance()
+ {
+ static $instance;
+
+ if (!$instance)
+ {
+ $instance = new self();
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Returns the renderer
+ *
+ * @return object Renderer\Renderable
+ */
+ public function getRenderer()
+ {
+ return $this->_renderer;
+ }
+
+ /**
+ * Sets the renderer
+ *
+ * @param mixed $renderer string or Renderable
+ * @return object
+ */
+ public function setRenderer($renderer)
+ {
+ if (is_string($renderer))
+ {
+ $cName = $this->scrub($renderer);
+
+ $invokable = __NAMESPACE__ . '\\Dumper\\' . ucfirst($cName);
+
+ if (!class_exists($invokable))
+ {
+ throw new RendererNotFoundException(sprintf(
+ '%s: failed retrieving renderer via invokable class "%s"; class does not exist',
+ __CLASS__ . '::' . __FUNCTION__,
+ $invokable
+ ));
+ }
+ $renderer = new $invokable;
+ }
+
+ if (!($renderer instanceof Renderable))
+ {
+ throw new InvalidArgumentException(sprintf(
+ '%s was unable to fetch renderer or renderer was not an instance of %s',
+ get_class($this) . '::' . __FUNCTION__,
+ __NAMESPACE__ . '\\Dumper\\Renderable'
+ ));
+ }
+
+ $this->_renderer = $renderer;
+
+ return $this;
+ }
+
+ /**
+ * Canonicalize name
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function scrub($name)
+ {
+ // this is just for performance instead of using str_replace
+ return strtolower(strtr($name, $this->_nameReplacements));
+ }
+
+ /**
+ * Adds a message
+ *
+ * @param mixed $message
+ * @param string $label
+ * @return object
+ */
+ public function addVar($var)
+ {
+ $varc = (is_object($var) ? clone $var : $var);
+ $this->_messages[] = array(
+ 'var' => $varc,
+ 'time' => microtime(true)
+ );
+
+ return $this;
+ }
+
+ /**
+ * Get a list of messages
+ *
+ * @return array
+ */
+ public function messages()
+ {
+ $messages = $this->_messages;
+
+ // sort messages by their timestamp
+ usort($messages, function($a, $b)
+ {
+ if ($a['time'] === $b['time'])
+ {
+ return 0;
+ }
+ return $a['time'] < $b['time'] ? -1 : 1;
+ });
+
+ return $messages;
+ }
+
+ /**
+ * Deletes all messages
+ *
+ * @return void
+ */
+ public function clear()
+ {
+ $this->_messages = array();
+ }
+
+ /**
+ * Get a count and list of messages
+ *
+ * @return array
+ */
+ public function collect()
+ {
+ $messages = $this->messages();
+
+ return array(
+ 'count' => count($messages),
+ 'messages' => $messages
+ );
+ }
+
+ /**
+ * Does the log have any messages?
+ *
+ * @return integer
+ */
+ public function hasMessages()
+ {
+ return count($this->messages());
+ }
+
+ /**
+ * Render
+ *
+ * @param object $renderer
+ * @return string
+ */
+ public function render($renderer=null)
+ {
+ if (!$renderer)
+ {
+ $renderer = php_sapi_name() === 'cli' ? 'console' : 'html';
+ }
+
+ return $this->setRenderer($renderer)
+ ->getRenderer()
+ ->render($this->messages());
+ }
+
+ /**
+ * Adds a message
+ *
+ * @param mixed $var
+ * @return void
+ */
+ public static function log($var)
+ {
+ $console = self::getInstance();
+ $console->addVar($var);
+ }
+
+ /**
+ * Dumps a var
+ *
+ * @param mixed $var
+ * @param string $to
+ * @return void
+ */
+ public static function dump($var, $to=null)
+ {
+ $console = self::getInstance();
+ $console->addVar($var);
+
+ echo $console->render($to);
+ $console->clear();
+ }
+
+ /**
+ * Dumps a var and dies();
+ *
+ * @param mixed $var
+ * @param string $to
+ * @return void
+ */
+ public static function stop($var, $to=null)
+ {
+ self::dump($var, $to);
+ die();
+ }
+}
diff --git a/core/libraries/Hubzero/Debug/Dumper/AbstractRenderer.php b/core/libraries/Hubzero/Debug/Dumper/AbstractRenderer.php
new file mode 100644
index 00000000000..5c7a87b0110
--- /dev/null
+++ b/core/libraries/Hubzero/Debug/Dumper/AbstractRenderer.php
@@ -0,0 +1,114 @@
+setMessages($messages);
+ }
+ }
+
+ /**
+ * Returns renderer name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return '__abstract__';
+ }
+
+ /**
+ * Get the list of messages
+ *
+ * @return array
+ */
+ public function getMessages()
+ {
+ return $this->_messages;
+ }
+
+ /**
+ * Set the list of messages
+ *
+ * @param mixed $messages
+ * @return object
+ */
+ public function setMessages($messages)
+ {
+ if (!is_array($messages))
+ {
+ throw new InvalidArgumentException(sprintf(
+ 'Messages must be an array. Type of "%s" passed.',
+ gettype($messages)
+ ));
+ }
+
+ $this->_messages = $messages;
+
+ return $this;
+ }
+
+ /**
+ * Render a list of messages
+ *
+ * @param array $messages
+ * @return string
+ */
+ public function render($messages = null)
+ {
+ if ($messages)
+ {
+ $this->setMessages($messages);
+ }
+
+ $messages = $this->getMessages();
+
+ $output = array();
+ foreach ($messages as $item)
+ {
+ $output[] = print_r($item['var'], true);
+ }
+
+ return implode("\n", $output);
+ }
+
+ /**
+ * Turn an array into a pretty print format
+ *
+ * @param array $arr
+ * @return string
+ */
+ protected function _deflate($arr)
+ {
+ $arr = str_replace(array("\n", "\r", "\t"), ' ', $arr);
+ return preg_replace('/\s+/', ' ', $arr);
+ }
+}
diff --git a/core/libraries/Hubzero/Debug/Dumper/Console.php b/core/libraries/Hubzero/Debug/Dumper/Console.php
new file mode 100644
index 00000000000..d5340ed4b44
--- /dev/null
+++ b/core/libraries/Hubzero/Debug/Dumper/Console.php
@@ -0,0 +1,73 @@
+setMessages($messages);
+ }
+
+ $messages = $this->getMessages();
+
+ echo '-----';
+ foreach ($messages as $item)
+ {
+ echo print_r($item['var'], true); //$this->_deflate($item['var']);
+ }
+ echo '-----';
+ }
+
+ /**
+ * Turn an array into a pretty print format
+ *
+ * @param array $arr
+ * @return string
+ */
+ protected function _deflate($arr)
+ {
+ $output = 'Array( ' . "\n";
+ $a = array();
+ foreach ($arr as $key => $val)
+ {
+ if (is_array($val))
+ {
+ $a[] = "\t" . $key . ' => ' . $this->_deflate($val);
+ }
+ else
+ {
+ $a[] = "\t" . $key . ' => ' . htmlentities($val, ENT_COMPAT, 'UTF-8') . '';
+ }
+ }
+ $output .= implode(", \n", $a) . "\n" . ' )' . "\n";
+
+ return $output;
+ }
+}
diff --git a/core/libraries/Hubzero/Debug/Dumper/Html.php b/core/libraries/Hubzero/Debug/Dumper/Html.php
new file mode 100644
index 00000000000..fb0a8e616ae
--- /dev/null
+++ b/core/libraries/Hubzero/Debug/Dumper/Html.php
@@ -0,0 +1,100 @@
+setMessages($messages);
+ }
+
+ $messages = $this->getMessages();
+
+ $output = array();
+ $output[] = '
';
+ }
+
+ return implode("\n", $html);
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Fields/Menuitem.php b/core/libraries/Hubzero/Form/Fields/Menuitem.php
new file mode 100644
index 00000000000..cd3e46d0cac
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Fields/Menuitem.php
@@ -0,0 +1,88 @@
+element['menu_type'];
+ $published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array();
+ $disable = $this->element['disable'] ? explode(',', (string) $this->element['disable']) : array();
+ $language = $this->element['language'] ? explode(',', (string) $this->element['language']) : array();
+
+ // Get the menu items.
+ $items = array();
+ if (file_exists(PATH_CORE . '/components/com_menus/helpers/menus.php'))
+ {
+ // Import the com_menus helper.
+ require_once PATH_CORE . '/components/com_menus/helpers/menus.php';
+
+ $items = \Components\Menus\Helpers\Menus::getMenuLinks($menuType, 0, 0, $published, $language);
+ }
+
+ // Build group for a specific menu type.
+ if ($menuType)
+ {
+ // Initialize the group.
+ $groups[$menuType] = array();
+
+ // Build the options array.
+ foreach ($items as $link)
+ {
+ $groups[$menuType][] = Dropdown::option($link->value, $link->text, 'value', 'text', in_array($link->type, $disable));
+ }
+ }
+ // Build groups for all menu types.
+ else
+ {
+ // Build the groups arrays.
+ foreach ($items as $menu)
+ {
+ // Initialize the group.
+ $groups[$menu->menutype] = array();
+
+ // Build the options array.
+ foreach ($menu->links as $link)
+ {
+ $groups[$menu->menutype][] = Dropdown::option(
+ $link->value, $link->text, 'value', 'text',
+ in_array($link->type, $disable)
+ );
+ }
+ }
+ }
+
+ // Merge any additional groups in the XML definition.
+ $groups = array_merge(parent::getGroups(), $groups);
+
+ return $groups;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Fields/Modulelayout.php b/core/libraries/Hubzero/Form/Fields/Modulelayout.php
new file mode 100644
index 00000000000..bbadb6925ac
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Fields/Modulelayout.php
@@ -0,0 +1,234 @@
+element['client_id'];
+
+ if (is_null($clientId) && $this->form instanceof Form)
+ {
+ $clientId = $this->form->getValue('client_id');
+ }
+ $clientId = (int) $clientId;
+
+ $client = ClientManager::client($clientId);
+
+ // Get the module.
+ $module = (string) $this->element['module'];
+
+ if (empty($module) && ($this->form instanceof Form))
+ {
+ $module = $this->form->getValue('module');
+ }
+
+ $module = preg_replace('#\W#', '', $module);
+
+ // Get the template.
+ $template = (string) $this->element['template'];
+ $template = preg_replace('#\W#', '', $template);
+
+ // Get the style.
+ if ($this->form instanceof Form)
+ {
+ $template_style_id = $this->form->getValue('template_style_id');
+ }
+
+ $template_style_id = preg_replace('#\W#', '', $template_style_id);
+
+ // If an extension and view are present build the options.
+ if ($module && $client)
+ {
+
+ // Load language file
+ $lang = App::get('language');
+ $lang->load($module . '.sys', PATH_APP . '/modules/' . $module, null, false, true)
+ || $lang->load($module . '.sys', PATH_CORE . '/modules/' . $module, null, false, true);
+
+ // Get the database object and a new query object.
+ $db = App::get('db');
+
+ // Build the query.
+ $query = $db->getQuery()
+ ->select('e.element')
+ ->select('e.name')
+ ->from('#__extensions', 'e')
+ ->whereEquals('e.client_id', (int) $clientId)
+ ->whereEquals('e.type', 'template')
+ ->whereEquals('e.enabled', '1');
+
+ if ($template)
+ {
+ $query->whereEquals('e.element', $template);
+ }
+
+ if ($template_style_id)
+ {
+ $query
+ ->join('#__template_styles as s', 's.template', 'e.element', 'left')
+ ->whereEquals('s.id', (int) $template_style_id);
+ }
+
+ // Set the query and load the templates.
+ $db->setQuery($query->toString());
+ $templates = $db->loadObjectList('element');
+
+ // Check for a database error.
+ if ($db->getErrorNum())
+ {
+ App::abort(500, $db->getErrorMsg());
+ }
+
+ $paths = array(PATH_APP, PATH_CORE);
+
+ foreach ($paths as $path)
+ {
+ if (is_dir($path . '/modules/' . $module))
+ {
+ break;
+ }
+ }
+
+ // Build the search paths for module layouts.
+ $module_path = Util::normalizePath($path . '/modules/' . $module . '/tmpl');
+
+ // Prepare array of component layouts
+ $module_layouts = array();
+
+ // Prepare the grouped list
+ $groups = array();
+
+ // Add the layout options from the module path.
+ if (is_dir($module_path) && ($module_layouts = App::get('filesystem')->files($module_path, '^[^_]*\.php$')))
+ {
+ // Create the group for the module
+ $groups['_'] = array();
+ $groups['_']['id'] = $this->id . '__';
+ $groups['_']['text'] = $lang->txt('JOPTION_FROM_MODULE');
+ $groups['_']['items'] = array();
+
+ foreach ($module_layouts as $file)
+ {
+ // Add an option to the module group
+ $value = App::get('filesystem')->name(ltrim($file, DIRECTORY_SEPARATOR));
+ $text = $lang->hasKey($key = strtoupper($module . '_LAYOUT_' . $value)) ? $lang->txt($key) : $value;
+ $groups['_']['items'][] = Dropdown::option('_:' . $value, $text);
+ }
+ }
+
+ // Loop on all templates
+ if ($templates)
+ {
+ foreach ($templates as $template)
+ {
+ $template->path = '';
+
+ foreach ($paths as $p)
+ {
+ if (is_dir($p . '/templates/' . $template->element))
+ {
+ $template->path = $p . '/templates/' . $template->element;
+ break;
+ }
+ }
+
+ if (!$template->path)
+ {
+ continue;
+ }
+
+ // Load language file
+ $lang->load('tpl_' . $template->element . '.sys', $template->path, null, false, true);
+
+ $template_path = Util::normalizePath($template->path . '/html/' . $module);
+
+ // Add the layout options from the template path.
+ if (is_dir($template_path) && ($files = App::get('filesystem')->files($template_path, '^[^_]*\.php$')))
+ {
+ foreach ($files as $i => $file)
+ {
+ // Remove layout that already exist in component ones
+ if (in_array($file, $module_layouts))
+ {
+ unset($files[$i]);
+ }
+ }
+
+ if (count($files))
+ {
+ // Create the group for the template
+ $groups[$template->element] = array();
+ $groups[$template->element]['id'] = $this->id . '_' . $template->element;
+ $groups[$template->element]['text'] = $lang->txt('JOPTION_FROM_TEMPLATE', $template->name);
+ $groups[$template->element]['items'] = array();
+
+ foreach ($files as $file)
+ {
+ // Add an option to the template group
+ $value = App::get('filesystem')->name(ltrim($file, DIRECTORY_SEPARATOR));
+ $text = $lang->hasKey($key = strtoupper('TPL_' . $template->element . '_' . $module . '_LAYOUT_' . $value))
+ ? $lang->txt($key)
+ : $value;
+ $groups[$template->element]['items'][] = Dropdown::option($template->element . ':' . $value, $text);
+ }
+ }
+ }
+ }
+ }
+ // Compute attributes for the grouped list
+ $attr = $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : '';
+
+ // Prepare HTML code
+ $html = array();
+
+ // Compute the current selected values
+ $selected = array($this->value);
+
+ // Add a grouped list
+ $html[] = Dropdown::groupedlist(
+ $groups,
+ $this->name,
+ array(
+ 'id' => $this->id,
+ 'group.id' => 'id',
+ 'list.attr' => $attr,
+ 'list.select' => $selected
+ )
+ );
+
+ return implode($html);
+ }
+
+ return '';
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Fields/Number.php b/core/libraries/Hubzero/Form/Fields/Number.php
new file mode 100644
index 00000000000..68eb6dcabb2
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Fields/Number.php
@@ -0,0 +1,65 @@
+ 'number',
+ 'value' => htmlspecialchars($this->value, ENT_COMPAT, 'UTF-8'),
+ 'name' => $this->name,
+ 'id' => $this->id,
+ 'min' => (!is_null($this->element['min']) ? (int) $this->element['min'] : null),
+ 'max' => ($this->element['max'] ? (int) $this->element['max'] : null),
+ 'step' => ($this->element['step'] ? (int) $this->element['step'] : null),
+ 'pattern' => ($this->element['pattern'] ? $this->element['pattern'] : null),
+ 'class' => ($this->element['class'] ? (string) $this->element['class'] : null),
+ 'readonly' => ((string) $this->element['readonly'] == 'true' ? 'readonly' : null),
+ 'disabled' => ((string) $this->element['disabled'] == 'true' ? 'disabled' : null),
+ 'onchange' => ($this->element['onchange'] ? (string) $this->element['onchange'] : null)
+ );
+
+ $attr = array();
+ foreach ($attributes as $key => $value)
+ {
+ if ($key != 'value' && $key != 'min' && !$value)
+ {
+ continue;
+ }
+ if ($key == 'min' && is_null($value))
+ {
+ continue;
+ }
+
+ $attr[] = $key . '="' . $value . '"';
+ }
+ $attr = implode(' ', $attr);
+
+ return '';
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Fields/Password.php b/core/libraries/Hubzero/Form/Fields/Password.php
new file mode 100644
index 00000000000..6cb92d9f619
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Fields/Password.php
@@ -0,0 +1,60 @@
+element['size'] ? ' size="' . (int) $this->element['size'] . '"' : '';
+ $maxLength = $this->element['maxlength'] ? ' maxlength="' . (int) $this->element['maxlength'] . '"' : '';
+ $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : '';
+ $auto = ((string) $this->element['autocomplete'] == 'off') ? ' autocomplete="off"' : '';
+ $readonly = ((string) $this->element['readonly'] == 'true') ? ' readonly="readonly"' : '';
+ $disabled = ((string) $this->element['disabled'] == 'true') ? ' disabled="disabled"' : '';
+ $meter = ((string) $this->element['strengthmeter'] == 'true');
+ $threshold = $this->element['threshold'] ? (int) $this->element['threshold'] : 66;
+
+ $script = '';
+ if ($meter)
+ {
+ Asset::script('system/passwordstrength.js', true, true);
+ $script = '';
+ }
+
+ return '' . $script;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Fields/Plugins.php b/core/libraries/Hubzero/Form/Fields/Plugins.php
new file mode 100644
index 00000000000..e8bad679886
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Fields/Plugins.php
@@ -0,0 +1,76 @@
+element['folder'];
+
+ if (!empty($folder))
+ {
+ // Get list of plugins
+ $db = App::get('db');
+ $query = $db->getQuery()
+ ->select('element', 'value')
+ ->select('name', 'text')
+ ->from('#__extensions')
+ ->whereEquals('folder', $folder)
+ ->whereEquals('enabled', '1')
+ ->order('ordering', 'asc')
+ ->order('name', 'asc');
+ $db->setQuery($query->toString());
+
+ $options = $db->loadObjectList();
+
+ $lang = App::get('language');
+ foreach ($options as $i => $item)
+ {
+ $extension = 'plg_' . $folder . '_' . $item->value;
+ $lang->load($extension . '.sys', PATH_APP . '/plugins/' . $folder . '/' . $item->value, null, false, true)
+ || $lang->load($extension . '.sys', PATH_CORE . '/plugins/' . $folder . '/' . $item->value, null, false, true);
+
+ $options[$i]->text = $lang->txt($item->text);
+ }
+
+ if ($db->getErrorMsg())
+ {
+ return '';
+ }
+ }
+ else
+ {
+ App::abort(500, App::get('language')->txt('JFRAMEWORK_FORM_FIELDS_PLUGINS_ERROR_FOLDER_EMPTY'));
+ }
+
+ // Merge any additional options in the XML definition.
+ $options = array_merge(parent::getOptions(), $options);
+
+ return $options;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Fields/Radio.php b/core/libraries/Hubzero/Form/Fields/Radio.php
new file mode 100644
index 00000000000..18141afcf97
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Fields/Radio.php
@@ -0,0 +1,150 @@
+element['class'] ? ' class="radio ' . (string) $this->element['class'] . '"' : ' class="radio"';
+
+ // Start the radio field output.
+ $html[] = '';
+
+ return implode($html);
+ }
+
+ /**
+ * Method to get the field options for radio buttons.
+ *
+ * @return array The field option objects.
+ */
+ protected function getOptions()
+ {
+ // Initialize variables.
+ $options = array();
+
+ foreach ($this->element->children() as $option)
+ {
+ // Only add elements.
+ if ($option->getName() != 'option')
+ {
+ continue;
+ }
+
+ $label = (isset($option[0]) ? $option[0] : $option['label']);
+
+ // Create a new option object based on the element.
+ $tmp = Dropdown::option(
+ (string) $option['value'], trim((string) $label), 'value', 'text',
+ ((string) $option['disabled'] == 'true')
+ );
+ foreach ($option->attributes() as $index => $value)
+ {
+ $dataCheck = strtolower(substr($index, 0, 4));
+ if ($dataCheck == 'data')
+ {
+ $tmp->$index = (string) $value;
+ }
+ }
+
+ // Set some option attributes.
+ $tmp->class = (string) $option['class'];
+
+ // Set some JavaScript option attributes.
+ $tmp->onclick = (string) $option['onclick'];
+
+ // Add the option object to the result set.
+ $options[] = $tmp;
+ }
+
+ reset($options);
+
+ return $options;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Fields/Rules.php b/core/libraries/Hubzero/Form/Fields/Rules.php
new file mode 100644
index 00000000000..c8ab785db32
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Fields/Rules.php
@@ -0,0 +1,297 @@
+element['section'] ? (string) $this->element['section'] : '';
+ $component = $this->element['component'] ? (string) $this->element['component'] : '';
+ $assetField = $this->element['asset_field'] ? (string) $this->element['asset_field'] : 'asset_id';
+
+ // Get the actions for the asset.
+ $comfile = $component ? App::get('component')->path($component) . '/config/access.xml' : '';
+ $sectioned = "/access/section[@name='" . ($section ?: 'component') . "']/";
+
+ $actions = Access::getActionsFromFile($comfile, $sectioned);
+
+ // Iterate over the children and add to the actions.
+ foreach ($this->element->children() as $el)
+ {
+ if ($el->getName() == 'action')
+ {
+ $actions[] = (object) array(
+ 'name' => (string) $el['name'],
+ 'title' => (string) $el['title'],
+ 'description' => (string) $el['description']
+ );
+ }
+ }
+
+ // Get the explicit rules for this asset.
+ if ($section == 'component')
+ {
+ // Need to find the asset id by the name of the component.
+ $db = App::get('db');
+
+ $query = $db->getQuery()
+ ->select('id')
+ ->from('#__assets')
+ ->whereEquals('name', $component);
+ $db->setQuery($query->toString());
+ $assetId = (int) $db->loadResult();
+
+ if ($error = $db->getErrorMsg())
+ {
+ throw new Exception(500, $error);
+ }
+ }
+ else
+ {
+ // Find the asset id of the content.
+ // Note that for global configuration, com_config injects asset_id = 1 into the form.
+ $assetId = $this->form->getValue($assetField);
+ }
+
+ // Full width format.
+
+ // Get the rules for just this asset (non-recursive).
+ $assetRules = Access::getAssetRules($assetId);
+
+ // Get the available user groups.
+ $groups = $this->getUserGroups();
+
+ // Build the form control.
+ $curLevel = 0;
+
+ $lang = App::get('language');
+
+ // Prepare output
+ $html = array();
+ $html[] = '
';
+ $html[] = '
' . $lang->txt('JLIB_RULES_SETTINGS_DESC') . '
';
+ $html[] = '
';
+ // If AssetId is blank and section wasn't set to component, set it to the component name here for inheritance checks.
+ $assetId = empty($assetId) && $section != 'component' ? $component : $assetId;
+
+ // Start a row for each user group.
+ foreach ($groups as $group)
+ {
+ $difLevel = $group->level - $curLevel;
+
+ $html[] = '
';
+
+ // The calculated setting is not shown for the root group of global configuration.
+ $canCalculateSettings = ($group->parent_id || !empty($component));
+ if ($canCalculateSettings)
+ {
+ $html[] = '
';
+
+ $html[] = ' ';
+
+ // If this asset's rule is allowed, but the inherited rule is deny, we have a conflict.
+ if (($assetRule === true) && ($inheritedRule === false))
+ {
+ $html[] = $lang->txt('JLIB_RULES_CONFLICT');
+ }
+
+ $html[] = '
';
+
+ // Build the Calculated Settings column.
+ // The inherited settings column is not displayed for the root group in global configuration.
+ if ($canCalculateSettings)
+ {
+ $html[] = '
';
+
+ // This is where we show the current effective settings considering currrent group, path and cascade.
+ // Check whether this is a component or global. Change the text slightly.
+ if (Access::checkGroup($group->value, 'core.admin', $assetId) !== true)
+ {
+ if ($inheritedRule === null)
+ {
+ $html[] = '' . $lang->txt('JLIB_RULES_NOT_ALLOWED') . '';
+ }
+ elseif ($inheritedRule === true)
+ {
+ $html[] = '' . $lang->txt('JLIB_RULES_ALLOWED') . '';
+ }
+ elseif ($inheritedRule === false)
+ {
+ if ($assetRule === false)
+ {
+ $html[] = '' . $lang->txt('JLIB_RULES_NOT_ALLOWED') . '';
+ }
+ else
+ {
+ $html[] = '' . $lang->txt('JLIB_RULES_NOT_ALLOWED_LOCKED') . '';
+ }
+ }
+ }
+ elseif (!empty($component))
+ {
+ $html[] = '' . $lang->txt('JLIB_RULES_ALLOWED_ADMIN') . '';
+ }
+ else
+ {
+ // Special handling for groups that have global admin because they can't be denied.
+ // The admin rights can be changed.
+ if ($action->name === 'core.admin')
+ {
+ $html[] = '' . $lang->txt('JLIB_RULES_ALLOWED') . '';
+ }
+ elseif ($inheritedRule === false)
+ {
+ // Other actions cannot be changed.
+ $html[] = '' . $lang->txt('JLIB_RULES_NOT_ALLOWED_ADMIN_CONFLICT') . '';
+ }
+ else
+ {
+ $html[] = '' . $lang->txt('JLIB_RULES_ALLOWED_ADMIN') . '';
+ }
+ }
+
+ $html[] = '
';
+ }
+
+ // Create the real field, hidden, that stored the user id.
+ $html[] = '';
+
+ return implode("\n", $html);
+ }
+
+ /**
+ * Method to get the filtering groups (null means no filtering)
+ *
+ * @return mixed array of filtering groups or null.
+ */
+ protected function getGroups()
+ {
+ return null;
+ }
+
+ /**
+ * Method to get the users to exclude from the list of users
+ *
+ * @return mixed Array of users to exclude or null to to not exclude them
+ */
+ protected function getExcluded()
+ {
+ return null;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Fields/Usergroup.php b/core/libraries/Hubzero/Form/Fields/Usergroup.php
new file mode 100644
index 00000000000..fd2af518bed
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Fields/Usergroup.php
@@ -0,0 +1,74 @@
+element['class'] ? ' class="' . (string) $this->element['class'] . '"' : '';
+ $attr .= ((string) $this->element['disabled'] == 'true') ? ' disabled="disabled"' : '';
+ $attr .= $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : '';
+ $attr .= $this->multiple ? ' multiple="multiple"' : '';
+
+ // Initialize JavaScript field attributes.
+ $attr .= $this->element['onchange'] ? ' onchange="' . (string) $this->element['onchange'] . '"' : '';
+
+ // Iterate through the children and build an array of options.
+ foreach ($this->element->children() as $option)
+ {
+
+ // Only add elements.
+ if ($option->getName() != 'option')
+ {
+ continue;
+ }
+
+ // Create a new option object based on the element.
+ $tmp = Dropdown::option((string) $option['value'], trim((string) $option), 'value', 'text',
+ ((string) $option['disabled'] == 'true')
+ );
+
+ // Set some option attributes.
+ $tmp->class = (string) $option['class'];
+
+ // Set some JavaScript option attributes.
+ $tmp->onclick = (string) $option['onclick'];
+
+ // Add the option object to the result set.
+ $options[] = $tmp;
+ }
+
+ return Access::usergroup($this->name, $this->value, $attr, $options, $this->id);
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Form.php b/core/libraries/Hubzero/Form/Form.php
new file mode 100644
index 00000000000..dad5c474780
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Form.php
@@ -0,0 +1,2034 @@
+name = $name;
+
+ // Initialise the Registry data.
+ $this->data = new Registry;
+
+ // Set the options if specified.
+ $this->options['control'] = isset($options['control']) ? $options['control'] : false;
+ }
+
+ /**
+ * Method to bind data to the form.
+ *
+ * @param mixed $data An array or object of data to bind to the form.
+ * @return boolean True on success.
+ */
+ public function bind($data)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+
+ // The data must be an object or array.
+ if (!is_object($data) && !is_array($data))
+ {
+ return false;
+ }
+
+ // Convert the input to an array.
+ if (is_object($data))
+ {
+ if ($data instanceof Registry)
+ {
+ // Handle a Registry.
+ $data = $data->toArray();
+ }
+ elseif ($data instanceof Obj)
+ {
+ // Handle a Object.
+ $data = $data->getProperties();
+ }
+ else
+ {
+ // Handle other types of objects.
+ $data = (array) $data;
+ }
+ }
+
+ // Process the input data.
+ foreach ($data as $k => $v)
+ {
+ if ($this->findField($k))
+ {
+ // If the field exists set the value.
+ $this->data->set($k, $v);
+ }
+ elseif (is_object($v) || Arr::isAssociative($v))
+ {
+ // If the value is an object or an associative array hand it off to the recursive bind level method.
+ $this->bindLevel($k, $v);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to bind data to the form for the group level.
+ *
+ * @param string $group The dot-separated form group path on which to bind the data.
+ * @param mixed $data An array or object of data to bind to the form for the group level.
+ * @return void
+ */
+ protected function bindLevel($group, $data)
+ {
+ // Ensure the input data is an array.
+ settype($data, 'array');
+
+ // Process the input data.
+ foreach ($data as $k => $v)
+ {
+ if ($this->findField($k, $group))
+ {
+ // If the field exists set the value.
+ $this->data->set($group . '.' . $k, $v);
+ }
+ elseif (is_object($v) || Arr::isAssociative($v))
+ {
+ // If the value is an object or an associative array, hand it off to the recursive bind level method
+ $this->bindLevel($group . '.' . $k, $v);
+ }
+ }
+ }
+
+ /**
+ * Method to filter the form data.
+ *
+ * @param array $data An array of field values to filter.
+ * @param string $group The dot-separated form group path on which to filter the fields.
+ * @return mixed Array or false.
+ */
+ public function filter($data, $group = null)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+
+ // Initialise variables.
+ $input = new Registry($data);
+ $output = new Registry;
+
+ // Get the fields for which to filter the data.
+ $fields = $this->findFieldsByGroup($group);
+ if (!$fields)
+ {
+ // PANIC!
+ return false;
+ }
+
+ // Filter the fields.
+ foreach ($fields as $field)
+ {
+ // Initialise variables.
+ $name = (string) $field['name'];
+
+ // Get the field groups for the element.
+ $attrs = $field->xpath('ancestor::fields[@name]/@name');
+ $groups = array_map('strval', $attrs ? $attrs : array());
+ $group = implode('.', $groups);
+
+ // Get the field value from the data input.
+ if ($group)
+ {
+ // Filter the value if it exists.
+ if ($input->has($group . '.' . $name))
+ {
+ $output->set($group . '.' . $name, $this->filterField($field, $input->get($group . '.' . $name, (string) $field['default'])));
+ }
+ }
+ else
+ {
+ // Filter the value if it exists.
+ if ($input->has($name))
+ {
+ $output->set($name, $this->filterField($field, $input->get($name, (string) $field['default'])));
+ }
+ }
+ }
+
+ return $output->toArray();
+ }
+
+ /**
+ * Return all errors, if any.
+ *
+ * @return array Array of error messages or Exception objects.
+ */
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+
+ /**
+ * Method to get a form field represented as a Field object.
+ *
+ * @param string $name The name of the form field.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @param mixed $value The optional value to use as the default for the field.
+ * @return mixed The Field object for the field or boolean false on error.
+ */
+ public function getField($name, $group = null, $value = null)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+
+ // Attempt to find the field by name and group.
+ $element = $this->findField($name, $group);
+
+ // If the field element was not found return false.
+ if (!$element)
+ {
+ return false;
+ }
+
+ return $this->loadField($element, $group, $value);
+ }
+
+ /**
+ * Method to get an attribute value from a field XML element. If the attribute doesn't exist or
+ * is null then the optional default value will be used.
+ *
+ * @param string $name The name of the form field for which to get the attribute value.
+ * @param string $attribute The name of the attribute for which to get a value.
+ * @param mixed $default The optional default value to use if no attribute value exists.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @return mixed The attribute value for the field.
+ */
+ public function getFieldAttribute($name, $attribute, $default = null, $group = null)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ // TODO: throw exception.
+ return $default;
+ }
+
+ // Find the form field element from the definition.
+ $element = $this->findField($name, $group);
+
+ // If the element exists and the attribute exists for the field return the attribute value.
+ if (($element instanceof SimpleXMLElement) && ((string) $element[$attribute]))
+ {
+ return (string) $element[$attribute];
+ }
+
+ // Otherwise return the given default value.
+ return $default;
+ }
+
+ /**
+ * Method to get an array of Field objects in a given fieldset by name. If no name is
+ * given then all fields are returned.
+ *
+ * @param string $set The optional name of the fieldset.
+ * @return array The array of Field objects in the fieldset.
+ */
+ public function getFieldset($set = null)
+ {
+ // Initialise variables.
+ $fields = array();
+
+ // Get all of the field elements in the fieldset.
+ if ($set)
+ {
+ $elements = $this->findFieldsByFieldset($set);
+ }
+ // Get all fields.
+ else
+ {
+ $elements = $this->findFieldsByGroup();
+ }
+
+ // If no field elements were found return empty.
+ if (empty($elements))
+ {
+ return $fields;
+ }
+
+ // Build the result array from the found field elements.
+ foreach ($elements as $element)
+ {
+ // Get the field groups for the element.
+ $attrs = $element->xpath('ancestor::fields[@name]/@name');
+ $groups = array_map('strval', $attrs ? $attrs : array());
+ $group = implode('.', $groups);
+
+ // If the field is successfully loaded add it to the result array.
+ if ($field = $this->loadField($element, $group))
+ {
+ $fields[$field->id] = $field;
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Method to get an array of fieldset objects optionally filtered over a given field group.
+ *
+ * @param string $group The dot-separated form group path on which to filter the fieldsets.
+ * @return array The array of fieldset objects.
+ */
+ public function getFieldsets($group = null)
+ {
+ // Initialise variables.
+ $fieldsets = array();
+ $sets = array();
+
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return $fieldsets;
+ }
+
+ if ($group)
+ {
+ // Get the fields elements for a given group.
+ $elements = &$this->findGroup($group);
+
+ foreach ($elements as &$element)
+ {
+ // Get an array of elements and fieldset attributes within the fields element.
+ if ($tmp = $element->xpath('descendant::fieldset[@name] | descendant::field[@fieldset]/@fieldset'))
+ {
+ $sets = array_merge($sets, (array) $tmp);
+ }
+ }
+ }
+ else
+ {
+ // Get an array of elements and fieldset attributes.
+ $sets = $this->xml->xpath('//fieldset[@name] | //field[@fieldset]/@fieldset');
+ }
+
+ // If no fieldsets are found return empty.
+ if (empty($sets))
+ {
+
+ return $fieldsets;
+ }
+
+ // Process each found fieldset.
+ foreach ($sets as $set)
+ {
+ // Are we dealing with a fieldset element?
+ if ((string) $set['name'])
+ {
+ // Only create it if it doesn't already exist.
+ if (empty($fieldsets[(string) $set['name']]))
+ {
+
+ // Build the fieldset object.
+ $fieldset = (object) array('name' => '', 'label' => '', 'description' => '');
+ foreach ($set->attributes() as $name => $value)
+ {
+ $fieldset->$name = (string) $value;
+ }
+
+ // Add the fieldset object to the list.
+ $fieldsets[$fieldset->name] = $fieldset;
+ }
+ }
+ // Must be dealing with a fieldset attribute.
+ else
+ {
+ // Only create it if it doesn't already exist.
+ if (empty($fieldsets[(string) $set]))
+ {
+
+ // Attempt to get the fieldset element for data (throughout the entire form document).
+ $tmp = $this->xml->xpath('//fieldset[@name="' . (string) $set . '"]');
+
+ // If no element was found, build a very simple fieldset object.
+ if (empty($tmp))
+ {
+ $fieldset = (object) array('name' => (string) $set, 'label' => '', 'description' => '');
+ }
+ // Build the fieldset object from the element.
+ else
+ {
+ $fieldset = (object) array('name' => '', 'label' => '', 'description' => '');
+ foreach ($tmp[0]->attributes() as $name => $value)
+ {
+ $fieldset->$name = (string) $value;
+ }
+ }
+
+ // Add the fieldset object to the list.
+ $fieldsets[$fieldset->name] = $fieldset;
+ }
+ }
+ }
+
+ return $fieldsets;
+ }
+
+ /**
+ * Method to get the form control. This string serves as a container for all form fields. For
+ * example, if there is a field named 'foo' and a field named 'bar' and the form control is
+ * empty the fields will be rendered like: and . If
+ * the form control is set to 'fields' however, the fields would be rendered like:
+ * and .
+ *
+ * @return string The form control string.
+ */
+ public function getFormControl()
+ {
+ return (string) $this->options['control'];
+ }
+
+ /**
+ * Method to get an array of Field objects in a given field group by name.
+ *
+ * @param string $group The dot-separated form group path for which to get the form fields.
+ * @param boolean $nested True to also include fields in nested groups that are inside of the group for which to find fields.
+ * @return array The array of Field objects in the field group.
+ */
+ public function getGroup($group, $nested = false)
+ {
+ // Initialise variables.
+ $fields = array();
+
+ // Get all of the field elements in the field group.
+ $elements = $this->findFieldsByGroup($group, $nested);
+
+ // If no field elements were found return empty.
+ if (empty($elements))
+ {
+ return $fields;
+ }
+
+ // Build the result array from the found field elements.
+ foreach ($elements as $element)
+ {
+ // Get the field groups for the element.
+ $attrs = $element->xpath('ancestor::fields[@name]/@name');
+ $groups = array_map('strval', $attrs ? $attrs : array());
+ $group = implode('.', $groups);
+
+ // If the field is successfully loaded add it to the result array.
+ if ($field = $this->loadField($element, $group))
+ {
+ $fields[$field->id] = $field;
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Method to get a form field markup for the field input.
+ *
+ * @param string $name The name of the form field.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @param mixed $value The optional value to use as the default for the field.
+ * @return string The form field markup.
+ */
+ public function getInput($name, $group = null, $value = null)
+ {
+ // Attempt to get the form field.
+ if ($field = $this->getField($name, $group, $value))
+ {
+ return $field->input;
+ }
+
+ return '';
+ }
+
+ /**
+ * Method to get the label for a field input.
+ *
+ * @param string $name The name of the form field.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @return string The form field label.
+ */
+ public function getLabel($name, $group = null)
+ {
+ // Attempt to get the form field.
+ if ($field = $this->getField($name, $group))
+ {
+ return $field->label;
+ }
+
+ return '';
+ }
+
+ /**
+ * Method to get the form name.
+ *
+ * @return string The name of the form.
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Method to get the value of a field.
+ *
+ * @param string $name The name of the field for which to get the value.
+ * @param string $group The optional dot-separated form group path on which to get the value.
+ * @param mixed $default The optional default value of the field value is empty.
+ * @return mixed The value of the field or the default value if empty.
+ */
+ public function getValue($name, $group = null, $default = null)
+ {
+ // If a group is set use it.
+ if ($group)
+ {
+ $return = $this->data->get($group . '.' . $name, $default);
+ }
+ else
+ {
+ $return = $this->data->get($name, $default);
+ }
+
+ return $return;
+ }
+
+ /**
+ * Method to load the form description from an XML string or object.
+ *
+ * The replace option works per field. If a field being loaded already exists in the current
+ * form definition then the behavior or load will vary depending upon the replace flag. If it
+ * is set to true, then the existing field will be replaced in its exact location by the new
+ * field being loaded. If it is false, then the new field being loaded will be ignored and the
+ * method will move on to the next field to load.
+ *
+ * @param string $data The name of an XML string or object.
+ * @param string $replace Flag to toggle whether form fields should be replaced if a field already exists with the same group/name.
+ * @param string $xpath An optional xpath to search for the fields.
+ * @return boolean True on success, false otherwise.
+ */
+ public function load($data, $replace = true, $xpath = false)
+ {
+ // If the data to load isn't already an XML element or string return false.
+ if ((!($data instanceof SimpleXMLElement)) && (!is_string($data)))
+ {
+ return false;
+ }
+
+ // Attempt to load the XML if a string.
+ if (is_string($data))
+ {
+ $data = self::getXML($data, false);
+
+ // Make sure the XML loaded correctly.
+ if (!$data)
+ {
+ return false;
+ }
+ }
+
+ // If we have no XML definition at this point let's make sure we get one.
+ if (empty($this->xml))
+ {
+ // If no XPath query is set to search for fields, and we have a , set it and return.
+ if (!$xpath && ($data->getName() == 'form'))
+ {
+ $this->xml = $data;
+
+ // Synchronize any paths found in the load.
+ $this->syncPaths();
+
+ return true;
+ }
+ // Create a root element for the form.
+ else
+ {
+ $this->xml = new SimpleXMLElement('');
+ }
+ }
+
+ // Get the XML elements to load.
+ $elements = array();
+ if ($xpath)
+ {
+ $elements = $data->xpath($xpath);
+ }
+ elseif ($data->getName() == 'form')
+ {
+ $elements = $data->children();
+ }
+
+ // If there is nothing to load return true.
+ if (empty($elements))
+ {
+ return true;
+ }
+
+ // Load the found form elements.
+ foreach ($elements as $element)
+ {
+ // Get an array of fields with the correct name.
+ $fields = $element->xpath('descendant-or-self::field');
+ foreach ($fields as $field)
+ {
+ // Get the group names as strings for ancestor fields elements.
+ $attrs = $field->xpath('ancestor::fields[@name]/@name');
+ $groups = array_map('strval', $attrs ? $attrs : array());
+
+ // Check to see if the field exists in the current form.
+ if ($current = $this->findField((string) $field['name'], implode('.', $groups)))
+ {
+
+ // If set to replace found fields, replace the data and remove the field so we don't add it twice.
+ if ($replace)
+ {
+ $olddom = dom_import_simplexml($current);
+ $loadeddom = dom_import_simplexml($field);
+ $addeddom = $olddom->ownerDocument->importNode($loadeddom);
+ $olddom->parentNode->replaceChild($addeddom, $olddom);
+ $loadeddom->parentNode->removeChild($loadeddom);
+ }
+ else
+ {
+ unset($field);
+ }
+ }
+ }
+
+ // Merge the new field data into the existing XML document.
+ self::addNode($this->xml, $element);
+ }
+
+ // Synchronize any paths found in the load.
+ $this->syncPaths();
+
+ return true;
+ }
+
+ /**
+ * Method to load the form description from an XML file.
+ *
+ * The reset option works on a group basis. If the XML file references
+ * groups that have already been created they will be replaced with the
+ * fields in the new XML file unless the $reset parameter has been set
+ * to false.
+ *
+ * @param string $file The filesystem path of an XML file.
+ * @param string $reset Flag to toggle whether form fields should be replaced if a field
+ * already exists with the same group/name.
+ * @param string $xpath An optional xpath to search for the fields.
+ * @return boolean True on success, false otherwise.
+ */
+ public function loadFile($file, $reset = true, $xpath = false)
+ {
+ // Check to see if the path is an absolute path.
+ if (!is_file($file))
+ {
+
+ // Not an absolute path so let's attempt to find one using Filesystem.
+ $file = Filesystem::find(self::addFormPath(), strtolower($file) . '.xml');
+
+ // If unable to find the file return false.
+ if (!$file)
+ {
+ return false;
+ }
+ }
+ // Attempt to load the XML file.
+ $xml = self::getXML($file, true);
+
+ return $this->load($xml, $reset, $xpath);
+ }
+
+ /**
+ * Method to remove a field from the form definition.
+ *
+ * @param string $name The name of the form field for which remove.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @return boolean True on success.
+ */
+ public function removeField($name, $group = null)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ // TODO: throw exception.
+ return false;
+ }
+
+ // Find the form field element from the definition.
+ $element = $this->findField($name, $group);
+
+ // If the element exists remove it from the form definition.
+ if ($element instanceof SimpleXMLElement)
+ {
+ $dom = dom_import_simplexml($element);
+ $dom->parentNode->removeChild($dom);
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to remove a group from the form definition.
+ *
+ * @param string $group The dot-separated form group path for the group to remove.
+ * @return boolean True on success.
+ */
+ public function removeGroup($group)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ // TODO: throw exception.
+ return false;
+ }
+
+ // Get the fields elements for a given group.
+ $elements = &$this->findGroup($group);
+ foreach ($elements as &$element)
+ {
+ $dom = dom_import_simplexml($element);
+ $dom->parentNode->removeChild($dom);
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to reset the form data store and optionally the form XML definition.
+ *
+ * @param boolean $xml True to also reset the XML form definition.
+ * @return boolean True on success.
+ */
+ public function reset($xml = false)
+ {
+ unset($this->data);
+ $this->data = new Registry;
+
+ if ($xml)
+ {
+ unset($this->xml);
+ $this->xml = new SimpleXMLElement('');
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to set a field XML element to the form definition. If the replace flag is set then
+ * the field will be set whether it already exists or not. If it isn't set, then the field
+ * will not be replaced if it already exists.
+ *
+ * @param object &$element The XML element object representation of the form field.
+ * @param string $group The optional dot-separated form group path on which to set the field.
+ * @param boolean $replace True to replace an existing field if one already exists.
+ * @return boolean True on success.
+ */
+ public function setField(&$element, $group = null, $replace = true)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ // TODO: throw exception.
+
+ return false;
+ }
+
+ // Make sure the element to set is valid.
+ if (!($element instanceof SimpleXMLElement))
+ {
+ // TODO: throw exception.
+ return false;
+ }
+
+ // Find the form field element from the definition.
+ $old = &$this->findField((string) $element['name'], $group);
+
+ // If an existing field is found and replace flag is false do nothing and return true.
+ if (!$replace && !empty($old))
+ {
+ return true;
+ }
+
+ // If an existing field is found and replace flag is true remove the old field.
+ if ($replace && !empty($old) && ($old instanceof SimpleXMLElement))
+ {
+ $dom = dom_import_simplexml($old);
+ $dom->parentNode->removeChild($dom);
+ }
+
+ // If no existing field is found find a group element and add the field as a child of it.
+ if ($group)
+ {
+ // Get the fields elements for a given group.
+ $fields = &$this->findGroup($group);
+
+ // If an appropriate fields element was found for the group, add the element.
+ if (isset($fields[0]) && ($fields[0] instanceof SimpleXMLElement))
+ {
+ self::addNode($fields[0], $element);
+ }
+ }
+ else
+ {
+ // Set the new field to the form.
+ self::addNode($this->xml, $element);
+ }
+
+ // Synchronize any paths found in the load.
+ $this->syncPaths();
+
+ return true;
+ }
+
+ /**
+ * Method to set an attribute value for a field XML element.
+ *
+ * @param string $name The name of the form field for which to set the attribute value.
+ * @param string $attribute The name of the attribute for which to set a value.
+ * @param mixed $value The value to set for the attribute.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @return boolean True on success.
+ */
+ public function setFieldAttribute($name, $attribute, $value, $group = null)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ // TODO: throw exception.
+ return false;
+ }
+
+ // Find the form field element from the definition.
+ $element = $this->findField($name, $group);
+
+ // If the element doesn't exist return false.
+ if (!($element instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+ // Otherwise set the attribute and return true.
+ else
+ {
+ $element[$attribute] = $value;
+
+ // Synchronize any paths found in the load.
+ $this->syncPaths();
+
+ return true;
+ }
+ }
+
+ /**
+ * Method to set some field XML elements to the form definition. If the replace flag is set then
+ * the fields will be set whether they already exists or not. If it isn't set, then the fields
+ * will not be replaced if they already exist.
+ *
+ * @param object &$elements The array of XML element object representations of the form fields.
+ * @param string $group The optional dot-separated form group path on which to set the fields.
+ * @param boolean $replace True to replace existing fields if they already exist.
+ * @return boolean True on success.
+ */
+ public function setFields(&$elements, $group = null, $replace = true)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ // TODO: throw exception.
+ return false;
+ }
+
+ // Make sure the elements to set are valid.
+ foreach ($elements as $element)
+ {
+ if (!($element instanceof SimpleXMLElement))
+ {
+ // TODO: throw exception.
+ return false;
+ }
+ }
+
+ // Set the fields.
+ $return = true;
+ foreach ($elements as $element)
+ {
+ if (!$this->setField($element, $group, $replace))
+ {
+ $return = false;
+ }
+ }
+
+ // Synchronize any paths found in the load.
+ $this->syncPaths();
+
+ return $return;
+ }
+
+ /**
+ * Method to set the value of a field. If the field does not exist in the form then the method
+ * will return false.
+ *
+ * @param string $name The name of the field for which to set the value.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @param mixed $value The value to set for the field.
+ * @return boolean True on success.
+ */
+ public function setValue($name, $group = null, $value = null)
+ {
+ // If the field does not exist return false.
+ if (!$this->findField($name, $group))
+ {
+ return false;
+ }
+
+ // If a group is set use it.
+ if ($group)
+ {
+ $this->data->set($group . '.' . $name, $value);
+ }
+ else
+ {
+ $this->data->set($name, $value);
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to validate form data.
+ *
+ * Validation warnings will be pushed into Form::errors and should be
+ * retrieved with Form::getErrors() when validate returns boolean false.
+ *
+ * @param array $data An array of field values to validate.
+ * @param string $group The optional dot-separated form group path on which to filter the fields to be validated.
+ * @return mixed True on sucess.
+ */
+ public function validate($data, $group = null)
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+
+ // Initialise variables.
+ $return = true;
+
+ // Create an input registry object from the data to validate.
+ $input = new Registry($data);
+
+ // Get the fields for which to validate the data.
+ $fields = $this->findFieldsByGroup($group);
+ if (!$fields)
+ {
+ // PANIC!
+ return false;
+ }
+
+ // Validate the fields.
+ foreach ($fields as $field)
+ {
+ // Initialise variables.
+ $value = null;
+ $name = (string) $field['name'];
+
+ // Get the group names as strings for ancestor fields elements.
+ $attrs = $field->xpath('ancestor::fields[@name]/@name');
+ $groups = array_map('strval', $attrs ? $attrs : array());
+ $group = implode('.', $groups);
+
+ // Get the value from the input data.
+ if ($group)
+ {
+ $value = $input->get($group . '.' . $name);
+ }
+ else
+ {
+ $value = $input->get($name);
+ }
+
+ // Validate the field.
+ $valid = $this->validateField($field, $group, $value, $input);
+
+ // Check for an error.
+ if ($valid instanceof Exception)
+ {
+ if ($valid instanceof MissingData || $valid instanceof InvalidData)
+ {
+ $this->errors[$name] = $valid;
+ $return = false;
+ }
+ else
+ {
+ throw new Exception($valid->getMessage());
+ return false;
+ }
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Method to apply an input filter to a value based on field data.
+ *
+ * @param string $element The XML element object representation of the form field.
+ * @param mixed $value The value to filter for the field.
+ * @return mixed The filtered value.
+ */
+ protected function filterField($element, $value)
+ {
+ // Make sure there is a valid SimpleXMLElement.
+ if (!($element instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+
+ // Get the field filter type.
+ $filter = (string) $element['filter'];
+
+ // Process the input value based on the filter.
+ $return = null;
+
+ switch (strtoupper($filter))
+ {
+ // Access Control Rules.
+ case 'RULES':
+ $return = array();
+ foreach ((array) $value as $action => $ids)
+ {
+ // Build the rules array.
+ $return[$action] = array();
+ foreach ($ids as $id => $p)
+ {
+ if ($p !== '')
+ {
+ $return[$action][$id] = ($p == '1' || $p == 'true') ? true : false;
+ }
+ }
+ }
+ break;
+
+ // Do nothing, thus leaving the return value as null.
+ case 'UNSET':
+ break;
+
+ // No Filter.
+ case 'RAW':
+ $return = $value;
+ break;
+
+ // Filter the input as an array of integers.
+ case 'INT_ARRAY':
+ // Make sure the input is an array.
+ if (is_object($value))
+ {
+ $value = get_object_vars($value);
+ }
+ $value = is_array($value) ? $value : array($value);
+
+ Arr::toInteger($value);
+ $return = $value;
+ break;
+
+ // Filter safe HTML.
+ case 'SAFEHTML':
+ $return = Sanitize::clean((string)$value);
+ break;
+
+ // Convert a date to UTC based on the server timezone offset.
+ case 'SERVER_UTC':
+ if (intval($value) > 0)
+ {
+ // Get the server timezone setting.
+ $offset = App::get('config')->get('offset');
+
+ // Return an SQL formatted datetime string in UTC.
+ $return = with(new Date($value, $offset))->toSql();
+ }
+ else
+ {
+ $return = '';
+ }
+ break;
+
+ // Convert a date to UTC based on the user timezone offset.
+ case 'USER_UTC':
+ if (intval($value) > 0)
+ {
+ // Get the user timezone setting defaulting to the server timezone setting.
+ $offset = App::get('user')->getParam('timezone', App::get('config')->get('offset'));
+
+ // Return a MySQL formatted datetime string in UTC.
+ $return = with(new Date($value, $offset))->toSql();
+ }
+ else
+ {
+ $return = '';
+ }
+ break;
+
+ // Ensures a protocol is present in the saved field. Only use when
+ // the only permitted protocols requre '://'. See FormRuleUrl for list of these.
+
+ case 'URL':
+ if (empty($value))
+ {
+ return false;
+ }
+ $value = Sanitize::clean($value);
+ $value = trim($value);
+
+ // <>" are never valid in a uri see http://www.ietf.org/rfc/rfc1738.txt.
+ $value = str_replace(array('<', '>', '"'), '', $value);
+
+ // Check for a protocol
+ $protocol = parse_url($value, PHP_URL_SCHEME);
+
+ // If there is no protocol and the relative option is not specified,
+ // we assume that it is an external URL and prepend http://.
+ if (($element['type'] == 'url' && !$protocol && !$element['relative'])
+ || (!$element['type'] == 'url' && !$protocol))
+ {
+ $protocol = 'http';
+ // If it looks like an internal link, then add the root.
+ if (substr($value, 0, 9) == 'index.php')
+ {
+ $value = App::get('request')->root() . $value;
+ }
+
+ // Otherwise we treat it as an external link.
+ else
+ {
+ // Put the url back together.
+ $value = $protocol . '://' . $value;
+ }
+ }
+
+ // If relative URLS are allowed we assume that URLs without protocols are internal.
+ elseif (!$protocol && $element['relative'])
+ {
+ $host = App::get('request')->host();
+
+ // If it starts with the host string, just prepend the protocol.
+ if (substr($value, 0) == $host)
+ {
+ $value = 'http://' . $value;
+ }
+ // Otherwise prepend the root.
+ else
+ {
+ $value = App::get('request')->root() . $value;
+ }
+ }
+
+ $return = $value;
+ break;
+
+ case 'TEL':
+ $value = trim($value);
+ // Does it match the NANP pattern?
+ if (preg_match('/^(?:\+?1[-. ]?)?\(?([2-9][0-8][0-9])\)?[-. ]?([2-9][0-9]{2})[-. ]?([0-9]{4})$/', $value) == 1)
+ {
+ $number = (string) preg_replace('/[^\d]/', '', $value);
+ if (substr($number, 0, 1) == 1)
+ {
+ $number = substr($number, 1);
+ }
+ if (substr($number, 0, 2) == '+1')
+ {
+ $number = substr($number, 2);
+ }
+ $result = '1.' . $number;
+ }
+ // If not, does it match ITU-T?
+ elseif (preg_match('/^\+(?:[0-9] ?){6,14}[0-9]$/', $value) == 1)
+ {
+ $countrycode = substr($value, 0, strpos($value, ' '));
+ $countrycode = (string) preg_replace('/[^\d]/', '', $countrycode);
+ $number = strstr($value, ' ');
+ $number = (string) preg_replace('/[^\d]/', '', $number);
+ $result = $countrycode . '.' . $number;
+ }
+ // If not, does it match EPP?
+ elseif (preg_match('/^\+[0-9]{1,3}\.[0-9]{4,14}(?:x.+)?$/', $value) == 1)
+ {
+ if (strstr($value, 'x'))
+ {
+ $xpos = strpos($value, 'x');
+ $value = substr($value, 0, $xpos);
+ }
+ $result = str_replace('+', '', $value);
+
+ }
+ // Maybe it is already ccc.nnnnnnn?
+ elseif (preg_match('/[0-9]{1,3}\.[0-9]{4,14}$/', $value) == 1)
+ {
+ $result = $value;
+ }
+ // If not, can we make it a string of digits?
+ else
+ {
+ $value = (string) preg_replace('/[^\d]/', '', $value);
+ if ($value != null && strlen($value) <= 15)
+ {
+ $length = strlen($value);
+ // if it is fewer than 13 digits assume it is a local number
+ if ($length <= 12)
+ {
+ $result = '.' . $value;
+
+ }
+ else
+ {
+ // If it has 13 or more digits let's make a country code.
+ $cclen = $length - 12;
+ $result = substr($value, 0, $cclen) . '.' . substr($value, $cclen);
+ }
+ }
+ // If not let's not save anything.
+ else
+ {
+ $result = '';
+ }
+ }
+ $return = $result;
+
+ break;
+ default:
+ // Check for a callback filter.
+ if (strpos($filter, '::') !== false && is_callable(explode('::', $filter)))
+ {
+ $return = call_user_func(explode('::', $filter), $value);
+ }
+ // Filter using a callback function if specified.
+ elseif (function_exists($filter))
+ {
+ $return = call_user_func($filter, $value);
+ }
+ // Filter. All HTML code is filtered by default.
+ else
+ {
+ $return = Sanitize::filter($value, $filter);
+ }
+ break;
+ }
+
+ return $return;
+ }
+
+ /**
+ * Method to get a form field represented as an XML element object.
+ *
+ * @param string $name The name of the form field.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @return mixed The XML element object for the field or boolean false on error.
+ */
+ protected function findField($name, $group = null)
+ {
+ // Initialise variables.
+ $element = false;
+ $fields = array();
+
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+
+ // Let's get the appropriate field element based on the method arguments.
+ if ($group)
+ {
+ // Get the fields elements for a given group.
+ $elements = &$this->findGroup($group);
+
+ // Get all of the field elements with the correct name for the fields elements.
+ foreach ($elements as $element)
+ {
+ // If there are matching field elements add them to the fields array.
+ if ($tmp = $element->xpath('descendant::field[@name="' . $name . '"]'))
+ {
+ $fields = array_merge($fields, $tmp);
+ }
+ }
+
+ // Make sure something was found.
+ if (!$fields)
+ {
+ return false;
+ }
+
+ // Use the first correct match in the given group.
+ $groupNames = explode('.', $group);
+ foreach ($fields as &$field)
+ {
+ // Get the group names as strings for ancestor fields elements.
+ $attrs = $field->xpath('ancestor::fields[@name]/@name');
+ $names = array_map('strval', $attrs ? $attrs : array());
+
+ // If the field is in the exact group use it and break out of the loop.
+ if ($names == (array) $groupNames)
+ {
+ $element = &$field;
+ break;
+ }
+ }
+ }
+ else
+ {
+ // Get an array of fields with the correct name.
+ $fields = $this->xml->xpath('//field[@name="' . $name . '"]');
+
+ // Make sure something was found.
+ if (!$fields)
+ {
+ return false;
+ }
+
+ // Search through the fields for the right one.
+ foreach ($fields as &$field)
+ {
+ // If we find an ancestor fields element with a group name then it isn't what we want.
+ if ($field->xpath('ancestor::fields[@name]'))
+ {
+ continue;
+ }
+ // Found it!
+ else
+ {
+ $element = &$field;
+ break;
+ }
+ }
+ }
+
+ return $element;
+ }
+
+ /**
+ * Method to get an array of elements from the form XML document which are
+ * in a specified fieldset by name.
+ *
+ * @param string $name The name of the fieldset.
+ * @return mixed Boolean false on error or array of SimpleXMLElement objects.
+ */
+ protected function &findFieldsByFieldset($name)
+ {
+ // Initialise variables.
+ $false = false;
+
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return $false;
+ }
+
+ // Get an array of elements that are underneath a element
+ // with the appropriate name attribute, and also any elements with
+ // the appropriate fieldset attribute.
+ $fields = $this->xml->xpath('//fieldset[@name="' . $name . '"]//field | //field[@fieldset="' . $name . '"]');
+
+ return $fields;
+ }
+
+ /**
+ * Method to get an array of elements from the form XML document which are
+ * in a control group by name.
+ *
+ * @param mixed $group The optional dot-separated form group path on which to find the fields. Null will return all fields. False will return fields not in a group.
+ * @param boolean $nested True to also include fields in nested groups that are inside of the group for which to find fields.
+ * @return mixed Boolean false on error or array of SimpleXMLElement objects.
+ */
+ protected function &findFieldsByGroup($group = null, $nested = false)
+ {
+ // Initialise variables.
+ $false = false;
+ $fields = array();
+
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return $false;
+ }
+
+ // Get only fields in a specific group?
+ if ($group)
+ {
+ // Get the fields elements for a given group.
+ $elements = &$this->findGroup($group);
+
+ // Get all of the field elements for the fields elements.
+ foreach ($elements as $element)
+ {
+ // If there are field elements add them to the return result.
+ if ($tmp = $element->xpath('descendant::field'))
+ {
+ // If we also want fields in nested groups then just merge the arrays.
+ if ($nested)
+ {
+ $fields = array_merge($fields, $tmp);
+ }
+ // If we want to exclude nested groups then we need to check each field.
+ else
+ {
+ $groupNames = explode('.', $group);
+ foreach ($tmp as $field)
+ {
+ // Get the names of the groups that the field is in.
+ $attrs = $field->xpath('ancestor::fields[@name]/@name');
+ $names = array_map('strval', $attrs ? $attrs : array());
+
+ // If the field is in the specific group then add it to the return list.
+ if ($names == (array) $groupNames)
+ {
+ $fields = array_merge($fields, array($field));
+ }
+ }
+ }
+ }
+ }
+ }
+ elseif ($group === false)
+ {
+ // Get only field elements not in a group.
+ $fields = $this->xml->xpath('descendant::fields[not(@name)]/field | descendant::fields[not(@name)]/fieldset/field ');
+ }
+ else
+ {
+ // Get an array of all the elements.
+ $fields = $this->xml->xpath('//field');
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Method to get a form field group represented as an XML element object.
+ *
+ * @param string $group The dot-separated form group path on which to find the group.
+ * @return mixed An array of XML element objects for the group or boolean false on error.
+ */
+ protected function &findGroup($group)
+ {
+ // Initialise variables.
+ $false = false;
+ $groups = array();
+ $tmp = array();
+
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return $false;
+ }
+
+ // Make sure there is actually a group to find.
+ $group = explode('.', $group);
+ if (!empty($group))
+ {
+
+ // Get any fields elements with the correct group name.
+ $elements = $this->xml->xpath('//fields[@name="' . (string) $group[0] . '"]');
+
+ if ($elements)
+ {
+ // Check to make sure that there are no parent groups for each element.
+ foreach ($elements as $element)
+ {
+ if (!$element->xpath('ancestor::fields[@name]'))
+ {
+ $tmp[] = $element;
+ }
+ }
+ }
+
+ // Iterate through the nested groups to find any matching form field groups.
+ for ($i = 1, $n = count($group); $i < $n; $i++)
+ {
+ // Initialise some loop variables.
+ $validNames = array_slice($group, 0, $i + 1);
+ $current = $tmp;
+ $tmp = array();
+
+ // Check to make sure that there are no parent groups for each element.
+ foreach ($current as $element)
+ {
+ // Get any fields elements with the correct group name.
+ $children = $element->xpath('descendant::fields[@name="' . (string) $group[$i] . '"]');
+
+ // For the found fields elements validate that they are in the correct groups.
+ foreach ($children as $fields)
+ {
+ // Get the group names as strings for ancestor fields elements.
+ $attrs = $fields->xpath('ancestor-or-self::fields[@name]/@name');
+ $names = array_map('strval', $attrs ? $attrs : array());
+
+ // If the group names for the fields element match the valid names at this
+ // level add the fields element.
+ if ($validNames == $names)
+ {
+ $tmp[] = $fields;
+ }
+ }
+ }
+ }
+
+ // Only include valid XML objects.
+ foreach ($tmp as $element)
+ {
+ if ($element instanceof SimpleXMLElement)
+ {
+ $groups[] = $element;
+ }
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Method to load, setup and return a FormField object based on field data.
+ *
+ * @param string $element The XML element object representation of the form field.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @param mixed $value The optional value to use as the default for the field.
+ * @return mixed The Field object for the field or boolean false on error.
+ */
+ protected function loadField($element, $group = null, $value = null)
+ {
+ // Make sure there is a valid SimpleXMLElement.
+ if (!($element instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+
+ // Get the field type.
+ $type = $element['type'] ? (string) $element['type'] : 'text';
+
+ // Can't have a class called "List", so we alias it
+ if ($type == 'list')
+ {
+ $type = 'select';
+ }
+
+ // Load the FormField object for the field.
+ $field = $this->loadFieldType($type);
+
+ // If the object could not be loaded, get a text field object.
+ if ($field === false)
+ {
+ $field = $this->loadFieldType('text');
+ }
+
+ // Get the value for the form field if not set.
+ // Default to the translated version of the 'default' attribute
+ // if 'translate_default' attribute if set to 'true' or '1'
+ // else the value of the 'default' attribute for the field.
+ if ($value === null)
+ {
+ $default = (string) $element['default'];
+ if (($translate = $element['translate_default']) && ((string) $translate == 'true' || (string) $translate == '1'))
+ {
+ $lang = App::get('language');
+ if ($lang->hasKey($default))
+ {
+ $debug = $lang->setDebug(false);
+ $default = $lang->txt($default);
+
+ $lang->setDebug($debug);
+ }
+ else
+ {
+ $default = $lang->txt($default);
+ }
+ }
+ $value = $this->getValue((string) $element['name'], $group, $default);
+ }
+
+ if (!is_object($field))
+ {
+ return false;
+ }
+
+ // Setup the FormField object.
+ $field->setForm($this);
+
+ if ($field->setup($element, $value, $group))
+ {
+ return $field;
+ }
+
+ return false;
+ }
+
+ /**
+ * Proxy for Helper::loadFieldType().
+ *
+ * @param string $type The field type.
+ * @param boolean $new Flag to toggle whether we should get a new instance of the object.
+ * @return mixed Field object on success, false otherwise.
+ */
+ protected function loadFieldType($type, $new = true)
+ {
+ return Helper::loadFieldType($type, $new);
+ }
+
+ /**
+ * Proxy for Helper::loadRuleType().
+ *
+ * @param string $type The rule type.
+ * @param boolean $new Flag to toggle whether we should get a new instance of the object.
+ * @return mixed Rule object on success, false otherwise.
+ */
+ protected function loadRuleType($type, $new = true)
+ {
+ return Helper::loadRuleType($type, $new);
+ }
+
+ /**
+ * Method to synchronize any field, form or rule paths contained in the XML document.
+ *
+ * @return boolean True on success.
+ */
+ protected function syncPaths()
+ {
+ // Make sure there is a valid Form XML document.
+ if (!($this->xml instanceof SimpleXMLElement))
+ {
+ return false;
+ }
+
+ // Get any addfieldpath attributes from the form definition.
+ $paths = $this->xml->xpath('//*[@addfieldpath]/@addfieldpath');
+ $paths = array_map('strval', $paths ? $paths : array());
+
+ // Add the field paths.
+ foreach ($paths as $path)
+ {
+ $path = PATH_ROOT . '/' . ltrim($path, '/\\');
+ self::addFieldPath($path);
+ }
+
+ // Get any addformpath attributes from the form definition.
+ $paths = $this->xml->xpath('//*[@addformpath]/@addformpath');
+ $paths = array_map('strval', $paths ? $paths : array());
+
+ // Add the form paths.
+ foreach ($paths as $path)
+ {
+ $path = PATH_ROOT . '/' . ltrim($path, '/\\');
+ self::addFormPath($path);
+ }
+
+ // Get any addrulepath attributes from the form definition.
+ $paths = $this->xml->xpath('//*[@addrulepath]/@addrulepath');
+ $paths = array_map('strval', $paths ? $paths : array());
+
+ // Add the rule paths.
+ foreach ($paths as $path)
+ {
+ $path = PATH_ROOT . '/' . ltrim($path, '/\\');
+ self::addRulePath($path);
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to validate a Field object based on field data.
+ *
+ * @param string $element The XML element object representation of the form field.
+ * @param string $group The optional dot-separated form group path on which to find the field.
+ * @param mixed $value The optional value to use as the default for the field.
+ * @param object $input An optional Registry object with the entire data set to validate against the entire form.
+ * @return mixed Boolean true if field value is valid, Exception on failure.
+ */
+ protected function validateField($element, $group = null, $value = null, $input = null)
+ {
+ // Make sure there is a valid SimpleXMLElement.
+ if (!$element instanceof SimpleXMLElement)
+ {
+ return new Exception(Lang::txt('JLIB_FORM_ERROR_VALIDATE_FIELD'));
+ }
+
+ // Initialise variables.
+ $valid = true;
+
+ // Check if the field is required.
+ $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required');
+
+ if ($required)
+ {
+ // If the field is required and the value is empty return an error message.
+ if (($value === '') || ($value === null))
+ {
+ // Does the field have a defined error message?
+ if ($element['message'])
+ {
+ $message = $element['message'];
+ }
+ else
+ {
+ if ($element['label'])
+ {
+ $message = Lang::txt($element['label']);
+ }
+ else
+ {
+ $message = Lang::txt($element['name']);
+ }
+ $message = Lang::txt('JLIB_FORM_VALIDATE_FIELD_REQUIRED', $message);
+ }
+ return new MissingData($message);
+ }
+ }
+
+ // Get the field validation rule.
+ if ($type = (string) $element['validate'])
+ {
+ // Load the FormRule object for the field.
+ $rule = $this->loadRuleType($type);
+
+ // If the object could not be loaded return an error message.
+ if ($rule === false)
+ {
+ return new Exception(Lang::txt('JLIB_FORM_VALIDATE_FIELD_RULE_MISSING', $type));
+ }
+
+ // Run the field validation rule test.
+ $valid = $rule->test($element, $value, $group, $input, $this);
+
+ // Check for an error in the validation test.
+ if ($valid instanceof Exception)
+ {
+ return $valid;
+ }
+ }
+
+ // Check if the field is valid.
+ if ($valid === false)
+ {
+ // Does the field have a defined error message?
+ $message = (string) $element['message'];
+
+ if ($message)
+ {
+ $message = Lang::txt($message);
+ }
+ else
+ {
+ $message = Lang::txt('JLIB_FORM_VALIDATE_FIELD_INVALID', Lang::txt((string) $element['label']));
+ }
+
+ return new InvalidData($message);
+ }
+
+ return true;
+ }
+
+ /**
+ * Proxy for Helper::addFieldPath().
+ *
+ * @param mixed $new A path or array of paths to add.
+ * @return array The list of paths that have been added.
+ */
+ public static function addFieldPath($new = null)
+ {
+ return Helper::addFieldPath($new);
+ }
+
+ /**
+ * Proxy for Helper::addFormPath().
+ *
+ * @param mixed $new A path or array of paths to add.
+ * @return array The list of paths that have been added.
+ */
+ public static function addFormPath($new = null)
+ {
+ return Helper::addFormPath($new);
+ }
+
+ /**
+ * Proxy for Helper::addRulePath().
+ *
+ * @param mixed $new A path or array of paths to add.
+ * @return array The list of paths that have been added.
+ */
+ public static function addRulePath($new = null)
+ {
+ return Helper::addRulePath($new);
+ }
+
+ /**
+ * Method to get an instance of a form.
+ *
+ * @param string $name The name of the form.
+ * @param string $data The name of an XML file or string to load as the form definition.
+ * @param array $options An array of form options.
+ * @param string $replace Flag to toggle whether form fields should be replaced if a field already exists with the same group/name.
+ * @param string $xpath An optional xpath to search for the fields.
+ * @return object Form instance.
+ * @throws Exception if an error occurs.
+ */
+ public static function getInstance($name, $data = null, $options = array(), $replace = true, $xpath = false)
+ {
+ // Reference to array with form instances
+ $forms = &self::$forms;
+
+ // Only instantiate the form if it does not already exist.
+ if (!isset($forms[$name]))
+ {
+
+ $data = trim($data);
+
+ if (empty($data))
+ {
+ throw new MissingData(Lang::txt('JLIB_FORM_ERROR_NO_DATA'));
+ }
+
+ // Instantiate the form.
+ $forms[$name] = new self($name, $options);
+
+ // Load the data.
+ if (substr(trim($data), 0, 1) == '<')
+ {
+ if ($forms[$name]->load($data, $replace, $xpath) == false)
+ {
+ throw new Exception(Lang::txt('JLIB_FORM_ERROR_XML_FILE_DID_NOT_LOAD'));
+
+ return false;
+ }
+ }
+ else
+ {
+ if ($forms[$name]->loadFile($data, $replace, $xpath) == false)
+ {
+ throw new Exception(Lang::txt('JLIB_FORM_ERROR_XML_FILE_DID_NOT_LOAD'));
+
+ return false;
+ }
+ }
+ }
+
+ return $forms[$name];
+ }
+
+ /**
+ * Adds a new child SimpleXMLElement node to the source.
+ *
+ * @param object $source The source element on which to append.
+ * @param object $new The new element to append.
+ * @return void
+ * @throws Exception if an error occurs.
+ */
+ protected static function addNode(SimpleXMLElement $source, SimpleXMLElement $new)
+ {
+ // Add the new child node.
+ $node = $source->addChild($new->getName(), trim($new));
+
+ // Add the attributes of the child node.
+ foreach ($new->attributes() as $name => $value)
+ {
+ $node->addAttribute($name, $value);
+ }
+
+ // Add any children of the new node.
+ foreach ($new->children() as $child)
+ {
+ self::addNode($node, $child);
+ }
+ }
+
+ /**
+ * Adds a new child SimpleXMLElement node to the source.
+ *
+ * @param object $source The source element on which to append.
+ * @param object $new The new element to append.
+ * @return void
+ */
+ protected static function mergeNode(SimpleXMLElement $source, SimpleXMLElement $new)
+ {
+ // Update the attributes of the child node.
+ foreach ($new->attributes() as $name => $value)
+ {
+ if (isset($source[$name]))
+ {
+ $source[$name] = (string) $value;
+ }
+ else
+ {
+ $source->addAttribute($name, $value);
+ }
+ }
+
+ // What to do with child elements?
+ }
+
+ /**
+ * Merges new elements into a source element.
+ *
+ * @param object $source The source element.
+ * @param object $new The new element to merge.
+ * @return void
+ */
+ protected static function mergeNodes(SimpleXMLElement $source, SimpleXMLElement $new)
+ {
+ // The assumption is that the inputs are at the same relative level.
+ // So we just have to scan the children and deal with them.
+
+ // Update the attributes of the child node.
+ foreach ($new->attributes() as $name => $value)
+ {
+ if (isset($source[$name]))
+ {
+ $source[$name] = (string) $value;
+ }
+ else
+ {
+ $source->addAttribute($name, $value);
+ }
+ }
+
+ foreach ($new->children() as $child)
+ {
+ $type = $child->getName();
+ $name = $child['name'];
+
+ // Does this node exist?
+ $fields = $source->xpath($type . '[@name="' . $name . '"]');
+
+ if (empty($fields))
+ {
+ // This node does not exist, so add it.
+ self::addNode($source, $child);
+ }
+ else
+ {
+ // This node does exist.
+ switch ($type)
+ {
+ case 'field':
+ self::mergeNode($fields[0], $child);
+ break;
+
+ default:
+ self::mergeNodes($fields[0], $child);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads a XML file.
+ *
+ * @param string $data Full path and file name.
+ * @param boolean $isFile true to load a file or false to load a string.
+ * @return mixed XMLElement on success or false on error.
+ */
+ public static function getXML($data, $isFile = true)
+ {
+ // Disable libxml errors and allow to fetch error information as needed
+ libxml_use_internal_errors(true);
+
+ if ($isFile)
+ {
+ // Try to load the XML file
+ $xml = simplexml_load_file($data);
+ }
+ else
+ {
+ // Try to load the XML string
+ $xml = simplexml_load_string($data);
+ }
+
+ return $xml;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Helper.php b/core/libraries/Hubzero/Form/Helper.php
new file mode 100644
index 00000000000..1bd088998d4
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Helper.php
@@ -0,0 +1,306 @@
+
+ * paths:
+ * {ENTITY_NAME}:
+ * - /path/1
+ * - /path/2
+ *
+ *
+ * @var array
+ */
+ protected static $paths;
+
+ /**
+ * Static array of Form's entity objects for re-use.
+ * Prototypes for all fields and rules are here.
+ *
+ * Array's structure:
+ *
+ * entities:
+ * {ENTITY_NAME}:
+ * {KEY}: {OBJECT}
+ *
+ *
+ * @var array
+ */
+ protected static $entities = array();
+
+ /**
+ * Method to load a form field object given a type.
+ *
+ * @param string $type The field type.
+ * @param boolean $new Flag to toggle whether we should get a new instance of the object.
+ * @return mixed Field object on success, false otherwise.
+ */
+ public static function loadFieldType($type, $new = true)
+ {
+ return self::loadType('field', $type, $new);
+ }
+
+ /**
+ * Method to load a form rule object given a type.
+ *
+ * @param string $type The rule type.
+ * @param boolean $new Flag to toggle whether we should get a new instance of the object.
+ * @return mixed Rule object on success, false otherwise.
+ */
+ public static function loadRuleType($type, $new = true)
+ {
+ return self::loadType('rule', $type, $new);
+ }
+
+ /**
+ * Method to load a form entity object given a type.
+ * Each type is loaded only once and then used as a prototype for other objects of same type.
+ * Please, use this method only with those entities which support types (forms don't support them).
+ *
+ * @param string $entity The entity.
+ * @param string $type The entity type.
+ * @param boolean $new Flag to toggle whether we should get a new instance of the object.
+ * @return mixed Entity object on success, false otherwise.
+ */
+ protected static function loadType($entity, $type, $new = true)
+ {
+ // Reference to an array with current entity's type instances
+ $types = &self::$entities[$entity];
+
+ // Initialize variables.
+ $key = md5($type);
+ $class = '';
+
+ // Return an entity object if it already exists and we don't need a new one.
+ if (isset($types[$key]) && $new === false)
+ {
+ return $types[$key];
+ }
+
+ if (($class = self::loadClass($entity, $type)) !== false)
+ {
+ // Instantiate a new type object.
+ $types[$key] = new $class;
+
+ return $types[$key];
+ }
+
+ return false;
+ }
+
+ /**
+ * Attempt to import the Field class file if it isn't already imported.
+ * You can use this method outside of Form for loading a field for inheritance or composition.
+ *
+ * @param string $type Type of a field whose class should be loaded.
+ * @return mixed Class name on success or false otherwise.
+ */
+ public static function loadFieldClass($type)
+ {
+ return self::loadClass('field', $type);
+ }
+
+ /**
+ * Attempt to import the Rule class file if it isn't already imported.
+ * You can use this method outside of Form for loading a rule for inheritance or composition.
+ *
+ * @param string $type Type of a rule whose class should be loaded.
+ * @return mixed Class name on success or false otherwise.
+ */
+ public static function loadRuleClass($type)
+ {
+ return self::loadClass('rule', $type);
+ }
+
+ /**
+ * Load a class for one of the form's entities of a particular type.
+ * Currently, it makes sense to use this method for the "field" and "rule" entities
+ * (but you can support more entities in your subclass).
+ *
+ * @param string $entity One of the form entities (field or rule).
+ * @param string $type Type of an entity.
+ * @return mixed Class name on success or false otherwise.
+ */
+ protected static function loadClass($entity, $type)
+ {
+ $parts = explode('_', $type);
+ $parts = array_map('ucfirst', $parts);
+ $parts = implode('\\', $parts);
+
+ $class = __NAMESPACE__ . '\\' . ucfirst($entity) . 's' . '\\' . $parts;
+
+ if (class_exists($class))
+ {
+ return $class;
+ }
+
+ // Get the field search path array.
+ $paths = self::addPath($entity);
+
+ // If the type is complex, add the base type to the paths.
+ if ($pos = strpos($type, '_'))
+ {
+
+ // Add the complex type prefix to the paths.
+ for ($i = 0, $n = count($paths); $i < $n; $i++)
+ {
+ // Derive the new path.
+ $path = $paths[$i] . '/' . strtolower(substr($type, 0, $pos));
+
+ // If the path does not exist, add it.
+ if (!in_array($path, $paths))
+ {
+ $paths[] = $path;
+ }
+ }
+ // Break off the end of the complex type.
+ $type = substr($type, $pos + 1);
+ }
+
+ // Try to find the class file.
+ $type = strtolower($type) . '.php';
+
+ foreach ($paths as $path)
+ {
+ if ($file = self::find($path, $type))
+ {
+ require_once $file;
+
+ if (class_exists($class))
+ {
+ break;
+ }
+ }
+ }
+
+ // Check for all if the class exists.
+ return class_exists($class) ? $class : false;
+ }
+
+ /**
+ * Searches the directory paths for a given file.
+ *
+ * @param mixed $paths An path string or array of path strings to search in
+ * @param string $file The file name to look for.
+ * @return mixed Full path and name for the target file, or false if file not found.
+ */
+ protected static function find($paths, $file)
+ {
+ $paths = is_array($paths) ? $paths : array($paths);
+
+ foreach ($paths as $path)
+ {
+ $fullname = $path . DIRECTORY_SEPARATOR . $file;
+
+ // Is the path based on a stream?
+ if (strpos($path, '://') === false)
+ {
+ // Not a stream, so do a realpath() to avoid directory
+ // traversal attempts on the local file system.
+ $path = realpath($path);
+ $fullname = realpath($fullname);
+ }
+
+ // The substr() check added to make sure that the realpath()
+ // results in a directory registered so that
+ // non-registered directories are not accessible via directory
+ // traversal attempts.
+ if (file_exists($fullname) && substr($fullname, 0, strlen($path)) == $path)
+ {
+ return $fullname;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Method to add a path to the list of field include paths.
+ *
+ * @param mixed $new A path or array of paths to add.
+ * @return array The list of paths that have been added.
+ */
+ public static function addFieldPath($new = null)
+ {
+ return self::addPath('field', $new);
+ }
+
+ /**
+ * Method to add a path to the list of form include paths.
+ *
+ * @param mixed $new A path or array of paths to add.
+ * @return array The list of paths that have been added.
+ */
+ public static function addFormPath($new = null)
+ {
+ return self::addPath('form', $new);
+ }
+
+ /**
+ * Method to add a path to the list of rule include paths.
+ *
+ * @param mixed $new A path or array of paths to add.
+ * @return array The list of paths that have been added.
+ */
+ public static function addRulePath($new = null)
+ {
+ return self::addPath('rule', $new);
+ }
+
+ /**
+ * Method to add a path to the list of include paths for one of the form's entities.
+ * Currently supported entities: field, rule and form. You are free to support your own in a subclass.
+ *
+ * @param string $entity Form's entity name for which paths will be added.
+ * @param mixed $new A path or array of paths to add.
+ * @return array The list of paths that have been added.
+ */
+ protected static function addPath($entity, $new = null)
+ {
+ // Reference to an array with paths for current entity
+ $paths = &self::$paths[$entity];
+
+ // Add the default entity's search path if not set.
+ if (empty($paths))
+ {
+ // While we support limited number of entities (form, field and rule)
+ // we can do this simple pluralisation:
+ $entity_plural = ucfirst($entity) . 's';
+
+ $paths[] = __DIR__ . DIRECTORY_SEPARATOR . $entity_plural;
+ }
+
+ // Force the new path(s) to an array.
+ settype($new, 'array');
+
+ // Add the new paths to the stack if not already there.
+ foreach ($new as $path)
+ {
+ if (!in_array($path, $paths))
+ {
+ array_unshift($paths, trim($path));
+ }
+ }
+
+ return $paths;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rule.php b/core/libraries/Hubzero/Form/Rule.php
new file mode 100644
index 00000000000..14d4a6db752
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rule.php
@@ -0,0 +1,77 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ * @throws Exception on invalid rule.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ // Check for a valid regex.
+ if (empty($this->regex))
+ {
+ throw new Exception(Lang::txt('JLIB_FORM_INVALID_FORM_RULE', get_class($this)));
+ }
+
+ // Add unicode property support if available.
+ if (JCOMPAT_UNICODE_PROPERTIES)
+ {
+ $this->modifiers = (strpos($this->modifiers, 'u') !== false) ? $this->modifiers : $this->modifiers . 'u';
+ }
+
+ // Test the value against the regular expression.
+ if (preg_match(chr(1) . $this->regex . chr(1) . $this->modifiers, $value))
+ {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rules/Boolean.php b/core/libraries/Hubzero/Form/Rules/Boolean.php
new file mode 100644
index 00000000000..11accd597dd
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rules/Boolean.php
@@ -0,0 +1,30 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ $value = trim($value);
+
+ if (empty($value))
+ {
+ // A color field can't be empty, we default to black. This is the same as the HTML5 spec.
+ $value = '#000000';
+ return true;
+ }
+
+ if ($value[0] != '#')
+ {
+ return false;
+ }
+
+ // Remove the leading # if present to validate the numeric part
+ $value = ltrim($value, '#');
+
+ // The value must be 6 or 3 characters long
+ if (!((strlen($value) == 6 || strlen($value) == 3) && ctype_xdigit($value)))
+ {
+ return false;
+ }
+
+ // Prepend the # again
+ $value = '#' . $value;
+
+ return true;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rules/Email.php b/core/libraries/Hubzero/Form/Rules/Email.php
new file mode 100644
index 00000000000..391f247b217
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rules/Email.php
@@ -0,0 +1,75 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ // If the field is empty and not required, the field is valid.
+ $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required');
+
+ if (!$required && empty($value))
+ {
+ return true;
+ }
+
+ // Test the value against the regular expression.
+ if (!parent::test($element, $value, $group, $input, $form))
+ {
+ return false;
+ }
+
+ // Check if we should test for uniqueness.
+ $unique = ((string) $element['unique'] == 'true' || (string) $element['unique'] == 'unique');
+
+ if ($unique)
+ {
+ // Get the extra field check attribute.
+ $userId = ($form instanceof Form) ? $form->getValue('id') : '';
+
+ $duplicate = User::all()
+ ->whereEquals('email', $value)
+ ->where('id', '<>', (int) $userId)
+ ->total();
+
+ if ($duplicate)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rules/Equals.php b/core/libraries/Hubzero/Form/Rules/Equals.php
new file mode 100644
index 00000000000..3681e8eb2a9
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rules/Equals.php
@@ -0,0 +1,56 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ // Initialize variables.
+ $field = (string) $element['field'];
+
+ // Check that a validation field is set.
+ if (!$field)
+ {
+ return new Exception('JLIB_FORM_INVALID_FORM_RULE' . get_class($this));
+ }
+
+ // Check that a valid Form object is given for retrieving the validation field value.
+ if (!($form instanceof Form))
+ {
+ return new Exception('JLIB_FORM_INVALID_FORM_OBJECT' . get_class($this));
+ }
+
+ // Test the two values against each other.
+ if ($value == $input->get($field))
+ {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rules/Options.php b/core/libraries/Hubzero/Form/Rules/Options.php
new file mode 100644
index 00000000000..308e2c1e158
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rules/Options.php
@@ -0,0 +1,43 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ * @throws Exception on invalid rule.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ // Check each value and return true if we get a match
+ foreach ($element->option as $option)
+ {
+ if ($value == (string) $option->attributes()->value)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rules/Rules.php b/core/libraries/Hubzero/Form/Rules/Rules.php
new file mode 100644
index 00000000000..cf0f6a2d8f1
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rules/Rules.php
@@ -0,0 +1,109 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ // Get the possible field actions and the ones posted to validate them.
+ $fieldActions = self::getFieldActions($element);
+ $valueActions = self::getValueActions($value);
+
+ // Make sure that all posted actions are in the list of possible actions for the field.
+ foreach ($valueActions as $action)
+ {
+ if (!in_array($action, $fieldActions))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to get the list of permission action names from the form field value.
+ *
+ * @param mixed $value The form field value to validate.
+ * @return array A list of permission action names from the form field value.
+ */
+ protected function getValueActions($value)
+ {
+ // Initialise variables.
+ $actions = array();
+
+ // Iterate over the asset actions and add to the actions.
+ foreach ((array) $value as $name => $rules)
+ {
+ $actions[] = $name;
+ }
+
+ return $actions;
+ }
+
+ /**
+ * Method to get the list of possible permission action names for the form field.
+ *
+ * @param object $element The SimpleXMLElement object representing the tag for the form field object.
+ * @return array A list of permission action names from the form field element definition.
+ */
+ protected function getFieldActions($element)
+ {
+ // Initialise variables.
+ $actions = array();
+
+ // Initialise some field attributes.
+ $section = $element['section'] ? (string) $element['section'] : '';
+ $component = $element['component'] ? (string) $element['component'] : '';
+
+ // Get the asset actions for the element.
+ $component = $component ? \App::get('component')->path($component) . '/config/access.xml' : '';
+ $section = $section ? "/access/section[@name='" . $section . "']/" : '';
+
+ $elActions = Access::getActionsFromFile($component, $section);
+
+ if (is_array($elActions))
+ {
+ // Iterate over the asset actions and add to the actions.
+ foreach ($elActions as $item)
+ {
+ $actions[] = $item->name;
+ }
+ }
+
+ // Iterate over the children and add to the actions.
+ foreach ($element->children() as $el)
+ {
+ if ($el->getName() == 'action')
+ {
+ $actions[] = (string) $el['name'];
+ }
+ }
+
+ return $actions;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rules/Tel.php b/core/libraries/Hubzero/Form/Rules/Tel.php
new file mode 100644
index 00000000000..50dcc8ccae1
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rules/Tel.php
@@ -0,0 +1,96 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ // If the field is empty and not required, the field is valid.
+ $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required');
+
+ if (!$required && empty($value))
+ {
+ return true;
+ }
+
+ // @see http://www.nanpa.com/
+ // @see http://tools.ietf.org/html/rfc4933
+ // @see http://www.itu.int/rec/T-REC-E.164/en
+
+ // Regex by Steve Levithan
+ // @see http://blog.stevenlevithan.com/archives/validate-phone-number
+ // @note that valid ITU-T and EPP must begin with +.
+ $regexarray = array(
+ 'NANP' => '/^(?:\+?1[-. ]?)?\(?([2-9][0-8][0-9])\)?[-. ]?([2-9][0-9]{2})[-. ]?([0-9]{4})$/',
+ 'ITU-T' => '/^\+(?:[0-9] ?){6,14}[0-9]$/',
+ 'EPP' => '/^\+[0-9]{1,3}\.[0-9]{4,14}(?:x.+)?$/'
+ );
+
+ if (isset($element['plan']))
+ {
+
+ $plan = (string) $element['plan'];
+ if ($plan == 'northamerica' || $plan == 'us')
+ {
+ $plan = 'NANP';
+ }
+ elseif ($plan == 'International' || $plan == 'int' || $plan == 'missdn' || !$plan)
+ {
+ $plan = 'ITU-T';
+ }
+ elseif ($plan == 'IETF')
+ {
+ $plan = 'EPP';
+ }
+
+ $regex = $regexarray[$plan];
+ // Test the value against the regular expression.
+ if (preg_match($regex, $value) == false)
+ {
+
+ return false;
+ }
+ }
+ else
+ {
+ // If the rule is set but no plan is selected just check that there are between
+ // 7 and 15 digits inclusive and no illegal characters (but common number separators
+ // are allowed).
+ $cleanvalue = preg_replace('/[+. \-(\)]/', '', $value);
+ $regex = '/^[0-9]{7,15}?$/';
+ if (preg_match($regex, $cleanvalue) == true)
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rules/Url.php b/core/libraries/Hubzero/Form/Rules/Url.php
new file mode 100644
index 00000000000..ff542883fc8
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rules/Url.php
@@ -0,0 +1,256 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ // If the field is empty and not required, the field is valid.
+ $required = ((string) $element['required'] == 'true' || (string) $element['required'] == 'required');
+ if (!$required && empty($value))
+ {
+ return true;
+ }
+
+ $urlParts = self::parseUrl($value);
+
+ // See http://www.w3.org/Addressing/URL/url-spec.txt
+ // Use the full list or optionally specify a list of permitted schemes.
+ if ($element['schemes'] == '')
+ {
+ $scheme = array('http', 'https', 'ftp', 'ftps', 'gopher', 'mailto', 'news', 'prospero', 'telnet', 'rlogin', 'tn3270', 'wais', 'url',
+ 'mid', 'cid', 'nntp', 'tel', 'urn', 'ldap', 'file', 'fax', 'modem', 'git');
+ }
+ else
+ {
+ $scheme = explode(',', $element['schemes']);
+
+ }
+ // This rule is only for full URLs with schemes because parse_url does not parse
+ // accurately without a scheme.
+ // @see http://php.net/manual/en/function.parse-url.php
+ if (!array_key_exists('scheme', $urlParts))
+ {
+ return false;
+ }
+ $urlScheme = (string) $urlParts['scheme'];
+ $urlScheme = strtolower($urlScheme);
+ if (in_array($urlScheme, $scheme) == false)
+ {
+ return false;
+ }
+
+ // For some schemes here must be two slashes.
+ if (($urlScheme == 'http' || $urlScheme == 'https' || $urlScheme == 'ftp' || $urlScheme == 'sftp' || $urlScheme == 'gopher'
+ || $urlScheme == 'wais' || $urlScheme == 'gopher' || $urlScheme == 'prospero' || $urlScheme == 'telnet' || $urlScheme == 'git')
+ && ((substr($value, strlen($urlScheme), 3)) !== '://'))
+ {
+ return false;
+ }
+
+ // The best we can do for the rest is make sure that the strings are valid UTF-8
+ // and the port is an integer.
+ if (array_key_exists('host', $urlParts) && !self::valid((string) $urlParts['host']))
+ {
+ return false;
+ }
+ if (array_key_exists('port', $urlParts) && !is_int((int) $urlParts['port']))
+ {
+ return false;
+ }
+ if (array_key_exists('path', $urlParts) && !self::valid((string) $urlParts['path']))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Does a UTF-8 safe version of PHP parse_url function
+ *
+ * @param string $url URL to parse
+ * @return mixed Associative array or false if badly formed URL.
+ */
+ public static function parseUrl($url)
+ {
+ $result = false;
+
+ // Build arrays of values we need to decode before parsing
+ $entities = array('%21', '%2A', '%27', '%28', '%29', '%3B', '%3A', '%40', '%26', '%3D', '%24', '%2C', '%2F', '%3F', '%25', '%23', '%5B', '%5D');
+ $replacements = array('!', '*', "'", "(", ")", ";", ":", "@", "&", "=", "$", ",", "/", "?", "%", "#", "[", "]");
+
+ // Create encoded URL with special URL characters decoded so it can be parsed
+ // All other characters will be encoded
+ $encodedURL = str_replace($entities, $replacements, urlencode($url));
+
+ // Parse the encoded URL
+ $encodedParts = parse_url($encodedURL);
+
+ // Now, decode each value of the resulting array
+ if ($encodedParts)
+ {
+ foreach ($encodedParts as $key => $value)
+ {
+ $result[$key] = urldecode($value);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Tests a string as to whether it's valid UTF-8 and supported by the Unicode standard.
+ *
+ * Note: this function has been modified to simple return true or false.
+ *
+ * @param string $str UTF-8 encoded string.
+ * @return boolean true if valid
+ */
+ public static function valid($str)
+ {
+ // Cached expected number of octets after the current octet
+ // until the beginning of the next UTF8 character sequence
+ $mState = 0;
+
+ // Cached Unicode character
+ $mUcs4 = 0;
+
+ // Cached expected number of octets in the current sequence
+ $mBytes = 1;
+
+ $len = strlen($str);
+
+ for ($i = 0; $i < $len; $i++)
+ {
+ $in = ord($str{$i});
+
+ if ($mState == 0)
+ {
+ // When mState is zero we expect either a US-ASCII character or a
+ // multi-octet sequence.
+ if (0 == (0x80 & ($in)))
+ {
+ // US-ASCII, pass straight through.
+ $mBytes = 1;
+ }
+ elseif (0xC0 == (0xE0 & ($in)))
+ {
+ // First octet of 2 octet sequence
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 0x1F) << 6;
+ $mState = 1;
+ $mBytes = 2;
+ }
+ elseif (0xE0 == (0xF0 & ($in)))
+ {
+ // First octet of 3 octet sequence
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 0x0F) << 12;
+ $mState = 2;
+ $mBytes = 3;
+ }
+ elseif (0xF0 == (0xF8 & ($in)))
+ {
+ // First octet of 4 octet sequence
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 0x07) << 18;
+ $mState = 3;
+ $mBytes = 4;
+ }
+ elseif (0xF8 == (0xFC & ($in)))
+ {
+ // First octet of 5 octet sequence.
+ //
+ // This is illegal because the encoded codepoint must be either
+ // (a) not the shortest form or
+ // (b) outside the Unicode range of 0-0x10FFFF.
+ // Rather than trying to resynchronize, we will carry on until the end
+ // of the sequence and let the later error handling code catch it.
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 0x03) << 24;
+ $mState = 4;
+ $mBytes = 5;
+ }
+ elseif (0xFC == (0xFE & ($in)))
+ {
+ // First octet of 6 octet sequence, see comments for 5 octet sequence.
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 1) << 30;
+ $mState = 5;
+ $mBytes = 6;
+ }
+ else
+ {
+ // Current octet is neither in the US-ASCII range nor a legal first
+ // octet of a multi-octet sequence.
+ return false;
+ }
+ }
+ else
+ {
+ // When mState is non-zero, we expect a continuation of the multi-octet
+ // sequence
+ if (0x80 == (0xC0 & ($in)))
+ {
+ // Legal continuation.
+ $shift = ($mState - 1) * 6;
+ $tmp = $in;
+ $tmp = ($tmp & 0x0000003F) << $shift;
+ $mUcs4 |= $tmp;
+
+ // End of the multi-octet sequence. mUcs4 now contains the final
+ // Unicode codepoint to be output
+ if (0 == --$mState)
+ {
+ // Check for illegal sequences and codepoints.
+ // From Unicode 3.1, non-shortest form is illegal
+ if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || ((3 == $mBytes) && ($mUcs4 < 0x0800)) || ((4 == $mBytes) && ($mUcs4 < 0x10000))
+ || (4 < $mBytes)
+ || (($mUcs4 & 0xFFFFF800) == 0xD800) // From Unicode 3.2, surrogate characters are illegal
+ || ($mUcs4 > 0x10FFFF)) // Codepoints outside the Unicode range are illegal
+ {
+ return false;
+ }
+
+ // Initialize UTF8 cache.
+ $mState = 0;
+ $mUcs4 = 0;
+ $mBytes = 1;
+ }
+ }
+ else
+ {
+ //((0xC0 & (*in) != 0x80) && (mState != 0))
+ // Incomplete multi-octet sequence.
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/core/libraries/Hubzero/Form/Rules/Username.php b/core/libraries/Hubzero/Form/Rules/Username.php
new file mode 100644
index 00000000000..427f7712736
--- /dev/null
+++ b/core/libraries/Hubzero/Form/Rules/Username.php
@@ -0,0 +1,44 @@
+ tag for the form field object.
+ * @param mixed $value The form field value to validate.
+ * @param string $group The field name group control value. This acts as as an array container for the field.
+ * For example if the field has name="foo" and the group value is set to "bar" then the
+ * full field name would end up being "bar[foo]".
+ * @param object &$input An optional Registry object with the entire data set to validate against the entire form.
+ * @param object &$form The form object for which the field is being tested.
+ * @return boolean True if the value is valid, false otherwise.
+ */
+ public function test(&$element, $value, $group = null, &$input = null, &$form = null)
+ {
+ $duplicate = User::all()
+ ->whereEquals('username', $value)
+ ->where('id', '<>', (int) $userId)
+ ->total();
+
+ if ($duplicate)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/core/libraries/Hubzero/Geocode/Geocode.php b/core/libraries/Hubzero/Geocode/Geocode.php
new file mode 100644
index 00000000000..ed292eadc70
--- /dev/null
+++ b/core/libraries/Hubzero/Geocode/Geocode.php
@@ -0,0 +1,509 @@
+registerProvider(new \Geocoder\Provider\ChainProvider($p));
+
+ // Try to get some data...
+ $geocoder->setResultFactory(new Result\CountriesResultFactory());
+
+ $countries = array();
+
+ if ($data = $geocoder->geocode($continent))
+ {
+ foreach ($data as $item)
+ {
+ $country = new \stdClass();
+ $country->code = $item->getCountryCode();
+ $country->name = $item->getCountry();
+ $country->continent = $item->getRegion();
+
+ $countries[] = $country;
+ }
+ }
+
+ self::$countries[$continent] = $countries;
+
+ return self::$countries[$continent];
+ }
+
+ /**
+ * Get country based on short code
+ *
+ * @param string $code Short code (ex: us, de, fr, jp)
+ * @return string
+ */
+ public static function country($code)
+ {
+ $adapter = new \Geocoder\HttpAdapter\CurlHttpAdapter();
+
+ $p = array();
+
+ // Get a list of providers
+ if ($providers = \Event::trigger('geocode.onGeocodeProvider', array('geocode.country', $adapter)))
+ {
+ foreach ($providers as $provider)
+ {
+ if ($provider)
+ {
+ $p[] = $provider;
+ }
+ }
+ }
+
+ if (!count($p))
+ {
+ return '';
+ }
+
+ // Instantiate the Geocoder service and pass it the list of providers
+ $geocoder = new \Geocoder\Geocoder();
+ $geocoder->registerProvider(new \Geocoder\Provider\ChainProvider($p));
+
+ // Try to get some data...
+ $geocoder->setResultFactory(new Result\CountryResultFactory());
+
+ $country = $code;
+ if ($data = $geocoder->geocode($code))
+ {
+ if (is_array($data))
+ {
+ $country = $data[0]->getCountry();
+ }
+ else
+ {
+ $country = $data->getCountry();
+ }
+ }
+ return $country;
+ }
+
+ /**
+ * Geo-locate an address
+ *
+ * @param string $address
+ * @return array
+ */
+ public static function locate($address)
+ {
+ $ip = false;
+ if (filter_var($address, FILTER_VALIDATE_IP))
+ {
+ $ip = true;
+ }
+
+ $adapter = new \Geocoder\HttpAdapter\CurlHttpAdapter();
+
+ $p = array();
+
+ // Get a list of providers
+ if ($providers = \Event::trigger('geocode.onGeocodeProvider', array('geocode.locate', $adapter, $ip)))
+ {
+ foreach ($providers as $provider)
+ {
+ if ($provider)
+ {
+ $p[] = $provider;
+ }
+ }
+ }
+
+ if (!count($p))
+ {
+ return '';
+ }
+
+ // Instantiate the Geocoder service and pass it the list of providers
+ $geocoder = new \Geocoder\Geocoder();
+ $geocoder->registerProvider(new \Geocoder\Provider\ChainProvider($p));
+
+ // Try to get some data...
+ return $geocoder->geocode($address);
+ }
+
+ /**
+ * Get the address (reverse locate)
+ *
+ * @param array $coordinates array(latitude, longitude)
+ * @return array
+ */
+ public static function address($coordinates)
+ {
+ $adapter = new \Geocoder\HttpAdapter\CurlHttpAdapter();
+
+ $p = array();
+
+ // Get a list of providers
+ if ($providers = \Event::trigger('geocode.onGeocodeProvider', array('geocode.address', $adapter)))
+ {
+ foreach ($providers as $provider)
+ {
+ if ($provider)
+ {
+ $p[] = $provider;
+ }
+ }
+ }
+
+ if (!count($p))
+ {
+ return '';
+ }
+
+ $latitude = isset($coordinates['latitude']) ? $coordinates['latitude'] : $coordinates[0];
+ $longitude = isset($coordinates['longitude']) ? $coordinates['longitude'] : $coordinates[1];
+
+ // Instantiate the Geocoder service and pass it the list of providers
+ $geocoder = new \Geocoder\Geocoder();
+ $geocoder->registerProvider(new \Geocoder\Provider\ChainProvider($p));
+
+ // Try to get some data...
+ return $geocoder->reverse($latitude, $longitude);
+ }
+
+ /**
+ * Get the geo database
+ *
+ * @return mixed Database object upon success, null if error
+ */
+ public static function getGeoDBO()
+ {
+ static $instance;
+
+ if (!is_object($instance))
+ {
+ $geodb_params = \Component::params('com_system');
+
+ $options = array();
+ $options['driver'] = $geodb_params->get('geodb_driver', 'pdo');
+ $options['host'] = $geodb_params->get('geodb_host', 'localhost');
+ $options['port'] = $geodb_params->get('geodb_port', '');
+ $options['user'] = $geodb_params->get('geodb_user', '');
+ $options['password'] = $geodb_params->get('geodb_password', '');
+ $options['database'] = $geodb_params->get('geodb_database', '');
+ $options['prefix'] = $geodb_params->get('geodb_prefix', '');
+
+ if (empty($options['database']) || empty($options['user']) || empty($options['password']))
+ {
+ return null;
+ }
+
+ try
+ {
+ $instance = \Hubzero\Database\Driver::getInstance($options);
+ }
+ catch (\Exception $e)
+ {
+ $instance = false;
+ }
+ }
+
+ if (!$instance || ($instance instanceof Exception) || !$instance->getConnection())
+ {
+ return null;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Get a list of countries and their coutnry code
+ *
+ * @return array
+ */
+ public static function getcountries()
+ {
+ $countries = array();
+
+ if (!($gdb = self::getGeoDBO()))
+ {
+ return $countries;
+ }
+
+ $gdb->setQuery("SELECT code, name FROM countries ORDER BY name");
+ $results = $gdb->loadObjectList();
+
+ if ($results)
+ {
+ foreach ($results as $row)
+ {
+ if ($row->code != '-' && $row->name != '-')
+ {
+ array_push($countries, array(
+ 'code' => strtolower($row->code),
+ 'id' => $row->code,
+ 'name' => $row->name
+ ));
+ }
+ }
+ }
+
+ return $countries;
+ }
+
+ /**
+ * Get a list of countries by continent
+ *
+ * @param string $continent
+ * @return array
+ */
+ public static function getCountriesByContinent($continent='')
+ {
+ if (!$continent || !($gdb = self::getGeoDBO()))
+ {
+ return array();
+ }
+
+ $gdb->setQuery("SELECT DISTINCT country FROM country_continent WHERE LOWER(continent) =" . $gdb->quote(strtolower($continent)));
+ return $gdb->loadColumn();
+ }
+
+ /**
+ * Get continent by the country
+ *
+ * @param string $country
+ * @return array
+ */
+ public static function getContinentByCountry($country='')
+ {
+ if (!$country || !($gdb = self::getGeoDBO()))
+ {
+ return array();
+ }
+
+ $gdb->setQuery("SELECT DISTINCT continent FROM country_continent WHERE LOWER(country) ='" . strtolower($country) . "'");
+ return $gdb->loadColumn();
+ }
+
+ /**
+ * Get a list of country codes by names
+ *
+ * @param array $names List of country names
+ * @return array
+ */
+ public static function getCodesByNames($names=array())
+ {
+ if (!($gdb = self::getGeoDBO()))
+ {
+ return array();
+ }
+
+ $names = array_map('strtolower', $names);
+ foreach ($names as $k => $name)
+ {
+ $names[$k] = $gdb->quote($name);
+ }
+
+ $gdb->setQuery("SELECT DISTINCT name, code FROM countries WHERE LOWER(name) IN (" . implode(",", $names) . ")");
+ $values = $gdb->loadAssocList('name');
+ if (!is_array($values))
+ {
+ $values = array();
+ }
+ return $values;
+ }
+
+ /**
+ * Get country based on short code
+ *
+ * @param string $code Short code (ex: us, de, fr, jp)
+ * @return string
+ */
+ public static function getCodeByName($name='')
+ {
+ $code = '';
+ if ($name)
+ {
+ if (!($gdb = self::getGeoDBO()))
+ {
+ return $code;
+ }
+
+ $gdb->setQuery("SELECT code FROM countries WHERE LOWER(name) = " . $gdb->quote(strtolower($name)));
+ $code = stripslashes($gdb->loadResult());
+ }
+ return $code;
+ }
+
+ /**
+ * Get country based on short code
+ *
+ * @param string $code Short code (ex: us, de, fr, jp)
+ * @return string
+ */
+ public static function getcountry($code='')
+ {
+ $name = '';
+ if ($code)
+ {
+ if (!($gdb = self::getGeoDBO()))
+ {
+ return $name;
+ }
+
+ $gdb->setQuery("SELECT name FROM countries WHERE code = " . $gdb->quote($code));
+ $name = stripslashes($gdb->loadResult());
+ }
+ return $name;
+ }
+
+ /**
+ * Get the country based on IP address
+ *
+ * @param string $ip IP address to look up
+ * @return string
+ */
+ public static function ipcountry($ip='')
+ {
+ $country = '';
+ if ($ip)
+ {
+ if (!($gdb = self::getGeoDBO()))
+ {
+ return $country;
+ }
+
+ $sql = "SELECT LOWER(countrySHORT) FROM ipcountry WHERE ipFROM <= INET_ATON(" . $gdb->quote($ip) . ") AND ipTO >= INET_ATON(" . $gdb->quote($ip) . ")";
+ $gdb->setQuery($sql);
+ $country = stripslashes($gdb->loadResult());
+ }
+ return $country;
+ }
+
+ /**
+ * Is a country an D1 nation?
+ *
+ * @param string $country Country to check
+ * @return boolean True if D1
+ */
+ public static function is_d1nation($country)
+ {
+ $d1nation = false;
+ if ($country)
+ {
+ if (!($gdb = self::getGeoDBO()))
+ {
+ return $d1nation;
+ }
+
+ $gdb->setQuery("SELECT COUNT(*) FROM countrygroup WHERE LOWER(countrycode) = LOWER(" . $gdb->quote($country) . ") AND countrygroup = 'D1'");
+ $c = $gdb->loadResult();
+ if ($c > 0)
+ {
+ $d1nation = true;
+ }
+ }
+ return $d1nation;
+ }
+
+ /**
+ * Is a country an E1 nation?
+ *
+ * @param string $country Country to check
+ * @return boolean True if E1
+ */
+ public static function is_e1nation($country)
+ {
+ $e1nation = false;
+ if ($country)
+ {
+ if (!($gdb = self::getGeoDBO()))
+ {
+ return $e1nation;
+ }
+
+ $gdb->setQuery("SELECT COUNT(*) FROM countrygroup WHERE LOWER(countrycode) = LOWER(" . $gdb->quote($country) . ") AND countrygroup = 'E1'");
+ $c = $gdb->loadResult();
+ if ($c > 0)
+ {
+ $e1nation = true;
+ }
+ }
+ return $e1nation;
+ }
+
+ /**
+ * Check if an IP is in a certain location
+ *
+ * @param string $ip IP address to check
+ * @param string $location Location to check in
+ * @return boolean True if IP is in the location
+ */
+ public static function is_iplocation($ip, $location)
+ {
+ $iplocation = false;
+ if ($ip && $location)
+ {
+ if (!($gdb = self::getGeoDBO()))
+ {
+ return $iplocation;
+ }
+
+ $sql = "SELECT COUNT(*) FROM iplocation WHERE ipfrom <= INET_ATON(" . $gdb->quote($ip) . ") AND ipto >= INET_ATON(" . $gdb->quote($ip) . ") AND LOWER(location) = LOWER(" . $gdb->quote($location) . ")";
+ $gdb->setQuery($sql);
+ $c = $gdb->loadResult();
+ if ($c > 0)
+ {
+ $iplocation = true;
+ }
+ }
+ return $iplocation;
+ }
+}
diff --git a/core/libraries/Hubzero/Geocode/Result/CountriesResultFactory.php b/core/libraries/Hubzero/Geocode/Result/CountriesResultFactory.php
new file mode 100644
index 00000000000..203b0b175b8
--- /dev/null
+++ b/core/libraries/Hubzero/Geocode/Result/CountriesResultFactory.php
@@ -0,0 +1,40 @@
+newInstance();
+ $instance->fromArray($row);
+ $result->attach($instance);
+ }
+
+ return $result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function newInstance()
+ {
+ return new Country();
+ }
+}
diff --git a/core/libraries/Hubzero/Geocode/Result/Country.php b/core/libraries/Hubzero/Geocode/Result/Country.php
new file mode 100644
index 00000000000..937a4e8eec6
--- /dev/null
+++ b/core/libraries/Hubzero/Geocode/Result/Country.php
@@ -0,0 +1,240 @@
+
+ */
+class Country extends AbstractResult implements ResultInterface
+{
+ /**
+ * Country name
+ *
+ * @var string
+ */
+ protected $name = null;
+
+ /**
+ * Country code
+ *
+ * @var string
+ */
+ protected $code = null;
+
+ /**
+ * Country continent
+ *
+ * @var string
+ */
+ protected $continent = null;
+
+ /**
+ * Get latitude/longitude coordinates
+ *
+ * @return array
+ */
+ public function getCoordinates()
+ {
+ return array(
+ 'latitude' => 0.0,
+ 'longitude' => 0.0
+ );
+ }
+
+ /**
+ * Get latitude
+ *
+ * @return float
+ */
+ public function getLatitude()
+ {
+ return 0.0;
+ }
+
+ /**
+ * Get longitude
+ *
+ * @return float
+ */
+ public function getLongitude()
+ {
+ return 0.0;
+ }
+
+ /**
+ * Get the coordinates for the "bounding box"
+ * that encompasses the coutnry.
+ *
+ * @return float
+ */
+ public function getBounds()
+ {
+ return array(
+ 'south' => 0.0,
+ 'west' => 0.0,
+ 'north' => 0.0,
+ 'east' => 0.0
+ );
+ }
+
+ /**
+ * Get street number (N/A)
+ *
+ * @return string
+ */
+ public function getStreetNumber()
+ {
+ return '';
+ }
+
+ /**
+ * Get street name (N/A)
+ *
+ * @return string
+ */
+ public function getStreetName()
+ {
+ return '';
+ }
+
+ /**
+ * Get city (N/A)
+ *
+ * @return string
+ */
+ public function getCity()
+ {
+ return '';
+ }
+
+ /**
+ * Get zip code (N/A)
+ *
+ * @return string
+ */
+ public function getZipcode()
+ {
+ return '';
+ }
+
+ /**
+ * Get city district (N/A)
+ *
+ * @return string
+ */
+ public function getCityDistrict()
+ {
+ return '';
+ }
+
+ /**
+ * Get county (N/A)
+ *
+ * @return string
+ */
+ public function getCounty()
+ {
+ return '';
+ }
+
+ /**
+ * Get county code (N/A)
+ *
+ * @return string
+ */
+ public function getCountyCode()
+ {
+ return '';
+ }
+
+ /**
+ * Get region
+ *
+ * @return string
+ */
+ public function getRegion()
+ {
+ return $this->continent;
+ }
+
+ /**
+ * Get region code
+ *
+ * @return string
+ */
+ public function getRegionCode()
+ {
+ return '';
+ }
+
+ /**
+ * Get country name
+ *
+ * @return string
+ */
+ public function getCountry()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get country code
+ *
+ * @return string
+ */
+ public function getCountryCode()
+ {
+ return $this->code;
+ }
+
+ /**
+ * Get timezone (N/A)
+ *
+ * @return string
+ */
+ public function getTimezone()
+ {
+ return '';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function fromArray(array $data = array())
+ {
+ if (isset($data['continent']))
+ {
+ $this->continent = $this->formatString($data['continent']);
+ }
+
+ if (isset($data['name']))
+ {
+ $this->name = $this->formatString($data['name']);
+ }
+
+ if (isset($data['code']))
+ {
+ $this->code = $this->upperize($data['code']);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toArray()
+ {
+ return array(
+ 'name' => $this->name,
+ 'code' => $this->code,
+ 'continent' => $this->continent
+ );
+ }
+}
diff --git a/core/libraries/Hubzero/Geocode/Result/CountryResultFactory.php b/core/libraries/Hubzero/Geocode/Result/CountryResultFactory.php
new file mode 100644
index 00000000000..2cceb774419
--- /dev/null
+++ b/core/libraries/Hubzero/Geocode/Result/CountryResultFactory.php
@@ -0,0 +1,35 @@
+newInstance();
+ $result->fromArray(isset($data[0]) ? $data[0] : $data);
+
+ return $result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function newInstance()
+ {
+ return new Country();
+ }
+}
diff --git a/core/libraries/Hubzero/Html/Builder.php b/core/libraries/Hubzero/Html/Builder.php
new file mode 100644
index 00000000000..42856d79137
--- /dev/null
+++ b/core/libraries/Hubzero/Html/Builder.php
@@ -0,0 +1,184 @@
+find($method);
+
+ if (!class_exists($cls))
+ {
+ throw new InvalidArgumentException(sprintf('%s %s not found.', $cls, $func), 500);
+ }
+ }
+
+ $callable = array($cls, $func);
+
+ if (!is_callable($callable))
+ {
+ throw new InvalidArgumentException(sprintf('%s %s not found.', $cls, $func), 500);
+ }
+
+ $this->register($key, $callable);
+ }
+
+ $function = static::$registry[$key];
+
+ return call_user_func_array($function, $parameters);
+ }
+
+ /**
+ * Registers a function to be called with a specific key
+ *
+ * @param string $key The name of the key
+ * @param array $callable Function or method
+ * @return boolean True if the function is callable
+ */
+ public function register($key, $callable)
+ {
+ if (!$this->has($key) && is_callable($callable))
+ {
+ self::$registry[$key] = $callable;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Removes a key for a method from registry.
+ *
+ * @param string $key The name of the key
+ * @return boolean True if a set key is unset
+ */
+ public function forget($key)
+ {
+ if (isset(self::$registry[$key]))
+ {
+ unset(self::$registry[$key]);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Test if the key is registered.
+ *
+ * @param string $key The name of the key
+ * @return boolean True if the key is registered.
+ */
+ public function has($key)
+ {
+ return isset(self::$registry[$key]);
+ }
+
+ /**
+ * Search added paths for a callable class
+ *
+ * @param string $cls
+ * @return string Fully resolved class name
+ */
+ protected function find($cls)
+ {
+ if (!empty(self::$paths))
+ {
+ foreach (self::$paths as $path)
+ {
+ $inc = $path . DS . strtolower($cls) . '.php';
+
+ if (file_exists($inc))
+ {
+ $code = file_get_contents($inc);
+
+ $tokens = token_get_all($code);
+
+ for ($i = 2; $i < count($tokens); $i++)
+ {
+ if ($tokens[$i - 2][0] === T_CLASS
+ && $tokens[$i - 1][0] === T_WHITESPACE
+ && $tokens[$i][0] === T_STRING)
+ {
+ include_once $inc;
+
+ return $tokens[$i][1];
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Add a directory where Html should search for helpers. You may
+ * either pass a string or an array of directories.
+ *
+ * @param string $path A path to search.
+ * @return array An array with directory elements
+ */
+ public function addIncludePath($path = '')
+ {
+ // Force path to array
+ settype($path, 'array');
+
+ // Loop through the path directories
+ foreach ($path as $dir)
+ {
+ if (!empty($dir) && !in_array($dir, self::$paths))
+ {
+ array_unshift(self::$paths, \Hubzero\Filesystem\Util::normalizePath($dir));
+ }
+ }
+
+ return self::$paths;
+ }
+}
diff --git a/core/libraries/Hubzero/Html/Builder/Access.php b/core/libraries/Hubzero/Html/Builder/Access.php
new file mode 100644
index 00000000000..86f318f5992
--- /dev/null
+++ b/core/libraries/Hubzero/Html/Builder/Access.php
@@ -0,0 +1,319 @@
+getQuery()
+ ->select('a.id', 'value')
+ ->select('a.title', 'text')
+ ->from('#__viewlevels', 'a')
+ ->group('a.id')
+ ->group('a.title')
+ ->group('a.ordering')
+ ->order('a.ordering', 'asc')
+ ->order('title', 'asc');
+
+ // Get the options.
+ $db->setQuery($query->toString());
+ $options = $db->loadObjectList();
+
+ // Check for a database error.
+ if ($db->getErrorNum())
+ {
+ throw new Exception($db->getErrorMsg(), 500, E_WARNING);
+ return null;
+ }
+
+ // If params is an array, push these options to the array
+ if (is_array($params))
+ {
+ $options = array_merge($params, $options);
+ }
+ // If all levels is allowed, push it into the array.
+ elseif ($params)
+ {
+ array_unshift($options, Select::option('', Lang::txt('JOPTION_ACCESS_SHOW_ALL_LEVELS')));
+ }
+
+ return Select::genericlist(
+ $options,
+ $name,
+ array(
+ 'list.attr' => $attribs,
+ 'list.select' => $selected,
+ 'id' => $id
+ )
+ );
+ }
+
+ /**
+ * Displays a list of the available user groups.
+ *
+ * @param string $name The form field name.
+ * @param string $selected The name of the selected section.
+ * @param string $attribs Additional attributes to add to the select field.
+ * @param boolean $allowAll True to add "All Groups" option.
+ * @return string The required HTML for the SELECT tag.
+ */
+ public static function usergroup($name, $selected, $attribs = '', $allowAll = true)
+ {
+ $db = App::get('db');
+ $query = $db->getQuery()
+ ->select('a.id', 'value')
+ ->select('a.title', 'text')
+ ->select('COUNT(DISTINCT b.id)', 'level')
+ ->from('#__usergroups', 'a')
+ ->joinRaw('#__usergroups AS b', 'a.lft > b.lft AND a.rgt < b.rgt', 'left')
+ ->group('a.id')
+ ->group('a.title')
+ ->group('a.lft')
+ ->group('a.rgt')
+ ->order('a.lft', 'asc');
+ $db->setQuery($query->toString());
+ $options = $db->loadObjectList();
+
+ // Check for a database error.
+ if ($db->getErrorNum())
+ {
+ throw new Exception($db->getErrorMsg(), 500, E_WARNING);
+ return null;
+ }
+
+ for ($i = 0, $n = count($options); $i < $n; $i++)
+ {
+ $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->text;
+ }
+
+ // If all usergroups is allowed, push it into the array.
+ if ($allowAll)
+ {
+ array_unshift($options, Select::option('', Lang::txt('JOPTION_ACCESS_SHOW_ALL_GROUPS')));
+ }
+
+ return Select::genericlist($options, $name, array('list.attr' => $attribs, 'list.select' => $selected));
+ }
+
+ /**
+ * Returns a UL list of user groups with check boxes
+ *
+ * @param string $name The name of the checkbox controls array
+ * @param array $selected An array of the checked boxes
+ * @param boolean $checkSuperAdmin If false only super admins can add to super admin groups
+ * @return string
+ */
+ public static function usergroups($name, $selected, $checkSuperAdmin = false)
+ {
+ static $count;
+
+ $count++;
+
+ $isSuperAdmin = \User::authorise('core.admin');
+
+ $db = App::get('db');
+ $query = $db->getQuery()
+ ->select('a.*')
+ ->select('COUNT(DISTINCT b.id)', 'level')
+ ->from('#__usergroups', 'a')
+ ->joinRaw('#__usergroups AS b', 'a.lft > b.lft AND a.rgt < b.rgt', 'left')
+ ->group('a.id')
+ ->group('a.title')
+ ->group('a.lft')
+ ->group('a.rgt')
+ ->group('a.parent_id')
+ ->order('a.lft', 'asc');
+ $db->setQuery($query->toString());
+ $groups = $db->loadObjectList();
+
+ // Check for a database error.
+ if ($db->getErrorNum())
+ {
+ throw new Exception($db->getErrorMsg(), 500, E_WARNING);
+ return null;
+ }
+
+ $html = array();
+
+ $html[] = '
';
+
+ for ($i = 0, $n = count($groups); $i < $n; $i++)
+ {
+ $item = &$groups[$i];
+
+ // If checkSuperAdmin is true, only add item if the user is superadmin or the group is not super admin
+ if ((!$checkSuperAdmin) || $isSuperAdmin || (!\Hubzero\Access\Access::checkGroup($item->id, 'core.admin')))
+ {
+ // Setup the variable attributes.
+ $eid = $count . 'group_' . $item->id;
+ // Don't call in_array unless something is selected
+ $checked = '';
+ if ($selected)
+ {
+ $checked = in_array($item->id, $selected) ? ' checked="checked"' : '';
+ }
+ $rel = ($item->parent_id > 0) ? ' rel="' . $count . 'group_' . $item->parent_id . '"' : '';
+
+ // Build the HTML for the item.
+ $html[] = '