├── .gitattributes ├── .gitignore ├── .php_cs ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── generate-api ├── composer.json ├── phpunit.xml.dist ├── renovate.json ├── src └── DirectAdmin │ ├── Context │ ├── AdminContext.php │ ├── BaseContext.php │ ├── ResellerContext.php │ └── UserContext.php │ ├── DirectAdmin.php │ ├── DirectAdminException.php │ ├── Objects │ ├── BaseObject.php │ ├── Database.php │ ├── Database │ │ └── AccessHost.php │ ├── Domain.php │ ├── DomainObject.php │ ├── Domains │ │ └── Subdomain.php │ ├── Email │ │ ├── Forwarder.php │ │ ├── MailObject.php │ │ └── Mailbox.php │ └── Users │ │ ├── Admin.php │ │ ├── Reseller.php │ │ └── User.php │ └── Utility │ └── Conversion.php └── tests ├── DirectAdmin ├── AccountManagementTest.php ├── AdminTest.php ├── EnvironmentTest.php ├── ResellerTest.php └── UserTest.php ├── README.md └── phpunit-bootstrap.php /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto-detect text files, ensure they use LF. 2 | * text=auto eol=lf 3 | 4 | # These files are always considered text and should use LF. 5 | # See core.whitespace @ http://git-scm.com/docs/git-config for whitespace flags. 6 | *.php text eol=lf whitespace=blank-at-eol,blank-at-eof,space-before-tab,tab-in-indent,tabwidth=4 diff=php 7 | *.json text eol=lf whitespace=blank-at-eol,blank-at-eof,space-before-tab,tab-in-indent,tabwidth=4 8 | *.test text eol=lf whitespace=blank-at-eol,blank-at-eof,space-before-tab,tab-in-indent,tabwidth=4 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.php_cs.cache 2 | /build/ 3 | /composer.lock 4 | /composer.phar 5 | /local/ 6 | /phpunit.xml 7 | /vendor/ 8 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | files() 13 | ->name('*.php') 14 | ->in(__DIR__.'/src') 15 | ->in(__DIR__.'/tests') 16 | ; 17 | 18 | return PhpCsFixer\Config::create() 19 | ->setRiskyAllowed(true) 20 | ->setRules([ 21 | '@Symfony' => true, 22 | 'strict_param' => true, 23 | 'array_syntax' => ['syntax' => 'short'], 24 | 'concat_space' => ['spacing' => 'one'], 25 | 'header_comment' => ['header' => $header], 26 | 27 | 'blank_line_before_return' => false, 28 | 'phpdoc_align' => false, 29 | 'phpdoc_separation' => false, 30 | 'phpdoc_var_without_name' => false, 31 | ]) 32 | ->setFinder($finder) 33 | ; 34 | 35 | 36 | /* 37 | use Symfony\CS\AbstractFixer; 38 | use Symfony\CS\DocBlock\DocBlock; 39 | use Symfony\CS\Tokenizer\Tokens; 40 | 41 | Symfony\CS\Fixer\Contrib\HeaderCommentFixer::setHeader(<<level(Symfony\CS\FixerInterface::SYMFONY_LEVEL) 52 | ->fixers([ 53 | '-phpdoc_params', 54 | '-phpdoc_separation', 55 | '-phpdoc_var_without_name', 56 | '-return', 57 | 'concat_with_spaces', 58 | 'header_comment', 59 | 'newline_after_open_tag', 60 | 'short_array_syntax', 61 | 'strict_param', 62 | ]) 63 | ->finder( 64 | Symfony\CS\Finder::create() 65 | ->files() 66 | ->name('*.php') 67 | ->in(__DIR__.'/src') 68 | ->in(__DIR__.'/tests') 69 | ) 70 | ;*/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | matrix: 6 | include: 7 | - php: 5.6 8 | - php: 7.0 9 | - php: 7.1 10 | - php: 7.0 11 | env: COMPOSER_FLAGS="--prefer-lowest" 12 | 13 | before_script: 14 | - curl --version 15 | - composer self-update 16 | - composer update --no-interaction --no-progress $COMPOSER_FLAGS 17 | 18 | script: 19 | - ./vendor/bin/phpunit -v 20 | 21 | after_script: 22 | - php vendor/bin/coveralls -v 23 | 24 | after_success: 25 | - if [ $TRAVIS_PHP_VERSION = '7.1' ] && [ $TRAVIS_BRANCH = 'master' ] && [ $TRAVIS_PULL_REQUEST = 'false' ] && [ $COMPOSER_FLAGS = '']; then sh bin/generate-api; fi 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | ## Changed 7 | - Switch to PHP-CS-Fixer 2 8 | - Switch to new Coveralls packages 9 | 10 | ## [0.1.4] - 2016-12-09 11 | ### Added 12 | - Creating domain pointers and aliases 13 | - Suspending/unsuspending resellers and users (#19) 14 | 15 | ## [0.1.3] - 2016-10-18 16 | ### Added 17 | - Add access hosts to database management (#13) 18 | - Enabled SSL verification (#14) 19 | 20 | ## [0.1.2] - 2016-01-31 21 | ### Added 22 | - Catch-all email management 23 | - Database management 24 | 25 | ## [0.1.1] - 2015-11-10 26 | ### Added 27 | - Domain management 28 | - Subdomain management 29 | - User config modification 30 | 31 | ## 0.1.0 - 2015-11-06 32 | ### Added 33 | - Admin/reseller/user contexts and logins 34 | - Impersonation of other users ("log in as") 35 | - Creating and deleting all account types 36 | - Fetching admin, reseller and user lists 37 | - Fetching user info 38 | - Fetching domain lists 39 | - Retrieving domain stats and configuration 40 | - Retrieving, creating and deleting email forwarders 41 | - Retrieving, creating and deleting mailboxes 42 | - Resetting mailbox passwords 43 | 44 | [Unreleased]: https://github.com/omines/directadmin/compare/v0.1.4...master 45 | [0.1.4]: https://github.com/omines/directadmin/compare/v0.1.3...v0.1.4 46 | [0.1.3]: https://github.com/omines/directadmin/compare/v0.1.2...v0.1.3 47 | [0.1.2]: https://github.com/omines/directadmin/compare/v0.1.1...v0.1.2 48 | [0.1.1]: https://github.com/omines/directadmin/compare/v0.1.0...v0.1.1 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Omines Internetbureau B.V. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DirectAdmin API client 2 | 3 | [![Build Status](https://travis-ci.org/omines/directadmin.svg?branch=master)](https://travis-ci.org/omines/directadmin) 4 | [![Coverage Status](https://coveralls.io/repos/omines/directadmin/badge.svg?branch=master&service=github)](https://coveralls.io/github/omines/directadmin?branch=master) 5 | [![Scrutinizer](https://img.shields.io/scrutinizer/g/omines/directadmin.svg)](https://scrutinizer-ci.com/g/omines/directadmin/?branch=master) 6 | [![SensioLabs Insight](https://img.shields.io/sensiolabs/i/47a71204-f274-4416-9db1-9773d65845ca.svg)](https://insight.sensiolabs.com/projects/47a71204-f274-4416-9db1-9773d65845ca) 7 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/omines/directadmin/master/LICENSE) 8 | 9 | **This package is no longer supported by the original authors as we no longer use DirectAdmin. The library still worked fine when we left it, use at your own risk. If you want to take over just push your fork to Packagist, it's MIT all the way.** 10 | 11 | This is a PHP client library to manage DirectAdmin control panel servers. We simply decided to develop this as we needed 12 | automation of our own DirectAdmin servers, and the existing implementations were unsupported and incomplete. 13 | 14 | [API documentation for this project is automatically generated on each push](https://omines.github.io/directadmin/api/). 15 | 16 | ## Installation 17 | 18 | [![Packagist](https://img.shields.io/packagist/v/omines/directadmin.svg)](https://packagist.org/packages/omines/directadmin) 19 | [![Packagist](https://img.shields.io/packagist/vpre/omines/directadmin.svg)](https://packagist.org/packages/omines/directadmin#dev-master) 20 | 21 | The recommended way to install this library is through [Composer](http://getcomposer.org): 22 | ```bash 23 | composer require omines/directadmin 24 | ``` 25 | 26 | If you're not familiar with `composer` follow the installation instructions for 27 | [Linux/Unix/Mac](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx) or 28 | [Windows](https://getcomposer.org/doc/00-intro.md#installation-windows), and then read the 29 | [basic usage introduction](https://getcomposer.org/doc/01-basic-usage.md). 30 | 31 | ## Dependencies 32 | 33 | The library uses [Guzzle 6](https://github.com/guzzle/guzzle) as its HTTP communication layer. PHP versions supported 34 | are 5.6, 7.0, 7.1 and hhvm. 35 | 36 | ## Basic usage 37 | 38 | To set up the connection use one of the base functions: 39 | 40 | ```php 41 | use Omines\DirectAdmin\DirectAdmin; 42 | 43 | $adminContext = DirectAdmin::connectAdmin('http://hostname:2222', 'admin', 'pass'); 44 | $resellerContext = DirectAdmin::connectReseller('http://hostname:2222', 'reseller', 'pass'); 45 | $userContext = DirectAdmin::connectUser('http://hostname:2222', 'user', 'pass'); 46 | ``` 47 | 48 | These functions return an 49 | [`AdminContext`](https://omines.github.io/directadmin/api/class-Omines.DirectAdmin.Context.AdminContext.html), 50 | [`ResellerContext`](https://omines.github.io/directadmin/api/class-Omines.DirectAdmin.Context.ResellerContext.html), and 51 | [`UserContext`](https://omines.github.io/directadmin/api/class-Omines.DirectAdmin.Context.UserContext.html) 52 | respectively exposing the functionality available at the given level. All three extend eachother as DirectAdmin uses a 53 | strict is-a model. To act on behalf of a user you can use impersonation calls: 54 | 55 | ```php 56 | $resellerContext = $adminContext->impersonateReseller($resellerName); 57 | $userContext = $resellerContext->impersonateUser($userName); 58 | ``` 59 | Both are essentially the same but mapped to the correct return type. Impersonation is also done implicitly 60 | when managing a user's domains: 61 | 62 | ```php 63 | $domain = $adminContext->getUser('user')->getDomain('example.tld'); 64 | ``` 65 | This returns, if the domain exists, a [`Domain`](https://omines.github.io/directadmin/api/class-Omines.DirectAdmin.Objects.Domain.html) 66 | instance in the context of its owning user, allowing you to manage its email accounts et al transparently. 67 | 68 | ## Contributions 69 | 70 | As the DirectAdmin API keeps expanding pull requests are welcomed, as are requests for specific functionality. 71 | Pull requests should in general include proper unit tests for the implemented or corrected functions. 72 | 73 | For more information about unit testing see the `README.md` in the tests folder. 74 | 75 | ## Legal 76 | 77 | This software was developed for internal use at [Omines Full Service Internetbureau](https://www.omines.nl/) 78 | in Eindhoven, the Netherlands. It is shared with the general public under the permissive MIT license, without 79 | any guarantee of fitness for any particular purpose. Refer to the included `LICENSE` file for more details. 80 | 81 | The project is not in any way affiliated with JBMC Software or its employees. 82 | -------------------------------------------------------------------------------- /bin/generate-api: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | cd $(dirname $0)/.. 4 | 5 | # Create required folders 6 | mkdir -p _docs 7 | cd _docs 8 | git clone https://${GH_TOKEN}@github.com/omines/directadmin.git . > /dev/null 9 | git checkout gh-pages 10 | rm -rf api 11 | 12 | # Generate Api 13 | wget http://www.apigen.org/apigen.phar 14 | php apigen.phar generate -s ../src -d api --template-theme "bootstrap" 15 | rm apigen.phar 16 | 17 | # Set identity 18 | git config --global user.email "travis@travis-ci.org" 19 | git config --global user.name "Travis" 20 | git config --global push.default "simple" 21 | 22 | # Push generated files 23 | git add --all . 24 | git commit -m "API documentation auto-updated" 25 | git push origin -fq > /dev/null 26 | 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omines/directadmin", 3 | "type": "library", 4 | "description": "PHP client library to manage DirectAdmin control panel servers.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "library", "php", "directadmin", "control panel", "api", "direct admin" 8 | ], 9 | "homepage": "https://github.com/omines/directadmin", 10 | "authors": [ 11 | { 12 | "name": "Niels Keurentjes", 13 | "email": "niels.keurentjes@omines.com", 14 | "homepage": "https://www.omines.nl/" 15 | } 16 | ], 17 | "support": { 18 | "issues": "https://github.com/omines/directadmin/issues" 19 | }, 20 | "require": { 21 | "php": ">=5.6.0", 22 | "guzzlehttp/guzzle": "^6.1|^7.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^5.7 || ^7.3", 26 | "friendsofphp/php-cs-fixer": "^2.0", 27 | "php-coveralls/php-coveralls": "^2.1" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Omines\\DirectAdmin\\": "src/DirectAdmin/" 32 | } 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "0.1.x-dev" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ./tests/DirectAdmin/ 25 | 26 | 27 | 28 | 29 | 30 | ./src/ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/DirectAdmin/Context/AdminContext.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class AdminContext extends ResellerContext 24 | { 25 | /** 26 | * Creates a new Admin level account. 27 | * 28 | * @param string $username 29 | * @param string $password 30 | * @param string $email 31 | * @return Admin The newly created Admin 32 | */ 33 | public function createAdmin($username, $password, $email) 34 | { 35 | return $this->createAccount($username, $password, $email, [], 'ACCOUNT_ADMIN', Admin::class); 36 | } 37 | 38 | /** 39 | * Creates a new Reseller level account. 40 | * 41 | * @param string $username 42 | * @param string $password 43 | * @param string $email 44 | * @param string $domain 45 | * @param string|array $package Either a package name or an array of options for custom 46 | * @param string $ip shared, sharedreseller or assign. Defaults to 'shared' 47 | * @return Reseller 48 | * @url http://www.directadmin.com/api.html#create for options to use. 49 | */ 50 | public function createReseller($username, $password, $email, $domain, $package = [], $ip = 'shared') 51 | { 52 | $options = array_merge( 53 | ['ip' => $ip, 'domain' => $domain, 'serverip' => 'ON', 'dns' => 'OFF'], 54 | is_array($package) ? $package : ['package' => $package] 55 | ); 56 | return $this->createAccount($username, $password, $email, $options, 'ACCOUNT_RESELLER', Reseller::class); 57 | } 58 | 59 | /** 60 | * Returns a list of known admins on the server. 61 | * 62 | * @return Admin[] 63 | */ 64 | public function getAdmins() 65 | { 66 | return BaseObject::toObjectArray($this->invokeApiGet('SHOW_ADMINS'), Admin::class, $this); 67 | } 68 | 69 | /** 70 | * Returns a full list of all accounts of any type on the server. 71 | * 72 | * @return User[] 73 | */ 74 | public function getAllAccounts() 75 | { 76 | $accounts = array_merge($this->getAllUsers(), $this->getResellers(), $this->getAdmins()); 77 | ksort($accounts); 78 | return $accounts; 79 | } 80 | 81 | /** 82 | * Returns a full list of all users on the server, so no resellers or admins. 83 | * 84 | * @return User[] 85 | */ 86 | public function getAllUsers() 87 | { 88 | return BaseObject::toObjectArray($this->invokeApiGet('SHOW_ALL_USERS'), User::class, $this); 89 | } 90 | 91 | /** 92 | * Returns a specific reseller by name, or NULL if there is no reseller by this name. 93 | * 94 | * @param string $username 95 | * @return null|Reseller 96 | */ 97 | public function getReseller($username) 98 | { 99 | $resellers = $this->getResellers(); 100 | return isset($resellers[$username]) ? $resellers[$username] : null; 101 | } 102 | 103 | /** 104 | * Returns the list of known resellers. 105 | * 106 | * @return Reseller[] 107 | */ 108 | public function getResellers() 109 | { 110 | return BaseObject::toObjectArray($this->invokeApiGet('SHOW_RESELLERS'), Reseller::class, $this); 111 | } 112 | 113 | /** 114 | * Returns a new AdminContext acting as the specified admin. 115 | * 116 | * @param string $username 117 | * @param bool $validate Whether to check the admin exists and is an admin 118 | * @return AdminContext 119 | */ 120 | public function impersonateAdmin($username, $validate = false) 121 | { 122 | return new self($this->getConnection()->loginAs($username), $validate); 123 | } 124 | 125 | /** 126 | * Returns a new ResellerContext acting as the specified reseller. 127 | * 128 | * @param string $username 129 | * @param bool $validate Whether to check the reseller exists and is a reseller 130 | * @return ResellerContext 131 | */ 132 | public function impersonateReseller($username, $validate = false) 133 | { 134 | return new ResellerContext($this->getConnection()->loginAs($username), $validate); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/DirectAdmin/Context/BaseContext.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | abstract class BaseContext 21 | { 22 | /** @var DirectAdmin */ 23 | private $connection; 24 | 25 | /** 26 | * Constructs the object. 27 | * 28 | * @param DirectAdmin $connection A prepared connection 29 | */ 30 | public function __construct(DirectAdmin $connection) 31 | { 32 | $this->connection = $connection; 33 | } 34 | 35 | /** 36 | * Returns the internal connection wrapper. 37 | * 38 | * @return DirectAdmin 39 | */ 40 | protected function getConnection() 41 | { 42 | return $this->connection; 43 | } 44 | 45 | /** 46 | * Invokes the DirectAdmin API via HTTP GET. 47 | * 48 | * @param string $command DirectAdmin API command to invoke 49 | * @param array $query Optional query parameters 50 | * @return array The parsed and validated response 51 | */ 52 | public function invokeApiGet($command, $query = []) 53 | { 54 | return $this->connection->invokeApi('GET', $command, ['query' => $query]); 55 | } 56 | 57 | /** 58 | * Invokes the DirectAdmin API via HTTP POST. 59 | * 60 | * @param string $command DirectAdmin API command to invoke 61 | * @param array $postParameters Optional form parameters 62 | * @return array The parsed and validated response 63 | */ 64 | public function invokeApiPost($command, $postParameters = []) 65 | { 66 | return $this->connection->invokeApi('POST', $command, ['form_params' => $postParameters]); 67 | } 68 | 69 | /** 70 | * @param $method 71 | * @param $uri 72 | * @param $options 73 | */ 74 | public function rawRequest($method, $uri, $options) 75 | { 76 | return $this->connection->rawRequest($method, $uri, ['form_params' => $options]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/DirectAdmin/Context/ResellerContext.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ResellerContext extends UserContext 22 | { 23 | /** 24 | * Creates a new user on the server. 25 | * 26 | * @param string $username Login for the new user 27 | * @param string $password Password for the new user 28 | * @param string $email Email for the new user 29 | * @param string $domain Default domain for the new user 30 | * @param string $ip IP for the user 31 | * @param string|array $package Either a package name or an array of options for custom 32 | * @return User Newly created user 33 | * @url http://www.directadmin.com/api.html#create for options to use. 34 | */ 35 | public function createUser($username, $password, $email, $domain, $ip, $package = []) 36 | { 37 | $options = array_merge( 38 | ['ip' => $ip, 'domain' => $domain], 39 | is_array($package) ? $package : ['package' => $package] 40 | ); 41 | return $this->createAccount($username, $password, $email, $options, 'ACCOUNT_USER', User::class); 42 | } 43 | 44 | /** 45 | * Internal helper function for creating new accounts. 46 | * 47 | * @param string $username Login for the new user 48 | * @param string $password Password for the new user 49 | * @param string $email Email for the new user 50 | * @param array $options List of DA account options to apply 51 | * @param string $endpoint API endpoint to invoke 52 | * @param string $returnType Class name that should wrap the resulting account 53 | * @return object An instance of the type specified in $returnType 54 | */ 55 | protected function createAccount($username, $password, $email, $options, $endpoint, $returnType) 56 | { 57 | $this->invokeApiPost($endpoint, array_merge($options, [ 58 | 'action' => 'create', 59 | 'add' => 'Submit', 60 | 'email' => $email, 61 | 'passwd' => $password, 62 | 'passwd2' => $password, 63 | 'username' => $username, 64 | ])); 65 | return new $returnType($username, $this); 66 | } 67 | 68 | /** 69 | * Deletes a single account. 70 | * 71 | * @param string $username Account to delete 72 | */ 73 | public function deleteAccount($username) 74 | { 75 | $this->deleteAccounts([$username]); 76 | } 77 | 78 | /** 79 | * Deletes multiple accounts. 80 | * 81 | * @param string[] $usernames Accounts to delete 82 | */ 83 | public function deleteAccounts(array $usernames) 84 | { 85 | $options = ['confirmed' => 'Confirm', 'delete' => 'yes']; 86 | foreach (array_values($usernames) as $idx => $username) { 87 | $options["select{$idx}"] = $username; 88 | } 89 | $this->invokeApiPost('SELECT_USERS', $options); 90 | } 91 | 92 | /** 93 | * Suspends a single account. 94 | * 95 | * @param string $username Account to delete 96 | */ 97 | public function suspendAccount($username) 98 | { 99 | $this->suspendAccounts([$username]); 100 | } 101 | 102 | /** 103 | * Unsuspends a single account. 104 | * 105 | * @param string $username Account to delete 106 | */ 107 | public function unsuspendAccount($username) 108 | { 109 | $this->suspendAccounts([$username], false); 110 | } 111 | 112 | /** 113 | * Suspends (or unsuspends) multiple accounts. 114 | * 115 | * @param string[] $usernames Accounts to delete 116 | * @param bool $suspend (true - suspend, false - unsuspend) 117 | */ 118 | public function suspendAccounts(array $usernames, $suspend = true) 119 | { 120 | $options = ['suspend' => $suspend ? 'Suspend' : 'Unsuspend']; 121 | foreach (array_values($usernames) as $idx => $username) { 122 | $options['select' . $idx] = $username; 123 | } 124 | $this->invokeApiPost('SELECT_USERS', $options); 125 | } 126 | 127 | /** 128 | * Unsuspends multiple accounts. 129 | * 130 | * @param string[] $usernames Accounts to delete 131 | */ 132 | public function unsuspendAccounts(array $usernames) 133 | { 134 | $this->suspendAccounts($usernames, false); 135 | } 136 | 137 | /** 138 | * Returns all IPs available to this reseller. 139 | * 140 | * @return array List of IPs as strings 141 | */ 142 | public function getIPs() 143 | { 144 | return $this->invokeApiGet('SHOW_RESELLER_IPS'); 145 | } 146 | 147 | /** 148 | * Returns a single user by name. 149 | * 150 | * @param string $username 151 | * @return User|null 152 | */ 153 | public function getUser($username) 154 | { 155 | $resellers = $this->getUsers(); 156 | return isset($resellers[$username]) ? $resellers[$username] : null; 157 | } 158 | 159 | /** 160 | * Returns all users for this reseller. 161 | * 162 | * @return User[] Associative array of users 163 | */ 164 | public function getUsers() 165 | { 166 | return BaseObject::toObjectArray($this->invokeApiGet('SHOW_USERS'), User::class, $this); 167 | } 168 | 169 | /** 170 | * Impersonates a user, allowing the reseller/admin to act on their behalf. 171 | * 172 | * @param string $username Login of the account to impersonate 173 | * @param bool $validate Whether to check the user exists and is a user 174 | * @return UserContext 175 | */ 176 | public function impersonateUser($username, $validate = false) 177 | { 178 | return new UserContext($this->getConnection()->loginAs($username), $validate); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/DirectAdmin/Context/UserContext.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class UserContext extends BaseContext 24 | { 25 | /** @var User */ 26 | private $user; 27 | 28 | /** 29 | * Constructs the object. 30 | * 31 | * @param DirectAdmin $connection A prepared connection 32 | * @param bool $validate Whether to check if the connection matches the context 33 | */ 34 | public function __construct(DirectAdmin $connection, $validate = false) 35 | { 36 | parent::__construct($connection); 37 | if ($validate) { 38 | $classMap = [ 39 | DirectAdmin::ACCOUNT_TYPE_ADMIN => AdminContext::class, 40 | DirectAdmin::ACCOUNT_TYPE_RESELLER => ResellerContext::class, 41 | DirectAdmin::ACCOUNT_TYPE_USER => self::class, 42 | ]; 43 | if ($classMap[$this->getType()] != get_class($this)) { 44 | /* @codeCoverageIgnoreStart */ 45 | throw new DirectAdminException('Validation mismatch on context construction'); 46 | /* @codeCoverageIgnoreEnd */ 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * Returns the type of the account (user/reseller/admin). 53 | * 54 | * @return string One of the DirectAdmin::ACCOUNT_TYPE_ constants describing the type of underlying account 55 | */ 56 | public function getType() 57 | { 58 | return $this->getContextUser()->getType(); 59 | } 60 | 61 | /** 62 | * Returns the actual user object behind the context. 63 | * 64 | * @return User The user object behind the context 65 | */ 66 | public function getContextUser() 67 | { 68 | if (!isset($this->user)) { 69 | $this->user = User::fromConfig($this->invokeApiGet('SHOW_USER_CONFIG'), $this); 70 | } 71 | return $this->user; 72 | } 73 | 74 | /** 75 | * Returns a domain managed by the current user. 76 | * 77 | * @param string $domainName The requested domain name 78 | * @return null|Domain The domain if found, or NULL if it does not exist 79 | */ 80 | public function getDomain($domainName) 81 | { 82 | return $this->getContextUser()->getDomain($domainName); 83 | } 84 | 85 | /** 86 | * Returns a full list of the domains managed by the current user. 87 | * 88 | * @return Domain[] 89 | */ 90 | public function getDomains() 91 | { 92 | return $this->getContextUser()->getDomains(); 93 | } 94 | 95 | /** 96 | * Returns the username of the current context. 97 | * 98 | * @return string Username for the current context 99 | */ 100 | public function getUsername() 101 | { 102 | return $this->getConnection()->getUsername(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/DirectAdmin/DirectAdmin.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class DirectAdmin 26 | { 27 | const ACCOUNT_TYPE_ADMIN = 'admin'; 28 | const ACCOUNT_TYPE_RESELLER = 'reseller'; 29 | const ACCOUNT_TYPE_USER = 'user'; 30 | 31 | /** @var string */ 32 | private $authenticatedUser; 33 | 34 | /** @var string */ 35 | private $username; 36 | 37 | /** @var string */ 38 | private $password; 39 | 40 | /** @var string */ 41 | private $baseUrl; 42 | 43 | /** @var Client */ 44 | private $connection; 45 | 46 | /** 47 | * Connects to DirectAdmin with an admin account. 48 | * 49 | * @param string $url The base URL of the DirectAdmin server 50 | * @param string $username The username of the account 51 | * @param string $password The password of the account 52 | * @param bool $validate Whether to ensure the account exists and is of the correct type 53 | * @return AdminContext 54 | */ 55 | public static function connectAdmin($url, $username, $password, $validate = false) 56 | { 57 | return new AdminContext(new self($url, $username, $password), $validate); 58 | } 59 | 60 | /** 61 | * Connects to DirectAdmin with a reseller account. 62 | * 63 | * @param string $url The base URL of the DirectAdmin server 64 | * @param string $username The username of the account 65 | * @param string $password The password of the account 66 | * @param bool $validate Whether to ensure the account exists and is of the correct type 67 | * @return ResellerContext 68 | */ 69 | public static function connectReseller($url, $username, $password, $validate = false) 70 | { 71 | return new ResellerContext(new self($url, $username, $password), $validate); 72 | } 73 | 74 | /** 75 | * Connects to DirectAdmin with a user account. 76 | * 77 | * @param string $url The base URL of the DirectAdmin server 78 | * @param string $username The username of the account 79 | * @param string $password The password of the account 80 | * @param bool $validate Whether to ensure the account exists and is of the correct type 81 | * @return UserContext 82 | */ 83 | public static function connectUser($url, $username, $password, $validate = false) 84 | { 85 | return new UserContext(new self($url, $username, $password), $validate); 86 | } 87 | 88 | /** 89 | * Creates a connection wrapper to DirectAdmin as the specified account. 90 | * 91 | * @param string $url The base URL of the DirectAdmin server 92 | * @param string $username The username of the account 93 | * @param string $password The password of the account 94 | */ 95 | protected function __construct($url, $username, $password) 96 | { 97 | $accounts = explode('|', $username); 98 | $this->authenticatedUser = current($accounts); 99 | $this->username = end($accounts); 100 | $this->password = $password; 101 | $this->baseUrl = rtrim($url, '/') . '/'; 102 | $this->connection = new Client([ 103 | 'base_uri' => $this->baseUrl, 104 | 'auth' => [$username, $password], 105 | ]); 106 | } 107 | 108 | /** 109 | * Returns the username behind the current connection. 110 | * 111 | * @return string Currently logged in user's username 112 | */ 113 | public function getUsername() 114 | { 115 | return $this->username; 116 | } 117 | 118 | /** 119 | * Invokes the DirectAdmin API with specific options. 120 | * 121 | * @param string $method HTTP method to use (ie. GET or POST) 122 | * @param string $command DirectAdmin API command to invoke 123 | * @param array $options Guzzle options to use for the call 124 | * @return array The unvalidated response 125 | * @throws DirectAdminException If anything went wrong on the network level 126 | */ 127 | public function invokeApi($method, $command, $options = []) 128 | { 129 | $result = $this->rawRequest($method, '/CMD_API_' . $command, $options); 130 | if (!empty($result['error'])) { 131 | throw new DirectAdminException("$method to $command failed: $result[details] ($result[text])"); 132 | } 133 | return Conversion::sanitizeArray($result); 134 | } 135 | 136 | /** 137 | * Returns a clone of the connection logged in as a managed user or reseller. 138 | * 139 | * @param string $username 140 | * @return DirectAdmin 141 | */ 142 | public function loginAs($username) 143 | { 144 | // DirectAdmin format is to just pipe the accounts together under the master password 145 | return new self($this->baseUrl, $this->authenticatedUser . "|{$username}", $this->password); 146 | } 147 | 148 | /** 149 | * Sends a raw request to DirectAdmin. 150 | * 151 | * @param string $method 152 | * @param string $uri 153 | * @param array $options 154 | * @return array 155 | */ 156 | public function rawRequest($method, $uri, $options) 157 | { 158 | try { 159 | $response = $this->connection->request($method, $uri, $options); 160 | if ($response->getHeader('Content-Type')[0] == 'text/html') { 161 | throw new DirectAdminException(sprintf('DirectAdmin API returned text/html to %s %s containing "%s"', $method, $uri, strip_tags($response->getBody()->getContents()))); 162 | } 163 | $body = $response->getBody()->getContents(); 164 | return Conversion::responseToArray($body); 165 | } catch (TransferException $exception) { 166 | // Rethrow anything that causes a network issue 167 | throw new DirectAdminException(sprintf('%s request to %s failed', $method, $uri), 0, $exception); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/DirectAdmin/DirectAdminException.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class DirectAdminException extends \RuntimeException 19 | { 20 | /** 21 | * Construct the exception object. 22 | * 23 | * @param string $message The Exception message to throw 24 | * @param int $code The Exception code 25 | * @param \Exception|null $previous The previous exception used for the exception chaining. Since 5.3.0 26 | */ 27 | public function __construct($message = '', $code = 0, \Exception $previous = null) 28 | { 29 | parent::__construct($message, $code, $previous); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/BaseObject.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | abstract class BaseObject 22 | { 23 | /** @var string */ 24 | private $name; 25 | 26 | /** @var UserContext */ 27 | private $context; 28 | 29 | /** @var array */ 30 | private $cache = []; 31 | 32 | /** 33 | * @param string $name Canonical name for the object 34 | * @param UserContext $context Context within which the object is valid 35 | */ 36 | protected function __construct($name, UserContext $context) 37 | { 38 | $this->name = $name; 39 | $this->context = $context; 40 | } 41 | 42 | /** 43 | * Clear the object's internal cache. 44 | */ 45 | public function clearCache() 46 | { 47 | $this->cache = []; 48 | } 49 | 50 | /** 51 | * Retrieves an item from the internal cache. 52 | * 53 | * @param string $key Key to retrieve 54 | * @param callable|mixed $default Either a callback or an explicit default value 55 | * @return mixed Cached value 56 | */ 57 | protected function getCache($key, $default) 58 | { 59 | if (!isset($this->cache[$key])) { 60 | $this->cache[$key] = is_callable($default) ? $default() : $default; 61 | } 62 | return $this->cache[$key]; 63 | } 64 | 65 | /** 66 | * Retrieves a keyed item from inside a cache item. 67 | * 68 | * @param string $key 69 | * @param string $item 70 | * @param callable|mixed $defaultKey 71 | * @param mixed|null $defaultItem 72 | * @return mixed Cached value 73 | * 74 | * @codeCoverageIgnore 75 | */ 76 | protected function getCacheItem($key, $item, $defaultKey, $defaultItem = null) 77 | { 78 | if (empty($cache = $this->getCache($key, $defaultKey))) { 79 | return $defaultItem; 80 | } 81 | if (!is_array($cache)) { 82 | throw new DirectAdminException("Cache item $key is not an array"); 83 | } 84 | return isset($cache[$item]) ? $cache[$item] : $defaultItem; 85 | } 86 | 87 | /** 88 | * Sets a specific cache item, for when a cacheable value was a by-product. 89 | * 90 | * @param string $key 91 | * @param mixed $value 92 | */ 93 | protected function setCache($key, $value) 94 | { 95 | $this->cache[$key] = $value; 96 | } 97 | 98 | /** 99 | * @return UserContext 100 | */ 101 | public function getContext() 102 | { 103 | return $this->context; 104 | } 105 | 106 | /** 107 | * Protected as a derived class may want to offer the name under a different name. 108 | * 109 | * @return string 110 | */ 111 | protected function getName() 112 | { 113 | return $this->name; 114 | } 115 | 116 | /** 117 | * Converts an array of string items to an associative array of objects of the specified type. 118 | * 119 | * @param array $items 120 | * @param string $class 121 | * @param UserContext $context 122 | * @return array 123 | */ 124 | public static function toObjectArray(array $items, $class, UserContext $context) 125 | { 126 | return array_combine($items, array_map(function ($item) use ($class, $context) { 127 | return new $class($item, $context); 128 | }, $items)); 129 | } 130 | 131 | /** 132 | * Converts an associative array of descriptors to objects of the specified type. 133 | * 134 | * @param array $items 135 | * @param string $class 136 | * @param UserContext $context 137 | * @return array 138 | */ 139 | public static function toRichObjectArray(array $items, $class, UserContext $context) 140 | { 141 | array_walk($items, function (&$value, $name) use ($class, $context) { 142 | $value = new $class($name, $context, $value); 143 | }); 144 | return $items; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Database.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Database extends BaseObject 22 | { 23 | const CACHE_ACCESS_HOSTS = 'access_hosts'; 24 | 25 | /** @var User */ 26 | private $owner; 27 | 28 | /** @var string */ 29 | private $databaseName; 30 | 31 | /** 32 | * Database constructor. 33 | * 34 | * @param string $name Name of the database 35 | * @param User $owner Database owner 36 | * @param UserContext $context Context within which the object is valid 37 | */ 38 | public function __construct($name, User $owner, UserContext $context) 39 | { 40 | parent::__construct($name, $context); 41 | $this->owner = $owner; 42 | $this->databaseName = $this->owner->getUsername() . '_' . $this->getName(); 43 | } 44 | 45 | /** 46 | * Creates a new database under the specified user. 47 | * 48 | * @param User $user Owner of the database 49 | * @param string $name Database name, without _ prefix 50 | * @param string $username Username to access the database with, without _ prefix 51 | * @param string|null $password Password, or null if database user already exists 52 | * @return Database Newly created database 53 | */ 54 | public static function create(User $user, $name, $username, $password) 55 | { 56 | $options = [ 57 | 'action' => 'create', 58 | 'name' => $name, 59 | ]; 60 | if (!empty($password)) { 61 | $options += ['user' => $username, 'passwd' => $password, 'passwd2' => $password]; 62 | } else { 63 | $options += ['userlist' => $username]; 64 | } 65 | $user->getContext()->invokeApiPost('DATABASES', $options); 66 | return new self($name, $user, $user->getContext()); 67 | } 68 | 69 | /** 70 | * Deletes this database from the user. 71 | */ 72 | public function delete() 73 | { 74 | $this->getContext()->invokeApiPost('DATABASES', [ 75 | 'action' => 'delete', 76 | 'select0' => $this->getDatabaseName(), 77 | ]); 78 | $this->getContext()->getContextUser()->clearCache(); 79 | } 80 | 81 | /** 82 | * @return Database\AccessHost[] 83 | */ 84 | public function getAccessHosts() 85 | { 86 | return $this->getCache(self::CACHE_ACCESS_HOSTS, function () { 87 | $accessHosts = $this->getContext()->invokeApiGet('DATABASES', [ 88 | 'action' => 'accesshosts', 89 | 'db' => $this->getDatabaseName(), 90 | ]); 91 | 92 | return array_map(function ($name) { 93 | return new Database\AccessHost($name, $this); 94 | }, $accessHosts); 95 | }); 96 | } 97 | 98 | /** 99 | * @param string $name 100 | * @return Database\AccessHost 101 | */ 102 | public function createAccessHost($name) 103 | { 104 | $accessHost = Database\AccessHost::create($this, $name); 105 | $this->getContext()->getContextUser()->clearCache(); 106 | return $accessHost; 107 | } 108 | 109 | /** 110 | * @return string Name of the database 111 | */ 112 | public function getDatabaseName() 113 | { 114 | return $this->databaseName; 115 | } 116 | 117 | /** 118 | * @return User 119 | */ 120 | public function getOwner() 121 | { 122 | return $this->owner; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Database/AccessHost.php: -------------------------------------------------------------------------------- 1 | getContext()); 31 | $this->database = $database; 32 | } 33 | 34 | /** 35 | * @param Database $database 36 | * @param string $host 37 | * @return AccessHost 38 | */ 39 | public static function create(Database $database, $host) 40 | { 41 | $database->getContext()->invokeApiPost('DATABASES', [ 42 | 'action' => 'accesshosts', 43 | 'create' => 'yes', 44 | 'db' => $database->getDatabaseName(), 45 | 'host' => $host, 46 | ]); 47 | return new self($host, $database); 48 | } 49 | 50 | /** 51 | * Deletes the access host. 52 | */ 53 | public function delete() 54 | { 55 | $this->getContext()->invokeApiPost('DATABASES', [ 56 | 'action' => 'accesshosts', 57 | 'delete' => 'yes', 58 | 'db' => $this->database->getDatabaseName(), 59 | 'select0' => $this->getName(), 60 | ]); 61 | $this->database->clearCache(); 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getHost() 68 | { 69 | return $this->getName(); 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | public function __toString() 76 | { 77 | return $this->getHost(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Domain.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class Domain extends BaseObject 27 | { 28 | const CACHE_FORWARDERS = 'forwarders'; 29 | const CACHE_MAILBOXES = 'mailboxes'; 30 | const CACHE_SUBDOMAINS = 'subdomains'; 31 | 32 | const CATCHALL_BLACKHOLE = ':blackhole:'; 33 | const CATCHALL_FAIL = ':fail:'; 34 | 35 | /** @var string */ 36 | private $domainName; 37 | 38 | /** @var User */ 39 | private $owner; 40 | 41 | /** @var string[] */ 42 | private $aliases; 43 | 44 | /** @var string[] */ 45 | private $pointers; 46 | 47 | /** @var float */ 48 | private $bandwidthUsed; 49 | 50 | /** @var float|null */ 51 | private $bandwidthLimit; 52 | 53 | /** @var float */ 54 | private $diskUsage; 55 | 56 | /** 57 | * Construct the object. 58 | * 59 | * @param string $name The domain name 60 | * @param UserContext $context The owning user context 61 | * @param string|array $config The basic config string as returned by CMD_API_ADDITIONAL_DOMAINS 62 | */ 63 | public function __construct($name, UserContext $context, $config) 64 | { 65 | parent::__construct($name, $context); 66 | $this->setConfig($context, is_array($config) ? $config : \GuzzleHttp\Psr7\parse_query($config)); 67 | } 68 | 69 | /** 70 | * Creates a new domain under the specified user. 71 | * 72 | * @param User $user Owner of the domain 73 | * @param string $domainName Domain name to create 74 | * @param float|null $bandwidthLimit Bandwidth limit in MB, or NULL to share with account 75 | * @param float|null $diskLimit Disk limit in MB, or NULL to share with account 76 | * @param bool|null $ssl Whether SSL is to be enabled, or NULL to fallback to account default 77 | * @param bool|null $php Whether PHP is to be enabled, or NULL to fallback to account default 78 | * @param bool|null $cgi Whether CGI is to be enabled, or NULL to fallback to account default 79 | * @return Domain The newly created domain 80 | */ 81 | public static function create(User $user, $domainName, $bandwidthLimit = null, $diskLimit = null, $ssl = null, $php = null, $cgi = null) 82 | { 83 | $options = [ 84 | 'action' => 'create', 85 | 'domain' => $domainName, 86 | (isset($bandwidthLimit) ? 'bandwidth' : 'ubandwidth') => $bandwidthLimit, 87 | (isset($diskLimit) ? 'quota' : 'uquota') => $diskLimit, 88 | 'ssl' => Conversion::onOff($ssl, $user->hasSSL()), 89 | 'php' => Conversion::onOff($php, $user->hasPHP()), 90 | 'cgi' => Conversion::onOff($cgi, $user->hasCGI()), 91 | ]; 92 | $user->getContext()->invokeApiPost('DOMAIN', $options); 93 | $config = $user->getContext()->invokeApiGet('ADDITIONAL_DOMAINS'); 94 | return new self($domainName, $user->getContext(), $config[$domainName]); 95 | } 96 | 97 | /** 98 | * Creates a new email forwarder. 99 | * 100 | * @param string $prefix Part of the email address before the @ 101 | * @param string|string[] $recipients One or more recipients 102 | * @return Forwarder The newly created forwarder 103 | */ 104 | public function createForwarder($prefix, $recipients) 105 | { 106 | return Forwarder::create($this, $prefix, $recipients); 107 | } 108 | 109 | /** 110 | * Creates a new mailbox. 111 | * 112 | * @param string $prefix Prefix for the account 113 | * @param string $password Password for the account 114 | * @param int|null $quota Quota in megabytes, or zero/null for unlimited 115 | * @param int|null $sendLimit Send limit, or 0 for unlimited, or null for system default 116 | * @return Mailbox The newly created mailbox 117 | */ 118 | public function createMailbox($prefix, $password, $quota = null, $sendLimit = null) 119 | { 120 | return Mailbox::create($this, $prefix, $password, $quota, $sendLimit); 121 | } 122 | 123 | /** 124 | * Creates a pointer or alias. 125 | * 126 | * @param string $domain 127 | * @param bool $alias 128 | */ 129 | public function createPointer($domain, $alias = false) 130 | { 131 | $parameters = [ 132 | 'domain' => $this->domainName, 133 | 'from' => $domain, 134 | 'action' => 'add', 135 | ]; 136 | if ($alias) { 137 | $parameters['alias'] = 'yes'; 138 | $list = &$this->aliases; 139 | } else { 140 | $list = &$this->pointers; 141 | } 142 | $this->getContext()->invokeApiPost('DOMAIN_POINTER', $parameters); 143 | $list[] = $domain; 144 | $list = array_unique($list); 145 | } 146 | 147 | /** 148 | * Creates a new subdomain. 149 | * 150 | * @param string $prefix Prefix to add before the domain name 151 | * @return Subdomain The newly created subdomain 152 | */ 153 | public function createSubdomain($prefix) 154 | { 155 | return Subdomain::create($this, $prefix); 156 | } 157 | 158 | /** 159 | * Deletes this domain from the user. 160 | */ 161 | public function delete() 162 | { 163 | $this->getContext()->invokeApiPost('DOMAIN', [ 164 | 'delete' => true, 165 | 'confirmed' => true, 166 | 'select0' => $this->domainName, 167 | ]); 168 | $this->owner->clearCache(); 169 | } 170 | 171 | /** 172 | * @return string[] List of aliases for this domain 173 | */ 174 | public function getAliases() 175 | { 176 | return $this->aliases; 177 | } 178 | 179 | /** 180 | * @return float Bandwidth used in megabytes 181 | */ 182 | public function getBandwidthUsed() 183 | { 184 | return $this->bandwidthUsed; 185 | } 186 | 187 | /** 188 | * @return float|null Bandwidth quotum in megabytes, or NULL for unlimited 189 | */ 190 | public function getBandwidthLimit() 191 | { 192 | return $this->bandwidthLimit; 193 | } 194 | 195 | /** 196 | * @return string|null Currently configured catch-all configuration 197 | */ 198 | public function getCatchall() 199 | { 200 | $value = $this->getContext()->invokeApiGet('EMAIL_CATCH_ALL', ['domain' => $this->domainName]); 201 | return isset($value['value']) ? $value['value'] : null; 202 | } 203 | 204 | /** 205 | * @return float Disk usage in megabytes 206 | */ 207 | public function getDiskUsage() 208 | { 209 | return $this->diskUsage; 210 | } 211 | 212 | /** 213 | * @return string The real domain name 214 | */ 215 | public function getDomainName() 216 | { 217 | return $this->domainName; 218 | } 219 | 220 | /** 221 | * Returns unified sorted list of main domain name, aliases and pointers. 222 | * 223 | * @return string[] 224 | */ 225 | public function getDomainNames() 226 | { 227 | return $this->getCache('domainNames', function () { 228 | $list = array_merge($this->aliases, $this->pointers, [$this->getDomainName()]); 229 | sort($list); 230 | return $list; 231 | }); 232 | } 233 | 234 | /** 235 | * @return Forwarder[] Associative array of forwarders 236 | */ 237 | public function getForwarders() 238 | { 239 | return $this->getCache(self::CACHE_FORWARDERS, function () { 240 | $forwarders = $this->getContext()->invokeApiGet('EMAIL_FORWARDERS', [ 241 | 'domain' => $this->getDomainName(), 242 | ]); 243 | return DomainObject::toDomainObjectArray($forwarders, Forwarder::class, $this); 244 | }); 245 | } 246 | 247 | /** 248 | * @return Mailbox[] Associative array of mailboxes 249 | */ 250 | public function getMailboxes() 251 | { 252 | return $this->getCache(self::CACHE_MAILBOXES, function () { 253 | $boxes = $this->getContext()->invokeApiGet('POP', [ 254 | 'domain' => $this->getDomainName(), 255 | 'action' => 'full_list', 256 | ]); 257 | return DomainObject::toDomainObjectArray($boxes, Mailbox::class, $this); 258 | }); 259 | } 260 | 261 | /** 262 | * @return User 263 | */ 264 | public function getOwner() 265 | { 266 | return $this->owner; 267 | } 268 | 269 | /** 270 | * @return string[] List of domain pointers for this domain 271 | */ 272 | public function getPointers() 273 | { 274 | return $this->pointers; 275 | } 276 | 277 | /** 278 | * @return Subdomain[] Associative array of subdomains 279 | */ 280 | public function getSubdomains() 281 | { 282 | return $this->getCache(self::CACHE_SUBDOMAINS, function () { 283 | $subs = $this->getContext()->invokeApiGet('SUBDOMAINS', ['domain' => $this->getDomainName()]); 284 | $subs = array_combine($subs, $subs); 285 | return DomainObject::toDomainObjectArray($subs, Subdomain::class, $this); 286 | }); 287 | } 288 | 289 | /** 290 | * Invokes a POST command on a domain object. 291 | * 292 | * @param string $command Command to invoke 293 | * @param string $action Action to execute 294 | * @param array $parameters Additional options for the command 295 | * @param bool $clearCache Whether to clear the domain cache on success 296 | * @return array Response from the API 297 | */ 298 | public function invokePost($command, $action, $parameters = [], $clearCache = true) 299 | { 300 | $response = $this->getContext()->invokeApiPost($command, array_merge([ 301 | 'action' => $action, 302 | 'domain' => $this->domainName, 303 | ], $parameters)); 304 | if ($clearCache) { 305 | $this->clearCache(); 306 | } 307 | return $response; 308 | } 309 | 310 | /** 311 | * @param string $newValue New address for the catch-all, or one of the CATCHALL_ constants 312 | */ 313 | public function setCatchall($newValue) 314 | { 315 | $parameters = array_merge(['domain' => $this->domainName, 'update' => 'Update'], 316 | (empty($newValue) || $newValue[0] == ':') ? ['catch' => $newValue] : ['catch' => 'address', 'value' => $newValue]); 317 | $this->getContext()->invokeApiPost('EMAIL_CATCH_ALL', $parameters); 318 | } 319 | 320 | /** 321 | * Allows Domain object to be passed as a string with its domain name. 322 | * 323 | * @return string 324 | */ 325 | public function __toString() 326 | { 327 | return $this->getDomainName(); 328 | } 329 | 330 | /** 331 | * Sets configuration options from raw DirectAdmin data. 332 | * 333 | * @param UserContext $context Owning user context 334 | * @param array $config An array of settings 335 | */ 336 | private function setConfig(UserContext $context, array $config) 337 | { 338 | $this->domainName = $config['domain']; 339 | 340 | // Determine owner 341 | if ($config['username'] === $context->getUsername()) { 342 | $this->owner = $context->getContextUser(); 343 | } else { 344 | throw new DirectAdminException('Could not determine relationship between context user and domain'); 345 | } 346 | 347 | // Parse plain options 348 | $bandwidths = array_map('trim', explode('/', $config['bandwidth'])); 349 | $this->bandwidthUsed = floatval($bandwidths[0]); 350 | $this->bandwidthLimit = !isset($bandwidths[1]) || ctype_alpha($bandwidths[1]) ? null : floatval($bandwidths[1]); 351 | $this->diskUsage = floatval($config['quota']); 352 | 353 | $this->aliases = array_filter(explode('|', $config['alias_pointers'])); 354 | $this->pointers = array_filter(explode('|', $config['pointers'])); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/DomainObject.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | abstract class DomainObject extends BaseObject 19 | { 20 | /** @var Domain */ 21 | private $domain; 22 | 23 | /** 24 | * @param string $name Canonical name for the object 25 | * @param Domain $domain Domain to which the object belongs 26 | */ 27 | protected function __construct($name, Domain $domain) 28 | { 29 | parent::__construct($name, $domain->getContext()); 30 | $this->domain = $domain; 31 | } 32 | 33 | /** 34 | * Invokes a POST command on a domain object. 35 | * 36 | * @param string $command Command to invoke 37 | * @param string $action Action to execute 38 | * @param array $parameters Additional options for the command 39 | * @param bool $clearCache Whether to clear the domain cache 40 | * @return array Response from the API 41 | */ 42 | protected function invokePost($command, $action, $parameters = [], $clearCache = true) 43 | { 44 | return $this->domain->invokePost($command, $action, $parameters, $clearCache); 45 | } 46 | 47 | /** 48 | * @return Domain 49 | */ 50 | public function getDomain() 51 | { 52 | return $this->domain; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getDomainName() 59 | { 60 | return $this->domain->getDomainName(); 61 | } 62 | 63 | /** 64 | * Converts an associative array of descriptors to objects of the specified type. 65 | * 66 | * @param array $items 67 | * @param string $class 68 | * @param Domain $domain 69 | * @return array 70 | */ 71 | public static function toDomainObjectArray(array $items, $class, Domain $domain) 72 | { 73 | array_walk($items, function (&$value, $name) use ($class, $domain) { 74 | $value = new $class($name, $domain, $value); 75 | }); 76 | return $items; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Domains/Subdomain.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Subdomain extends DomainObject 22 | { 23 | /** 24 | * Construct the object. 25 | * 26 | * @param string $prefix The domain name 27 | * @param Domain $domain The containing domain 28 | */ 29 | public function __construct($prefix, Domain $domain) 30 | { 31 | parent::__construct($prefix, $domain); 32 | } 33 | 34 | /** 35 | * Creates a new subdomain. 36 | * 37 | * @param Domain $domain Parent domain 38 | * @param string $prefix Prefix of the subdomain 39 | * @return Subdomain The newly created object 40 | */ 41 | public static function create(Domain $domain, $prefix) 42 | { 43 | $domain->invokePost('SUBDOMAIN', 'create', ['subdomain' => $prefix]); 44 | return new self($prefix, $domain); 45 | } 46 | 47 | /** 48 | * Deletes the subdomain. 49 | * 50 | * @param bool $deleteContents Whether to delete all directory contents as well 51 | */ 52 | public function delete($deleteContents = true) 53 | { 54 | $this->invokePost('SUBDOMAIN', 'delete', [ 55 | 'select0' => $this->getPrefix(), 56 | 'contents' => ($deleteContents ? 'yes' : 'no'), 57 | ]); 58 | } 59 | 60 | /** 61 | * Returns the full domain name for the subdomain. 62 | * 63 | * @return string 64 | */ 65 | public function getDomainName() 66 | { 67 | return $this->getPrefix() . '.' . parent::getDomainName(); 68 | } 69 | 70 | /** 71 | * Returns the full domain name for the subdomain. 72 | * 73 | * @return string 74 | */ 75 | public function getBaseDomainName() 76 | { 77 | return parent::getDomainName(); 78 | } 79 | 80 | /** 81 | * Returns the prefix of the subdomain. 82 | * 83 | * @return string 84 | */ 85 | public function getPrefix() 86 | { 87 | return $this->getName(); 88 | } 89 | 90 | /** 91 | * Allows the class to be used as a string representing the full domain name. 92 | * 93 | * @return string 94 | */ 95 | public function __toString() 96 | { 97 | return $this->getDomainName(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Email/Forwarder.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Forwarder extends MailObject 21 | { 22 | /** @var string[] */ 23 | private $recipients; 24 | 25 | /** 26 | * Construct the object. 27 | * 28 | * @param string $prefix The part before the @ in the address 29 | * @param Domain $domain The containing domain 30 | * @param array|string $recipients Array or string containing the recipients 31 | */ 32 | public function __construct($prefix, Domain $domain, $recipients) 33 | { 34 | parent::__construct($prefix, $domain); 35 | $this->recipients = is_string($recipients) ? array_map('trim', explode(',', $recipients)) : $recipients; 36 | } 37 | 38 | /** 39 | * Creates a new forwarder. 40 | * 41 | * @param Domain $domain 42 | * @param string $prefix 43 | * @param string|string[] $recipients 44 | * @return Forwarder 45 | */ 46 | public static function create(Domain $domain, $prefix, $recipients) 47 | { 48 | $domain->invokePost('EMAIL_FORWARDERS', 'create', [ 49 | 'user' => $prefix, 50 | 'email' => is_array($recipients) ? implode(',', $recipients) : $recipients, 51 | ]); 52 | return new self($prefix, $domain, $recipients); 53 | } 54 | 55 | /** 56 | * Deletes the forwarder. 57 | */ 58 | public function delete() 59 | { 60 | $this->invokeDelete('EMAIL_FORWARDERS', 'select0'); 61 | } 62 | 63 | /** 64 | * Returns a list of the recipients of this forwarder. 65 | * 66 | * @return string[] 67 | */ 68 | public function getRecipients() 69 | { 70 | return $this->recipients; 71 | } 72 | 73 | /** 74 | * Returns the list of valid aliases for this account. 75 | * 76 | * @return string[] 77 | */ 78 | public function getAliases() 79 | { 80 | return array_map(function ($domain) { 81 | return $this->getPrefix() . '@' . $domain; 82 | }, $this->getDomain()->getDomainNames()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Email/MailObject.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | abstract class MailObject extends DomainObject 21 | { 22 | /** 23 | * Delete the object. 24 | * 25 | * @param string $command Command to execute 26 | * @param string $paramName Parameter name for the delete command 27 | */ 28 | protected function invokeDelete($command, $paramName) 29 | { 30 | $this->invokePost($command, 'delete', [$paramName => $this->getPrefix()]); 31 | } 32 | 33 | /** 34 | * Returns the full email address for this forwarder. 35 | * 36 | * @return string 37 | */ 38 | public function getEmailAddress() 39 | { 40 | return $this->getPrefix() . '@' . $this->getDomainName(); 41 | } 42 | 43 | /** 44 | * Returns the domain-agnostic part before the @ in the forwarder. 45 | * 46 | * @return string 47 | */ 48 | public function getPrefix() 49 | { 50 | return $this->getName(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Email/Mailbox.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Mailbox extends MailObject 21 | { 22 | const CACHE_DATA = 'mailbox'; 23 | 24 | /** 25 | * Construct the object. 26 | * 27 | * @param string $prefix The part before the @ in the address 28 | * @param Domain $domain The containing domain 29 | * @param string|array|null $config URL encoded config string as returned by CMD_API_POP 30 | */ 31 | public function __construct($prefix, Domain $domain, $config = null) 32 | { 33 | parent::__construct($prefix, $domain); 34 | if (isset($config)) { 35 | $this->setCache(self::CACHE_DATA, is_string($config) ? \GuzzleHttp\Psr7\parse_query($config) : $config); 36 | } 37 | } 38 | 39 | /** 40 | * Creates a new mailbox. 41 | * 42 | * @param Domain $domain Domain to add the account to 43 | * @param string $prefix Prefix for the account 44 | * @param string $password Password for the account 45 | * @param int|null $quota Quota in megabytes, or zero/null for unlimited 46 | * @param int|null $sendLimit Send limit, or 0 for unlimited, or null for system default 47 | * @return Mailbox The created mailbox 48 | */ 49 | public static function create(Domain $domain, $prefix, $password, $quota = null, $sendLimit = null) 50 | { 51 | $domain->invokePost('POP', 'create', [ 52 | 'user' => $prefix, 53 | 'passwd' => $password, 54 | 'passwd2' => $password, 55 | 'quota' => intval($quota) ?: 0, 56 | 'limit' => isset($sendLimit) ? (intval($sendLimit) ?: 0) : null, 57 | ]); 58 | return new self($prefix, $domain); 59 | } 60 | 61 | /** 62 | * Deletes the mailbox. 63 | */ 64 | public function delete() 65 | { 66 | $this->invokeDelete('POP', 'user'); 67 | } 68 | 69 | /** 70 | * Reset the password for this mailbox. 71 | * 72 | * @param string $newPassword 73 | */ 74 | public function setPassword($newPassword) 75 | { 76 | $this->invokePost('POP', 'modify', [ 77 | 'user' => $this->getPrefix(), 78 | 'passwd' => $newPassword, 79 | 'passwd2' => $newPassword, 80 | ], false); 81 | } 82 | 83 | /** 84 | * Returns the disk quota in megabytes. 85 | * 86 | * @return float|null 87 | */ 88 | public function getDiskLimit() 89 | { 90 | return floatval($this->getData('quota')) ?: null; 91 | } 92 | 93 | /** 94 | * Returns the disk usage in megabytes. 95 | * 96 | * @return float 97 | */ 98 | public function getDiskUsage() 99 | { 100 | return floatval($this->getData('usage')); 101 | } 102 | 103 | /** 104 | * Return the amount of mails sent in the current period. 105 | * 106 | * @return int 107 | */ 108 | public function getMailsSent() 109 | { 110 | return intval($this->getData('sent')); 111 | } 112 | 113 | /** 114 | * Return the maximum number of mails that can be send each day 115 | * 116 | * @return int 117 | */ 118 | public function getMailLimit() 119 | { 120 | return intval($this->getData('limit')); 121 | } 122 | 123 | /** 124 | * Returns if the mailbox is suspended or not 125 | * 126 | * @return bool 127 | */ 128 | public function getMailSuspended() 129 | { 130 | return( strcasecmp($this->getData('suspended'),"yes" ) == 0 ); 131 | } 132 | 133 | 134 | /** 135 | * Cache wrapper to keep mailbox stats up to date. 136 | * 137 | * @param string $key 138 | * @return mixed 139 | */ 140 | protected function getData($key) 141 | { 142 | return $this->getCacheItem(self::CACHE_DATA, $key, function () { 143 | $result = $this->getContext()->invokeApiGet('POP', [ 144 | 'domain' => $this->getDomainName(), 145 | 'action' => 'full_list', 146 | ]); 147 | 148 | return \GuzzleHttp\Psr7\parse_query($result[$this->getPrefix()]); 149 | }); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Users/Admin.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Admin extends Reseller 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function __construct($name, UserContext $context, $config = null) 28 | { 29 | parent::__construct($name, $context, $config); 30 | } 31 | 32 | /** 33 | * @return AdminContext 34 | */ 35 | public function impersonate() 36 | { 37 | /** @var AdminContext $context */ 38 | if (!($context = $this->getContext()) instanceof AdminContext) { 39 | throw new DirectAdminException('You need to be an admin to impersonate another admin'); 40 | } 41 | return $context->impersonateAdmin($this->getUsername()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Users/Reseller.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Reseller extends User 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function __construct($name, UserContext $context, $config = null) 30 | { 31 | parent::__construct($name, $context, $config); 32 | } 33 | 34 | /** 35 | * @param string $username 36 | * @return null|User 37 | */ 38 | public function getUser($username) 39 | { 40 | $users = $this->getUsers(); 41 | return isset($users[$username]) ? $users[$username] : null; 42 | } 43 | 44 | /** 45 | * @return User[] 46 | */ 47 | public function getUsers() 48 | { 49 | return BaseObject::toObjectArray($this->getContext()->invokeApiGet('SHOW_USERS', ['reseller' => $this->getUsername()]), 50 | User::class, $this->getContext()); 51 | } 52 | 53 | /** 54 | * @return ResellerContext 55 | */ 56 | public function impersonate() 57 | { 58 | /** @var AdminContext $context */ 59 | if (!($context = $this->getContext()) instanceof AdminContext) { 60 | throw new DirectAdminException('You need to be an admin to impersonate a reseller'); 61 | } 62 | return $context->impersonateReseller($this->getUsername()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/DirectAdmin/Objects/Users/User.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class User extends BaseObject 28 | { 29 | const CACHE_CONFIG = 'config'; 30 | const CACHE_DATABASES = 'databases'; 31 | const CACHE_USAGE = 'usage'; 32 | 33 | /** @var Domain[] * */ 34 | private $domains; 35 | 36 | /** 37 | * Construct the object. 38 | * 39 | * @param string $name Username of the account 40 | * @param UserContext $context The context managing this object 41 | * @param mixed|null $config An optional preloaded configuration 42 | */ 43 | public function __construct($name, UserContext $context, $config = null) 44 | { 45 | parent::__construct($name, $context); 46 | if (isset($config)) { 47 | $this->setCache(self::CACHE_CONFIG, $config); 48 | } 49 | } 50 | 51 | /** 52 | * Clear the object's internal cache. 53 | */ 54 | public function clearCache() 55 | { 56 | unset($this->domains); 57 | parent::clearCache(); 58 | } 59 | 60 | /** 61 | * Creates a new database under this user. 62 | * 63 | * @param string $name Database name, without _ prefix 64 | * @param string $username Username to access the database with, without _ prefix 65 | * @param string|null $password Password, or null if database user already exists 66 | * @return Database Newly created database 67 | */ 68 | public function createDatabase($name, $username, $password = null) 69 | { 70 | $db = Database::create($this->getSelfManagedUser(), $name, $username, $password); 71 | $this->clearCache(); 72 | return $db; 73 | } 74 | 75 | /** 76 | * Creates a new domain under this user. 77 | * 78 | * @param string $domainName Domain name to create 79 | * @param float|null $bandwidthLimit Bandwidth limit in MB, or NULL to share with account 80 | * @param float|null $diskLimit Disk limit in MB, or NULL to share with account 81 | * @param bool|null $ssl Whether SSL is to be enabled, or NULL to fallback to account default 82 | * @param bool|null $php Whether PHP is to be enabled, or NULL to fallback to account default 83 | * @param bool|null $cgi Whether CGI is to be enabled, or NULL to fallback to account default 84 | * @return Domain Newly created domain 85 | */ 86 | public function createDomain($domainName, $bandwidthLimit = null, $diskLimit = null, $ssl = null, $php = null, $cgi = null) 87 | { 88 | $domain = Domain::create($this->getSelfManagedUser(), $domainName, $bandwidthLimit, $diskLimit, $ssl, $php, $cgi); 89 | $this->clearCache(); 90 | return $domain; 91 | } 92 | 93 | /** 94 | * @return string The username 95 | */ 96 | public function getUsername() 97 | { 98 | return $this->getName(); 99 | } 100 | 101 | /** 102 | * Returns the bandwidth limit of the user. 103 | * 104 | * @return float|null Limit in megabytes, or null for unlimited 105 | */ 106 | public function getBandwidthLimit() 107 | { 108 | return floatval($this->getConfig('bandwidth')) ?: null; 109 | } 110 | 111 | /** 112 | * Returns the current period's bandwidth usage in megabytes. 113 | * 114 | * @return float 115 | */ 116 | public function getBandwidthUsage() 117 | { 118 | return floatval($this->getUsage('bandwidth')); 119 | } 120 | 121 | /** 122 | * Returns the database quota of the user. 123 | * 124 | * @return int|null Limit, or null for unlimited 125 | */ 126 | public function getDatabaseLimit() 127 | { 128 | return intval($this->getConfig('mysql')) ?: null; 129 | } 130 | 131 | /** 132 | * Returns the current number databases in use. 133 | * 134 | * @return int 135 | */ 136 | public function getDatabaseUsage() 137 | { 138 | return intval($this->getUsage('mysql')); 139 | } 140 | 141 | /** 142 | * Returns the disk quota of the user. 143 | * 144 | * @return float|null Limit in megabytes, or null for unlimited 145 | */ 146 | public function getDiskLimit() 147 | { 148 | return floatval($this->getConfig('quota')) ?: null; 149 | } 150 | 151 | /** 152 | * Returns the current disk usage in megabytes. 153 | * 154 | * @return float 155 | */ 156 | public function getDiskUsage() 157 | { 158 | return floatval($this->getUsage('quota')); 159 | } 160 | 161 | /** 162 | * @return Domain|null The default domain for the user, if any 163 | */ 164 | public function getDefaultDomain() 165 | { 166 | if (empty($name = $this->getConfig('domain'))) { 167 | return null; 168 | } 169 | return $this->getDomain($name); 170 | } 171 | 172 | /** 173 | * Returns maximum number of domains allowed to this user, or NULL for unlimited. 174 | * 175 | * @return int|null 176 | */ 177 | public function getDomainLimit() 178 | { 179 | return intval($this->getConfig('vdomains')) ?: null; 180 | } 181 | 182 | /** 183 | * Returns number of domains owned by this user. 184 | * 185 | * @return int 186 | */ 187 | public function getDomainUsage() 188 | { 189 | return intval($this->getUsage('vdomains')); 190 | } 191 | 192 | /** 193 | * Returns whether the user is currently suspended. 194 | * 195 | * @return bool 196 | */ 197 | public function isSuspended() 198 | { 199 | return Conversion::toBool($this->getConfig('suspended')); 200 | } 201 | 202 | /** 203 | * @return Domain[] 204 | */ 205 | public function getDatabases() 206 | { 207 | return $this->getCache(self::CACHE_DATABASES, function () { 208 | $databases = []; 209 | foreach ($this->getSelfManagedContext()->invokeApiGet('DATABASES') as $fullName) { 210 | list($user, $db) = explode('_', $fullName, 2); 211 | if ($this->getUsername() != $user) { 212 | throw new DirectAdminException('Username incorrect on database ' . $fullName); 213 | } 214 | $databases[$db] = new Database($db, $this, $this->getSelfManagedContext()); 215 | } 216 | return $databases; 217 | }); 218 | } 219 | 220 | /** 221 | * @param string $domainName 222 | * @return null|Domain 223 | */ 224 | public function getDomain($domainName) 225 | { 226 | if (!isset($this->domains)) { 227 | $this->getDomains(); 228 | } 229 | return isset($this->domains[$domainName]) ? $this->domains[$domainName] : null; 230 | } 231 | 232 | /** 233 | * @return Domain[] 234 | */ 235 | public function getDomains() 236 | { 237 | if (!isset($this->domains)) { 238 | if (!$this->isSelfManaged()) { 239 | $this->domains = $this->impersonate()->getDomains(); 240 | } else { 241 | $this->domains = BaseObject::toRichObjectArray($this->getContext()->invokeApiGet('ADDITIONAL_DOMAINS'), Domain::class, $this->getContext()); 242 | } 243 | } 244 | return $this->domains; 245 | } 246 | 247 | /** 248 | * @return string The user type, as one of the ACCOUNT_TYPE_ constants in the DirectAdmin class 249 | */ 250 | public function getType() 251 | { 252 | return $this->getConfig('usertype'); 253 | } 254 | 255 | /** 256 | * @return bool Whether the user can use CGI 257 | */ 258 | public function hasCGI() 259 | { 260 | return Conversion::toBool($this->getConfig('cgi')); 261 | } 262 | 263 | /** 264 | * @return bool Whether the user can use PHP 265 | */ 266 | public function hasPHP() 267 | { 268 | return Conversion::toBool($this->getConfig('php')); 269 | } 270 | 271 | /** 272 | * @return bool Whether the user can use SSL 273 | */ 274 | public function hasSSL() 275 | { 276 | return Conversion::toBool($this->getConfig('ssl')); 277 | } 278 | 279 | /** 280 | * @return UserContext 281 | */ 282 | public function impersonate() 283 | { 284 | /** @var ResellerContext $context */ 285 | if (!($context = $this->getContext()) instanceof ResellerContext) { 286 | throw new DirectAdminException('You need to be at least a reseller to impersonate'); 287 | } 288 | return $context->impersonateUser($this->getUsername()); 289 | } 290 | 291 | /** 292 | * Modifies the configuration of the user. For available keys in the array check the documentation on 293 | * CMD_API_MODIFY_USER in the linked document. 294 | * 295 | * @param array $newConfig Associative array of values to be modified 296 | * @url http://www.directadmin.com/api.html#modify 297 | */ 298 | public function modifyConfig(array $newConfig) 299 | { 300 | $this->getContext()->invokeApiPost('MODIFY_USER', array_merge( 301 | $this->loadConfig(), 302 | Conversion::processUnlimitedOptions($newConfig), 303 | ['action' => 'customize', 'user' => $this->getUsername()] 304 | )); 305 | $this->clearCache(); 306 | } 307 | 308 | /** 309 | * @param bool $newValue Whether catch-all email is enabled for this user 310 | */ 311 | public function setAllowCatchall($newValue) 312 | { 313 | $this->modifyConfig(['catchall' => Conversion::onOff($newValue)]); 314 | } 315 | 316 | /** 317 | * @param float|null $newValue New value, or NULL for unlimited 318 | */ 319 | public function setBandwidthLimit($newValue) 320 | { 321 | $this->modifyConfig(['bandwidth' => isset($newValue) ? floatval($newValue) : null]); 322 | } 323 | 324 | /** 325 | * @param float|null $newValue New value, or NULL for unlimited 326 | */ 327 | public function setDiskLimit($newValue) 328 | { 329 | $this->modifyConfig(['quota' => isset($newValue) ? floatval($newValue) : null]); 330 | } 331 | 332 | /** 333 | * @param int|null $newValue New value, or NULL for unlimited 334 | */ 335 | public function setDomainLimit($newValue) 336 | { 337 | $this->modifyConfig(['vdomains' => isset($newValue) ? intval($newValue) : null]); 338 | } 339 | 340 | /** 341 | * Constructs the correct object from the given user config. 342 | * 343 | * @param array $config The raw config from DirectAdmin 344 | * @param UserContext $context The context within which the config was retrieved 345 | * @return Admin|Reseller|User The correct object 346 | * @throws DirectAdminException If the user type could not be determined 347 | */ 348 | public static function fromConfig($config, UserContext $context) 349 | { 350 | $name = $config['username']; 351 | switch ($config['usertype']) { 352 | case DirectAdmin::ACCOUNT_TYPE_USER: 353 | return new self($name, $context, $config); 354 | case DirectAdmin::ACCOUNT_TYPE_RESELLER: 355 | return new Reseller($name, $context, $config); 356 | case DirectAdmin::ACCOUNT_TYPE_ADMIN: 357 | return new Admin($name, $context, $config); 358 | default: 359 | throw new DirectAdminException("Unknown user type '$config[usertype]'"); 360 | } 361 | } 362 | 363 | /** 364 | * Internal function to safe guard config changes and cache them. 365 | * 366 | * @param string $item Config item to retrieve 367 | * @return mixed The value of the config item, or NULL 368 | */ 369 | private function getConfig($item) 370 | { 371 | return $this->getCacheItem(self::CACHE_CONFIG, $item, function () { 372 | return $this->loadConfig(); 373 | }); 374 | } 375 | 376 | /** 377 | * Internal function to safe guard usage changes and cache them. 378 | * 379 | * @param string $item Usage item to retrieve 380 | * @return mixed The value of the stats item, or NULL 381 | */ 382 | private function getUsage($item) 383 | { 384 | return $this->getCacheItem(self::CACHE_USAGE, $item, function () { 385 | return $this->getContext()->invokeApiGet('SHOW_USER_USAGE', ['user' => $this->getUsername()]); 386 | }); 387 | } 388 | 389 | /** 390 | * @return UserContext The local user context 391 | */ 392 | protected function getSelfManagedContext() 393 | { 394 | return $this->isSelfManaged() ? $this->getContext() : $this->impersonate(); 395 | } 396 | 397 | /** 398 | * @return User The user acting as himself 399 | */ 400 | protected function getSelfManagedUser() 401 | { 402 | return $this->isSelfManaged() ? $this : $this->impersonate()->getContextUser(); 403 | } 404 | 405 | /** 406 | * @return bool Whether the account is managing itself 407 | */ 408 | protected function isSelfManaged() 409 | { 410 | return $this->getUsername() === $this->getContext()->getUsername(); 411 | } 412 | 413 | /** 414 | * Loads the current user configuration from the server. 415 | * 416 | * @return array 417 | */ 418 | private function loadConfig() 419 | { 420 | return $this->getContext()->invokeApiGet('SHOW_USER_CONFIG', ['user' => $this->getUsername()]); 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/DirectAdmin/Utility/Conversion.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Conversion 19 | { 20 | /** 21 | * Reduces any input to an ON/OFF value. 22 | * 23 | * @param mixed $input Data to convert 24 | * @param mixed $default Fallback to use if $input is NULL 25 | * @return string Either ON or OFF 26 | */ 27 | public static function onOff($input, $default = false) 28 | { 29 | return self::toBool($input, $default) ? 'ON' : 'OFF'; 30 | } 31 | 32 | /** 33 | * Expands a single option to its unlimited counterpart if NULL or literal 'unlimited'. 34 | * 35 | * @param array $options Array of options to process 36 | * @param string $key Key of the item to process 37 | */ 38 | protected static function processUnlimitedOption(array &$options, $key) 39 | { 40 | $ukey = "u{$key}"; 41 | unset($options[$ukey]); 42 | if (array_key_exists($key, $options) && ($options[$key] === 'unlimited' || !isset($options[$key]))) { 43 | $options[$ukey] = 'ON'; 44 | } 45 | } 46 | 47 | /** 48 | * Detects package/domain options that can be unlimited and sets the accordingly. 49 | * 50 | * @param array $options 51 | * @return array Modified array 52 | */ 53 | public static function processUnlimitedOptions(array $options) 54 | { 55 | foreach (['bandwidth', 'domainptr', 'ftp', 'mysql', 'nemailf', 'nemailml', 'nemailr', 'nemails', 56 | 'nsubdomains', 'quota', 'vdomains', ] as $key) { 57 | self::processUnlimitedOption($options, $key); 58 | } 59 | return $options; 60 | } 61 | 62 | /** 63 | * Processes DirectAdmin style encoded responses into a sane array. 64 | * 65 | * @param string $data 66 | * @return array 67 | */ 68 | public static function responseToArray($data) 69 | { 70 | $unescaped = preg_replace_callback('/&#([0-9]{2})/', function ($val) { 71 | return chr($val[1]); 72 | }, $data); 73 | return \GuzzleHttp\Psr7\parse_query($unescaped); 74 | } 75 | 76 | /** 77 | * Ensures a DA-style response element is wrapped properly as an array. 78 | * 79 | * @param mixed $result Messy input 80 | * @return array Sane output 81 | */ 82 | public static function sanitizeArray($result) 83 | { 84 | if (count($result) == 1 && isset($result['list[]'])) { 85 | $result = $result['list[]']; 86 | } 87 | return is_array($result) ? $result : [$result]; 88 | } 89 | 90 | /** 91 | * Converts values like ON, YES etc. to proper boolean variables. 92 | * 93 | * @param mixed $value Value to be converted 94 | * @param mixed $default Value to use if $value is NULL 95 | * @return bool 96 | */ 97 | public static function toBool($value, $default = false) 98 | { 99 | return filter_var(isset($value) ? $value : $default, FILTER_VALIDATE_BOOLEAN); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/DirectAdmin/AccountManagementTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class AccountManagementTest extends \PHPUnit\Framework\TestCase 19 | { 20 | /** 21 | * This function is explicitly implemented as setup, not teardown, so in case of failed tests you may investigate 22 | * the accounts in DirectAdmin to see what's wrong. 23 | */ 24 | public static function setUpBeforeClass() 25 | { 26 | try { 27 | // Ensure all test accounts are gone 28 | $adminContext = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 29 | $adminContext->deleteAccounts([USER_USERNAME, RESELLER_USERNAME, ADMIN_USERNAME]); 30 | 31 | } catch (\Exception $e) { 32 | // Silently fail as this is expected behaviour 33 | } 34 | } 35 | 36 | public function testCreateAdmin() 37 | { 38 | $adminContext = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 39 | $admin = $adminContext->createAdmin(ADMIN_USERNAME, ADMIN_PASSWORD, TEST_EMAIL); 40 | $this->assertEquals(ADMIN_USERNAME, $admin->getUsername()); 41 | $this->assertEquals(DirectAdmin::ACCOUNT_TYPE_ADMIN, $admin->getType()); 42 | } 43 | 44 | /** 45 | * @depends testCreateAdmin 46 | */ 47 | public function testCreateReseller() 48 | { 49 | $adminContext = DirectAdmin::connectAdmin(DIRECTADMIN_URL, ADMIN_USERNAME, ADMIN_PASSWORD, true); 50 | $reseller = $adminContext->createReseller(RESELLER_USERNAME, RESELLER_PASSWORD, 51 | TEST_EMAIL, TEST_RESELLER_DOMAIN); 52 | 53 | $this->assertEquals(RESELLER_USERNAME, $reseller->getUsername()); 54 | $this->assertEquals(DirectAdmin::ACCOUNT_TYPE_RESELLER, $reseller->getType()); 55 | $this->assertEquals($reseller->getDefaultDomain()->getDomainName(), TEST_RESELLER_DOMAIN); 56 | 57 | $getReseller = $adminContext->getReseller(RESELLER_USERNAME); 58 | $this->assertEquals(RESELLER_USERNAME, $getReseller->getUsername()); 59 | $this->assertEquals(DirectAdmin::ACCOUNT_TYPE_RESELLER, $getReseller->getType()); 60 | } 61 | 62 | /** 63 | * @depends testCreateReseller 64 | */ 65 | public function testCreateUser() 66 | { 67 | $resellerContext = DirectAdmin::connectReseller(DIRECTADMIN_URL, RESELLER_USERNAME, RESELLER_PASSWORD, true); 68 | $this->assertNotEmpty($ips = $resellerContext->getIPs()); 69 | 70 | $user = $resellerContext->createUser(USER_USERNAME, USER_PASSWORD, 71 | TEST_EMAIL, TEST_USER_DOMAIN, $ips[0]); 72 | 73 | $this->assertEquals(USER_USERNAME, $user->getUsername()); 74 | $this->assertEquals(DirectAdmin::ACCOUNT_TYPE_USER, $user->getType()); 75 | $this->assertEquals($user->getDefaultDomain()->getDomainName(), TEST_USER_DOMAIN); 76 | } 77 | 78 | /** 79 | * @depends testCreateUser 80 | */ 81 | public function testLoginUser() 82 | { 83 | $userContext = DirectAdmin::connectUser(DIRECTADMIN_URL, USER_USERNAME, USER_PASSWORD, true); 84 | $this->assertEquals(USER_USERNAME, $userContext->getUsername()); 85 | $this->assertEquals(USER_USERNAME, $userContext->getContextUser()->getUsername()); 86 | } 87 | 88 | /** 89 | * @depends testCreateUser 90 | */ 91 | public function testImpersonation() 92 | { 93 | $adminContext = DirectAdmin::connectAdmin(DIRECTADMIN_URL, ADMIN_USERNAME, ADMIN_PASSWORD, true); 94 | $resellerContext = $adminContext->impersonateReseller(RESELLER_USERNAME); 95 | $userContext = $resellerContext->impersonateUser(USER_USERNAME); 96 | $this->assertEquals(DirectAdmin::ACCOUNT_TYPE_USER, $userContext->getType()); 97 | $this->assertEquals(USER_USERNAME, $userContext->getContextUser()->getUsername()); 98 | } 99 | 100 | /** 101 | * @depends testCreateUser 102 | */ 103 | public function testSuspendAccounts() 104 | { 105 | // Have to separately suspend the user as otherwise the order is not determined whether it's containing 106 | // reseller is suspended first. Also - it implicitly tests both calls like this 107 | $adminContext = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 108 | $adminContext->suspendAccount(USER_USERNAME); 109 | $adminContext->suspendAccounts([RESELLER_USERNAME, ADMIN_USERNAME]); 110 | 111 | $user = $adminContext->impersonateUser( USER_USERNAME, true ); 112 | $this->assertEquals( $user->getContextUser()->isSuspended(), true ); 113 | 114 | $reseller = $adminContext->impersonateReseller( RESELLER_USERNAME, true ); 115 | $this->assertEquals( $reseller->getContextUser()->isSuspended(), true ); 116 | 117 | $admin = $adminContext->impersonateAdmin( ADMIN_USERNAME, true ); 118 | $this->assertEquals( $admin->getContextUser()->isSuspended(), true ); 119 | } 120 | 121 | /** 122 | * @depends testCreateUser 123 | */ 124 | public function testUnsuspendAccounts() 125 | { 126 | $adminContext = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 127 | $adminContext->unsuspendAccount(USER_USERNAME); 128 | $adminContext->unsuspendAccounts([RESELLER_USERNAME, ADMIN_USERNAME]); 129 | 130 | $user = $adminContext->impersonateUser(USER_USERNAME, true); 131 | $this->assertEquals($user->getContextUser()->isSuspended(), false); 132 | 133 | $reseller = $adminContext->impersonateReseller(RESELLER_USERNAME, true); 134 | $this->assertEquals($reseller->getContextUser()->isSuspended(), false); 135 | 136 | $admin = $adminContext->impersonateAdmin(ADMIN_USERNAME, true); 137 | $this->assertEquals($admin->getContextUser()->isSuspended(), false); 138 | } 139 | 140 | /** 141 | * @depends testCreateUser 142 | */ 143 | public function testDeleteAccounts() 144 | { 145 | // Have to separately delete the user as otherwise the order is not determined whether it's containing 146 | // reseller is removed first. Also - it implicitly tests both calls like this 147 | $adminContext = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 148 | $adminContext->deleteAccount(USER_USERNAME); 149 | $adminContext->deleteAccounts([RESELLER_USERNAME, ADMIN_USERNAME]); 150 | 151 | $this->assertNull( $adminContext->getUser( USER_USERNAME ) ); 152 | $this->assertNull( $adminContext->getReseller( RESELLER_USERNAME ) ); 153 | 154 | $admins = $adminContext->getAdmins(); 155 | $this->assertFalse( in_array( ADMIN_USERNAME, $admins ) ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/DirectAdmin/AdminTest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class AdminTest extends \PHPUnit\Framework\TestCase 21 | { 22 | /** @var AdminContext */ 23 | private static $master; 24 | 25 | /** @var Admin */ 26 | private static $admin; 27 | 28 | public static function setUpBeforeClass() 29 | { 30 | self::$master = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 31 | self::$admin = self::$master->createAdmin(ADMIN_USERNAME, ADMIN_PASSWORD, TEST_EMAIL); 32 | } 33 | 34 | public static function tearDownAfterClass() 35 | { 36 | self::$master->deleteAccount(self::$admin->getUsername()); 37 | } 38 | 39 | public function testImpersonate() 40 | { 41 | $context = self::$admin->impersonate(); 42 | $this->assertEquals(self::$admin->getUsername(), $context->getUsername()); 43 | return $context; 44 | } 45 | 46 | /** 47 | * @depends testImpersonate 48 | */ 49 | public function testAccountListings(AdminContext $context) 50 | { 51 | $users = $context->getAllUsers(); 52 | $resellers = $context->getResellers(); 53 | $admins = $context->getAdmins(); 54 | $accounts = $context->getAllAccounts(); 55 | 56 | $this->assertArrayHasKey(ADMIN_USERNAME, $admins); 57 | $this->assertArrayHasKey(MASTER_ADMIN_USERNAME, $accounts); 58 | $this->assertEquals(count($accounts), count($users) + count($resellers) + count($admins)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/DirectAdmin/EnvironmentTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class EnvironmentTest extends \PHPUnit\Framework\TestCase 19 | { 20 | /** 21 | * @expectedException \Omines\DirectAdmin\DirectAdminException 22 | */ 23 | public function testCorruptedUrl() 24 | { 25 | $admin = DirectAdmin::connectAdmin('noproto://www.google.com/', 'username', 'password'); 26 | $admin->getContextUser()->getType(); 27 | } 28 | 29 | /** 30 | * @expectedException \Omines\DirectAdmin\DirectAdminException 31 | */ 32 | public function testInvalidUsername() 33 | { 34 | $admin = DirectAdmin::connectAdmin(DIRECTADMIN_URL, '_invalid', MASTER_ADMIN_PASSWORD); 35 | $admin->getContextUser()->getType(); 36 | } 37 | 38 | /** 39 | * @expectedException \Omines\DirectAdmin\DirectAdminException 40 | */ 41 | public function testInvalidPassword() 42 | { 43 | $admin = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD . '_invalid'); 44 | $admin->getContextUser()->getType(); 45 | } 46 | 47 | /** 48 | * @expectedException \Omines\DirectAdmin\DirectAdminException 49 | */ 50 | public function testInvalidCall() 51 | { 52 | $admin = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 53 | $admin->invokeApiGet('INVALID_COMMAND'); 54 | } 55 | 56 | /** 57 | * @expectedException \Omines\DirectAdmin\DirectAdminException 58 | */ 59 | public function testInvalidUrl() 60 | { 61 | $admin = DirectAdmin::connectAdmin('http://www.google.com/', 'username', 'password'); 62 | $admin->getContextUser()->getType(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/DirectAdmin/ResellerTest.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ResellerTest extends \PHPUnit\Framework\TestCase 22 | { 23 | /** @var AdminContext */ 24 | private static $master; 25 | 26 | /** @var Reseller */ 27 | private static $reseller; 28 | 29 | public static function setUpBeforeClass() 30 | { 31 | self::$master = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 32 | self::$reseller = self::$master->createReseller(RESELLER_USERNAME, RESELLER_PASSWORD, TEST_EMAIL, TEST_RESELLER_DOMAIN); 33 | } 34 | 35 | public static function tearDownAfterClass() 36 | { 37 | self::$master->deleteAccount(self::$reseller->getUsername()); 38 | } 39 | 40 | public function testImpersonate() 41 | { 42 | $context = self::$reseller->impersonate(); 43 | $this->assertEquals(self::$reseller->getUsername(), $context->getUsername()); 44 | return $context; 45 | } 46 | 47 | /** 48 | * @depends testImpersonate 49 | */ 50 | public function testGetUsers(ResellerContext $context) 51 | { 52 | $this->assertEmpty(self::$reseller->getUsers()); 53 | $this->assertEmpty($context->getUsers()); 54 | $this->assertNull(self::$reseller->getUser(USER_USERNAME)); 55 | $this->assertNull($context->getUser(USER_USERNAME)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/DirectAdmin/UserTest.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class UserTest extends \PHPUnit\Framework\TestCase 23 | { 24 | /** @var AdminContext */ 25 | private static $master; 26 | 27 | /** @var User */ 28 | private static $user; 29 | 30 | public static function setUpBeforeClass() 31 | { 32 | self::$master = DirectAdmin::connectAdmin(DIRECTADMIN_URL, MASTER_ADMIN_USERNAME, MASTER_ADMIN_PASSWORD); 33 | $ips = self::$master->getIPs(); 34 | self::$user = self::$master->createUser(USER_USERNAME, USER_PASSWORD, TEST_EMAIL, TEST_USER_DOMAIN, $ips[0]); 35 | } 36 | 37 | public static function tearDownAfterClass() 38 | { 39 | self::$master->deleteAccount(self::$user->getUsername()); 40 | } 41 | 42 | public function testImpersonate() 43 | { 44 | $context = self::$user->impersonate(); 45 | $this->assertEquals(self::$user->getUsername(), $context->getUsername()); 46 | return $context; 47 | } 48 | 49 | /** 50 | * @depends testImpersonate 51 | * @expectedException \Omines\DirectAdmin\DirectAdminException 52 | * @expectedExceptionMessage Unknown user type 'invalid' 53 | */ 54 | public function testInvalidUserType(UserContext $context) 55 | { 56 | User::fromConfig([ 57 | 'username' => 'invalid', 58 | 'usertype' => 'invalid', 59 | ], $context); 60 | } 61 | 62 | public function testDefaultDomain() 63 | { 64 | $domain = self::$user->getDefaultDomain(); 65 | $this->assertEquals(TEST_USER_DOMAIN, $domain->getDomainName()); 66 | return $domain; 67 | } 68 | 69 | /** 70 | * @depends testImpersonate 71 | */ 72 | public function testDomainStats(UserContext $context) 73 | { 74 | $domainAsAdmin = self::$user->getDomain(TEST_USER_DOMAIN); 75 | $domainAsUser = $context->getDomain(TEST_USER_DOMAIN); 76 | $this->assertEquals(strval($domainAsAdmin), strval($domainAsUser)); 77 | $this->assertEquals($domainAsAdmin->getBandwidthUsed(), $domainAsUser->getBandwidthUsed()); 78 | $this->assertEquals($domainAsAdmin->getDiskUsage(), $domainAsUser->getDiskUsage()); 79 | $this->assertEquals($context, $domainAsUser->getContext()); 80 | $this->assertEquals(self::$user->getUsername(), $domainAsUser->getOwner()->getUsername()); 81 | 82 | // Should be no further objects or settings yet 83 | $this->assertEmpty($domainAsUser->getAliases()); 84 | $this->assertNull($domainAsUser->getBandwidthLimit()); 85 | $this->assertEmpty($domainAsUser->getPointers()); 86 | $this->assertEmpty($domainAsUser->getForwarders()); 87 | $this->assertEmpty($domainAsUser->getMailboxes()); 88 | } 89 | 90 | /** 91 | * @expectedException \Omines\DirectAdmin\DirectAdminException 92 | * @expectedExceptionMessage Could not determine relationship between context user and domain 93 | */ 94 | public function testDomainCorruption() 95 | { 96 | new Domain('example.org', self::$master, ['domain' => 'example.org', 'username' => 'invalid']); 97 | } 98 | 99 | public function testUserStats() 100 | { 101 | $user = self::$user; 102 | 103 | // Assert the user is not suspended while of the correct type 104 | $this->assertFalse($user->isSuspended()); 105 | $this->assertEquals(DirectAdmin::ACCOUNT_TYPE_USER, $user->getType()); 106 | 107 | // It should not have any usage yet except a single domain 108 | $this->assertEquals(0, $user->getBandwidthUsage()); 109 | $this->assertNull($user->getBandwidthLimit()); 110 | $this->assertEquals(0, $user->getDatabaseUsage()); 111 | $this->assertNull($user->getDatabaseLimit()); 112 | $this->assertEquals(1, $user->getDomainUsage()); 113 | $this->assertNull($user->getDomainLimit()); 114 | $this->assertEquals(0, $user->getDiskUsage()); 115 | $this->assertNull($user->getDiskLimit()); 116 | } 117 | 118 | public function testDomains() 119 | { 120 | $user = self::$user; 121 | 122 | // Create some domains with various settings 123 | $domain1 = $user->createDomain('example1.org'); 124 | $domain2 = $user->createDomain('example2.org', 50); 125 | $domain3 = $user->createDomain('example3.org', 200, 50, false, null, true); 126 | $this->assertCount(4, $domains = $user->getDomains()); 127 | 128 | // Delete all added domains 129 | $domain1->delete(); 130 | $domain2->delete(); 131 | $domain3->delete(); 132 | $user->clearCache(); // This shouldn't be necessary...? 133 | $this->assertCount(1, $user->getDomains()); 134 | } 135 | 136 | public function testDatabases() 137 | { 138 | $user = self::$user; 139 | 140 | // Create 3 databases of which 1 with double user 141 | $db1 = $user->createDatabase('test1', 'test2', 'test3'); 142 | $user->createDatabase('test2', 'test2'); 143 | $user->createDatabase('test3', 'test3', 'test4'); 144 | $this->assertCount(3, $databases = $user->getDatabases()); 145 | $this->assertEquals($db1->getOwner()->getUsername(), $user->getUsername()); 146 | 147 | // Add access host 148 | $db1->createAccessHost('192.168.1.1'); 149 | $this->assertCount(2, $hosts = $db1->getAccessHosts()); 150 | $this->assertEquals('192.168.1.1', $hosts[0]->getHost()); 151 | $hosts[0]->delete(); 152 | $this->assertCount(1, $db1->getAccessHosts()); 153 | 154 | // Delete all of them 155 | $db1->delete(); 156 | $user->clearCache(); // Buggy... 157 | $this->assertCount(2, $databases = $user->getDatabases()); 158 | $databases['test2']->delete(); 159 | $databases['test3']->delete(); 160 | $user->clearCache(); // Buggy... 161 | $this->assertEmpty($user->getDatabases()); 162 | } 163 | 164 | public function testUserQuota() 165 | { 166 | $user = self::$user; 167 | 168 | // Set some quota 169 | $user->setBandwidthLimit(25000); 170 | $user->setDiskLimit(500); 171 | $user->setDomainLimit(5); 172 | 173 | // Verify the settings were applied correctly 174 | $this->assertEquals(25000, $user->getBandwidthLimit()); 175 | $this->assertEquals(500, $user->getDiskLimit()); 176 | $this->assertEquals(5, $user->getDomainLimit()); 177 | 178 | // Unset some quota (implying unlimited) and validate them 179 | $user->setBandwidthLimit(null); 180 | $user->setDiskLimit(null); 181 | $this->assertNull($user->getBandwidthLimit()); 182 | $this->assertNull($user->getDiskLimit()); 183 | } 184 | 185 | /** 186 | * @depends testDefaultDomain 187 | */ 188 | public function testCatchall(Domain $domain) 189 | { 190 | self::$user->setAllowCatchall(true); 191 | $domain->setCatchall('aap@noot.mies'); 192 | $this->assertEquals('aap@noot.mies', $domain->getCatchall()); 193 | $domain->setCatchall(Domain::CATCHALL_BLACKHOLE); 194 | $this->assertEquals(Domain::CATCHALL_BLACKHOLE, $domain->getCatchall()); 195 | $domain->setCatchall(Domain::CATCHALL_FAIL); 196 | $this->assertEquals(Domain::CATCHALL_FAIL, $domain->getCatchall()); 197 | } 198 | 199 | /** 200 | * @depends testDefaultDomain 201 | */ 202 | public function testForwarders(Domain $domain) 203 | { 204 | // Create 2 forwarders after asserting they are the first 205 | $this->assertEmpty($domain->getForwarders()); 206 | $domain->createForwarder('single', 'single@example.org'); 207 | $domain->createForwarder('multiple', ['recipient@example.org', 'recipient@gmail.com']); 208 | $this->assertCount(2, $forwarders = $domain->getForwarders()); 209 | 210 | // Manage single forwarder 211 | $forwarder = $forwarders['single']; 212 | $this->assertEquals('single', $forwarder->getPrefix()); 213 | $this->assertContains('single@example.org', $forwarder->getRecipients()); 214 | $this->assertContains('single@' . TEST_USER_DOMAIN, $forwarder->getAliases()); 215 | 216 | // Delete a forwarder and ensure domain stats are updated 217 | $forwarders['single']->delete(); 218 | $this->assertCount(1, $domain->getForwarders()); 219 | } 220 | 221 | /** 222 | * @depends testDefaultDomain 223 | */ 224 | public function testMailboxes(Domain $domain) 225 | { 226 | // Create 2 forwarders after asserting they are the first 227 | $this->assertEmpty($domain->getMailboxes()); 228 | $mail1 = $domain->createMailbox('mail1', generateTemporaryPassword()); 229 | $mail2 = $domain->createMailbox('mail2', generateTemporaryPassword(), 500, 50); 230 | $this->assertCount(2, $boxes = $domain->getMailboxes()); 231 | 232 | // Check mailbox statistics 233 | $this->assertEquals('mail1@' . TEST_USER_DOMAIN, $boxes['mail1']->getEmailAddress()); 234 | $this->assertNull( $mail1->getDiskLimit() ); 235 | $this->assertEquals(500, $mail2->getDiskLimit()); 236 | $this->assertEquals(0, $mail1->getDiskUsage(), 'Disk usage should be near empty', 0.1); 237 | $this->assertEquals(0, $mail2->getMailsSent()); 238 | $this->assertEquals(50, $mail2->getMailLimit()); 239 | $this->assertFalse( $mail2->getMailSuspended() ); 240 | 241 | // Changing password should not throw any errors 242 | $mail1->setPassword(generateTemporaryPassword()); 243 | 244 | // Delete the mailbox and ensure domain stats are updated 245 | $boxes['mail2']->delete(); 246 | $this->assertCount(1, $domain->getMailboxes()); 247 | $mail1->delete(); 248 | $this->assertEmpty($domain->getMailboxes()); 249 | } 250 | 251 | /** 252 | * @depends testDefaultDomain 253 | */ 254 | public function testPointers(Domain $domain) 255 | { 256 | $domain->createPointer('invalid.pointer.org'); 257 | $domain->createPointer('invalid.alias.org', true); 258 | $this->assertCount(1, $aliases = $domain->getAliases()); 259 | $this->assertCount(1, $pointers = $domain->getPointers()); 260 | $this->assertEquals('invalid.pointer.org', $pointers[0]); 261 | $this->assertEquals('invalid.alias.org', $aliases[0]); 262 | } 263 | 264 | /** 265 | * @depends testDefaultDomain 266 | */ 267 | public function testSubdomains(Domain $domain) 268 | { 269 | // Create 2 subdomains after asserting they are the first 270 | $this->assertEmpty($domain->getSubdomains()); 271 | $sub1 = $domain->createSubdomain('sub1'); 272 | $sub2 = $domain->createSubdomain('sub2'); 273 | $this->assertCount(2, $subdomains = $domain->getSubdomains()); 274 | 275 | // Check properties 276 | $this->assertEquals($sub1->getPrefix(), $subdomains['sub1']->getPrefix()); 277 | $this->assertEquals($sub2->getDomainName(), strval($subdomains['sub2'])); 278 | $this->assertEquals('sub1.' . TEST_USER_DOMAIN, $sub1->getDomainName()); 279 | $this->assertEquals(TEST_USER_DOMAIN, $sub1->getBaseDomainName()); 280 | 281 | // Check deletion 282 | $sub1->delete(); 283 | $this->assertCount(1, $domain->getSubdomains()); 284 | $subdomains['sub2']->delete(false); 285 | $this->assertEmpty($domain->getSubdomains()); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # DirectAdmin API Client Unit Testing 2 | 3 | **IMPORTANT**: as the unit tests create and delete multiple account multiple times per run it is *highly* 4 | recommended to disable the `use_uid_counting` feature on your testing server. See more details 5 | [here](http://www.directadmin.com/features.php?id=979). 6 | 7 | Unit tests are currently to be performed against a live server. The `phpunit.xml.dist` in the main folder 8 | contains commonly used settings. Three variables must be set, either as constants or as environment 9 | variables: 10 | 11 | Name | Meaning 12 | ----------------------- | ----------------------------------------------------- 13 | `DIRECTADMIN_URL` | Base URL of the DirectAdmin server 14 | `MASTER_ADMIN_USERNAME` | Name of an admin account with sufficient permissions 15 | `MASTER_ADMIN_PASSWORD` | Password of the admin account 16 | 17 | Additionally the tests create 3 accounts multiple times, at admin, reseller and user level respectively. 18 | The default names for these accounts are `testadmin`, `testresell` and `testuser`, with generated passwords 19 | sufficiently random not to be hacked during testing. If these values conflict with your testing server you 20 | can override all of them in config, using constants or environment variables. 21 | 22 | For all values you can override check the top of the `phpunit-bootstrap.php` file. 23 | -------------------------------------------------------------------------------- /tests/phpunit-bootstrap.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | 15 | define('PASSWORD_LENGTH', 16); 16 | 17 | function generateTemporaryPassword() 18 | { 19 | static $base = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 20 | $result = ''; 21 | for ($i = 0; $i < PASSWORD_LENGTH; ++$i) { 22 | $result .= $base[mt_rand(0, strlen($base) - 1)]; 23 | } 24 | for ($i = 0; $i < 100; ++$i) { 25 | $result = str_shuffle($result); 26 | } 27 | return $result; 28 | } 29 | 30 | $parameters = [ 31 | 'DIRECTADMIN_URL' => null, 32 | 'MASTER_ADMIN_USERNAME' => null, 33 | 'MASTER_ADMIN_PASSWORD' => null, 34 | 'ADMIN_USERNAME' => 'testadmin', 35 | 'ADMIN_PASSWORD' => generateTemporaryPassword(), 36 | 'RESELLER_USERNAME' => 'testresell', 37 | 'RESELLER_PASSWORD' => generateTemporaryPassword(), 38 | 'USER_USERNAME' => 'testuser', 39 | 'USER_PASSWORD' => generateTemporaryPassword(), 40 | 'TEST_EMAIL' => 'example@127.0.0.1', 41 | 'TEST_RESELLER_DOMAIN' => 'reseller.test.org', 42 | 'TEST_USER_DOMAIN' => 'user.test.org', 43 | ]; 44 | foreach ($parameters as $parameter => &$value) { 45 | // Constants override environment 46 | if (defined($parameter)) { 47 | continue; 48 | } 49 | if (!isset($value) && empty($value = getenv($parameter))) { 50 | throw new RuntimeException("Required setting $parameter was neither set as a constant or an environment variable"); 51 | } 52 | define($parameter, $value); 53 | } 54 | 55 | // Include composer autoload 56 | require __DIR__ . str_replace('/', DIRECTORY_SEPARATOR, '/../vendor/autoload.php'); 57 | 58 | // Polyfill PHPUnit 6.0 both ways 59 | if (!class_exists('\PHPUnit\Framework\TestCase', true)) { 60 | class_alias('\PHPUnit_Framework_TestCase', '\PHPUnit\Framework\TestCase'); 61 | } elseif (!class_exists('\PHPUnit_Framework_TestCase', true)) { 62 | class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase'); 63 | } 64 | --------------------------------------------------------------------------------