PageRenderTime 62ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/bonfire/libraries/Installer_lib.php

http://github.com/ci-bonfire/Bonfire
PHP | 664 lines | 371 code | 84 blank | 209 comment | 43 complexity | e8a93ebbf163eed83af777b6ac6a9e82 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php defined('BASEPATH') || exit('No direct script access allowed');
  2. /**
  3. * Bonfire
  4. *
  5. * An open source project to allow developers to jumpstart their development of
  6. * CodeIgniter applications
  7. *
  8. * @package Bonfire
  9. * @author Bonfire Dev Team
  10. * @copyright Copyright (c) 2011 - 2018, Bonfire Dev Team
  11. * @license http://opensource.org/licenses/MIT The MIT License
  12. * @link http://cibonfire.com
  13. * @since Version 1.0
  14. */
  15. /**
  16. * Installer library.
  17. *
  18. * @package Bonfire\Libraries\Installer_lib
  19. * @author Bonfire Dev Team
  20. * @link http://cibonfire.com/docs/developer/installation
  21. */
  22. class Installer_lib
  23. {
  24. /** @var boolean Indicates whether the default database settings were found. */
  25. public $db_exists = null;
  26. /** @var boolean Indicates whether the database settings were found. */
  27. public $db_settings_exist = null;
  28. /** @var string The version of the currently running PHP parser or extension. */
  29. public $php_version;
  30. /** @var CI The CodeIgniter controller instance. */
  31. private $ci;
  32. /** @var mixed Check whether cURL is enabled in PHP. */
  33. private $curl_error = 0;
  34. /** @var mixed Whether we should check for updates. */
  35. private $curl_update = 0;
  36. /** @var string[] Supported database engines. */
  37. private $supported_dbs = array('mysql', 'mysqli', 'bfmysqli');
  38. /** @var string[] Folders the installer checks for write access. */
  39. private $writable_folders;
  40. /** @var string[] Files the installer checks for write access. */
  41. private $writable_files;
  42. //--------------------------------------------------------------------------
  43. /**
  44. * Constructor
  45. *
  46. * @param array $config unused?
  47. *
  48. * @return void
  49. */
  50. public function __construct($config = array())
  51. {
  52. if (! empty($config['writable_folders'])) {
  53. $this->writable_folders = $config['writable_folders'];
  54. }
  55. if (! empty($config['writable_files'])) {
  56. $this->writable_files = $config['writable_files'];
  57. }
  58. $this->ci =& get_instance();
  59. $this->curl_update = $this->cURL_enabled();
  60. $this->php_version = phpversion();
  61. }
  62. /**
  63. * Check an array of files/folders to see if they are writable and return the
  64. * results in a format usable in the requirements check step of the installation.
  65. *
  66. * Note that this only returns the data in the format expected by the Install
  67. * controller if called via check_folders() and check_files(). Otherwise, the
  68. * files and folders are intermingled unless they are passed as input.
  69. *
  70. * @param array $filesAndFolders An array of paths to files/folders to check.
  71. *
  72. * @return array An associative array with the path as key and boolean value
  73. * indicating whether the path is writable.
  74. */
  75. public function checkWritable(array $filesAndFolders = array())
  76. {
  77. if (empty($filesAndFolders)) {
  78. $filesAndFolders = array_merge($this->writable_files, $this->writable_folders);
  79. }
  80. $this->ci->load->helper('file');
  81. $data = array();
  82. foreach ($filesAndFolders as $fileOrFolder) {
  83. // If it starts with 'public/', then that represents the web root.
  84. // Otherwise, try to locate it from the main folder. This does not use
  85. // DIRECTORY_SEPARATOR because the string is supplied by $this->writable_folders
  86. // or $this->writable_files.
  87. if (strpos($fileOrFolder, 'public/') === 0) {
  88. $realpath = FCPATH . preg_replace('{^public/}', '', $fileOrFolder);
  89. } else {
  90. // Because this is APPPATH, use DIRECTORY_SEPARATOR instead of '/'.
  91. $realpath = str_replace('application' . DIRECTORY_SEPARATOR, '', APPPATH) . $fileOrFolder;
  92. }
  93. $data[$fileOrFolder] = is_really_writable($realpath);
  94. }
  95. return $data;
  96. }
  97. /**
  98. * Determine whether the installed version of PHP is above $version.
  99. *
  100. * @param string $version The version to compare to the installed version.
  101. *
  102. * @return boolean True if the installed version is at or above $version, else
  103. * false.
  104. */
  105. public function php_acceptable($version = null)
  106. {
  107. return version_compare($this->php_version, $version, '>=');
  108. }
  109. /**
  110. * Tests whether the specified database type can be found.
  111. *
  112. * @return boolean
  113. */
  114. public function db_available()
  115. {
  116. $driver = $this->ci->input->post('driver');
  117. switch ($driver) {
  118. case 'mysql':
  119. return function_exists('mysql_connect');
  120. case 'bfmysqli':
  121. case 'mysqli':
  122. return class_exists('Mysqli');
  123. case 'cubrid':
  124. return function_exists('cubrid_connect');
  125. case 'mongodb': // deprecated
  126. return class_exists('Mongo');
  127. case 'mongoclient': // I don't believe we have a driver for this, yet
  128. return class_exists('MongoClient');
  129. case 'mssql':
  130. return function_exists('mssql_connect');
  131. case 'oci8':
  132. return function_exists('oci_connect');
  133. case 'odbc':
  134. return function_exists('odbc_connect');
  135. case 'pdo':
  136. return class_exists('PDO');
  137. case 'postgre':
  138. return function_exists('pg_connect');
  139. case 'sqlite':
  140. return function_exists('sqlite_open');
  141. case 'sqlsrv':
  142. return function_exists('sqlsrv_connect');
  143. default:
  144. return false;
  145. }
  146. }
  147. /**
  148. * Attempts to connect to the database given the existing $_POST vars.
  149. *
  150. * @return boolean
  151. */
  152. public function test_db_connection()
  153. {
  154. if (! $this->db_available()) {
  155. return false;
  156. }
  157. $db_name = $this->ci->input->post('database');
  158. $driver = $this->ci->input->post('driver');
  159. $hostname = $this->ci->input->post('hostname');
  160. $password = $this->ci->input->post('password');
  161. $port = $this->ci->input->post('port');
  162. $username = $this->ci->input->post('username');
  163. switch ($driver) {
  164. case 'mysql':
  165. return @mysql_connect("$hostname:$port", $username, $password);
  166. case 'bfmysqli':
  167. case 'mysqli':
  168. $mysqli = new mysqli($hostname, $username, $password, '', $port);
  169. if (! $mysqli->connect_error) {
  170. return true;
  171. }
  172. return false;
  173. case 'cubrid':
  174. return @cubrid_connect($hostname, $port, $db_name, $username, $password);
  175. case 'mongodb': // deprecated
  176. $connect_string = $this->getMongoConnectionString($hostname, $port, $username, $password, $db_name);
  177. try {
  178. $mongo = new Mongo($connect_string);
  179. return true;
  180. } catch (MongoConnectionException $e) {
  181. show_error('Unable to connect to MongoDB.', 500);
  182. }
  183. return false;
  184. break;
  185. case 'mongoclient': // no driver support at this time
  186. $connect_string = $this->getMongoConnectionString($hostname, $port, $username, $password, $db_name);
  187. try {
  188. $mongo = new MongoClient($connect_string);
  189. return true;
  190. } catch (MongoConnectionException $e) {
  191. show_error('Unable to connect MongoClient.', 500);
  192. }
  193. return false;
  194. break;
  195. case 'mssql':
  196. return @mssql_connect("$hostname,$port", $username, $password);
  197. case 'oci8':
  198. $connect_string = $this->getOracleConnectionString($hostname, $port);
  199. return @oci_connect($username, $password, $connect_string);
  200. case 'odbc':
  201. $connect_string = $this->getOdbcConnectionString($hostname);
  202. return @odbc_connect($connect_string, $username, $password);
  203. case 'pdo':
  204. $connect_string = $this->getPdoConnectionString($hostname, $db_name);
  205. try {
  206. $pdo = new PDO($connect_string, $username, $password);
  207. return true;
  208. } catch (PDOException $e) {
  209. show_error('Unable to connect using PDO.', 500);
  210. }
  211. return false;
  212. break;
  213. case 'postgre':
  214. $connect_string = $this->getPostgreConnectionString($hostname, $port, $username, $password, $db_name);
  215. return @pg_connect($connect_string);
  216. case 'sqlite':
  217. if (! $sqlite = @sqlite_open($db_name, FILE_WRITE_MODE, $error)) {
  218. show_error($error, 500);
  219. return false;
  220. }
  221. return $sqlite;
  222. case 'sqlsrv':
  223. $connection = $this->getSqlsrvConnection($username, $password, $db_name);
  224. return sqlsrv_connect($hostname, $connection);
  225. default:
  226. return false;
  227. }
  228. }
  229. /**
  230. * Perform some basic checks to see if the user has already installed the application
  231. * and just hasn't moved the install folder...
  232. *
  233. * @return boolean True if the application is installed, else false.
  234. */
  235. public function is_installed()
  236. {
  237. // If 'config/installed.txt' exists, the app is installed
  238. if (is_file(APPPATH . 'config/installed.txt')) {
  239. return true;
  240. }
  241. // If the database config doesn't exist, the app is not installed
  242. if (defined('ENVIRONMENT') && is_file(APPPATH . 'config/' . ENVIRONMENT . '/database.php')) {
  243. require(APPPATH . 'config/' . ENVIRONMENT . '/database.php');
  244. } elseif (is_file(APPPATH . 'config/development/database.php')) {
  245. require(APPPATH . 'config/development/database.php');
  246. } elseif (is_file(APPPATH . 'config/database.php')) {
  247. require(APPPATH . 'config/database.php');
  248. } else {
  249. $this->db_settings_exist = false;
  250. return false;
  251. }
  252. // If $db['default'] doesn't exist, the app can't load the database
  253. if (! isset($db) || ! isset($db['default'])) {
  254. $this->db_settings_exist = false;
  255. return false;
  256. }
  257. $this->db_settings_exist = true;
  258. // Make sure the database name is specified
  259. if (empty($db['default']['database'])) {
  260. $this->db_exists = false;
  261. return false;
  262. }
  263. $this->db_exists = true;
  264. $this->ci->load->database($db['default']);
  265. // Does the users table exist?
  266. if (! $this->ci->db->table_exists('users')) {
  267. return false;
  268. }
  269. // Make sure at least one row exists in the users table.
  270. $query = $this->ci->db->get('users');
  271. if ($query->num_rows() == 0) {
  272. return false;
  273. }
  274. defined('BF_INSTALLED') || define('BF_INSTALLED', true);
  275. return true;
  276. }
  277. /**
  278. * Verify that cURL is enabled as a PHP extension.
  279. *
  280. * @return boolean True if cURL is enabled, else false.
  281. */
  282. public function cURL_enabled()
  283. {
  284. return (bool) function_exists('curl_init');
  285. }
  286. /**
  287. * Check an array of folders to see if they are writable and return results
  288. * in a format usable in the requirements check step.
  289. *
  290. * @param string[] $folders the folders to check.
  291. *
  292. * @return array
  293. */
  294. public function check_folders($folders = null)
  295. {
  296. if (is_null($folders)) {
  297. $folders = $this->writable_folders;
  298. }
  299. return $this->checkWritable($folders);
  300. }
  301. /**
  302. * Check an array of files to see if they are writable and return results
  303. * usable in the requirements check step.
  304. *
  305. * @param string[] $files The files to check.
  306. *
  307. * @return array
  308. */
  309. public function check_files($files = null)
  310. {
  311. if (is_null($files)) {
  312. $files = $this->writable_files;
  313. }
  314. return $this->checkWritable($files);
  315. }
  316. /**
  317. * Perform the actual installation of the database, create the config files,
  318. * and install the user account.
  319. *
  320. * @return string|boolean True on successful installation, else an error message.
  321. */
  322. public function setup()
  323. {
  324. // Install default info into the database.
  325. // This is done by running the app, core, and module-specific migrations
  326. // Load the Database before calling the Migrations
  327. $this->ci->load->database();
  328. // Install the database tables.
  329. $this->ci->load->library(
  330. 'migrations/migrations',
  331. array('migrations_path' => BFPATH . 'migrations')
  332. );
  333. // Core Migrations - this is all that is needed for Bonfire install.
  334. if (! $this->ci->migrations->install()) {
  335. return $this->ci->migrations->getErrorMessage();
  336. }
  337. // Save the information to the settings table
  338. $settings = array(
  339. 'site.title' => 'My Bonfire',
  340. 'site.system_email' => 'admin@mybonfire.com',
  341. );
  342. foreach ($settings as $key => $value) {
  343. $setting_rec = array(
  344. 'name' => $key,
  345. 'module' => 'core',
  346. 'value' => $value,
  347. );
  348. $this->ci->db->where('name', $key);
  349. if ($this->ci->db->update('settings', $setting_rec) == false) {
  350. return lang('in_db_settings_error');
  351. }
  352. }
  353. // Update the emailer sender_email
  354. $setting_rec = array(
  355. 'name' => 'sender_email',
  356. 'module' => 'email',
  357. 'value' => '',
  358. );
  359. $this->ci->db->where('name', 'sender_email');
  360. if ($this->ci->db->update('settings', $setting_rec) == false) {
  361. return lang('in_db_settings_error');
  362. }
  363. // Install the admin user in the users table so they can login.
  364. $data = array(
  365. 'role_id' => 1,
  366. 'email' => 'admin@mybonfire.com',
  367. 'username' => 'admin',
  368. 'active' => 1,
  369. );
  370. // As of 0.7, using phpass for password encryption...
  371. require(BFPATH . 'modules/users/libraries/PasswordHash.php');
  372. $iterations = $this->ci->config->item('password_iterations');
  373. $hasher = new PasswordHash($iterations, false);
  374. $password = $hasher->HashPassword('password');
  375. $data['password_hash'] = $password;
  376. $data['created_on'] = date('Y-m-d H:i:s');
  377. $data['display_name'] = $data['username'];
  378. if ($this->ci->db->insert('users', $data) == false) {
  379. $this->errors = lang('in_db_account_error');
  380. return false;
  381. }
  382. // Create a unique encryption key
  383. $this->ci->load->helper('string');
  384. $key = random_string('md5', 40);
  385. $this->ci->load->helper('config_file');
  386. $config_array = array('encryption_key' => $key);
  387. write_config('config', $config_array, '', APPPATH);
  388. // Run custom migrations last. In particular this comes after the core
  389. // migrations, and after populating the user table.
  390. // Get the list of custom modules in the main application
  391. $module_list = $this->getModuleVersions();
  392. if (is_array($module_list) && count($module_list)) {
  393. foreach ($module_list as $module_name => $module_detail) {
  394. // Install the migrations for the custom modules
  395. if (! $this->ci->migrations->install("{$module_name}_")) {
  396. return $this->ci->migrations->getErrorMessage();
  397. }
  398. }
  399. }
  400. // Write a file to /public/install/installed.txt as a simple check
  401. // whether it's installed, so development doesn't require removing the
  402. // install folder.
  403. $this->ci->load->helper('file');
  404. $filename = APPPATH . 'config/installed.txt';
  405. $msg = 'Installed On: ' . date('r') . "\n";
  406. write_file($filename, $msg);
  407. $config_array = array(
  408. 'bonfire.installed' => true,
  409. );
  410. write_config('application', $config_array, '', APPPATH);
  411. return true;
  412. }
  413. //--------------------------------------------------------------------------
  414. // !Private Methods
  415. //--------------------------------------------------------------------------
  416. /**
  417. * Get the versions of the modules.
  418. *
  419. * @return array The installed/latest versions of each module.
  420. */
  421. private function getModuleVersions()
  422. {
  423. $modules = Modules::files(null, 'migrations');
  424. if ($modules === false) {
  425. return false;
  426. }
  427. // Sort modules by key (module directory name)
  428. ksort($modules);
  429. // Get the installed version of all of the modules (modules which have
  430. // not been installed will not be included)
  431. $installedVersions = $this->ci->migrations->getModuleVersions();
  432. $modVersions = array();
  433. // Add the migration data for each module
  434. foreach ($modules as $module => &$mod) {
  435. if (! array_key_exists('migrations', $mod)) {
  436. continue;
  437. }
  438. // Sort module migrations in reverse order
  439. arsort($mod['migrations']);
  440. // Add the installed version, latest version, and list of migrations
  441. $modVersions[$module] = array(
  442. 'installed_version' => isset($installedVersions["{$module}_"]) ? $installedVersions["{$module}_"] : 0,
  443. 'latest_version' => intval(substr(current($mod['migrations']), 0, 3), 10),
  444. 'migrations' => $mod['migrations'],
  445. );
  446. }
  447. return $modVersions;
  448. }
  449. /**
  450. * Get the connection string for MongoDB
  451. *
  452. * @param string $hostname The server hostname
  453. * @param string $port The server port on which MongoDB accepts connections
  454. * @param string $username User name
  455. * @param string $password Password
  456. * @param string $db_name The name of the database to connect to
  457. *
  458. * @return string The connection string used to connect to the database
  459. */
  460. private function getMongoConnectionString($hostname, $port = '', $username = '', $password = '', $db_name = '')
  461. {
  462. $connect_string = 'mongodb://';
  463. if (! empty($username) && ! empty($password)) {
  464. $connect_string .= "{$username}:{$password}@";
  465. }
  466. $connect_string .= $hostname;
  467. if (! empty($port)) {
  468. $connect_string .= ":{$port}";
  469. }
  470. if (! empty($db_name)) {
  471. $connect_string .= "/{$db_name}";
  472. }
  473. return trim($connect_string);
  474. }
  475. /**
  476. * Get the connection string for PostgreSQL
  477. *
  478. * @param string $hostname The server hostname
  479. * @param string $port The server port on which PostgreSQL accepts connections
  480. * @param string $username User name
  481. * @param string $password Password
  482. * @param string $db_name The name of the database to connect to
  483. *
  484. * @return string The connection string used to connect to the database
  485. */
  486. private function getPostgreConnectionString($hostname, $port = '', $username = '', $password = '', $db_name = '')
  487. {
  488. $connect_string = '';
  489. $components = array(
  490. 'host' => $hostname,
  491. 'port' => $port,
  492. 'dbname' => $db_name,
  493. 'user' => $username,
  494. 'password' => $password,
  495. );
  496. foreach ($components as $key => $val) {
  497. if (! empty($val)) {
  498. $connect_string .= " {$key}={$val}";
  499. }
  500. }
  501. return trim($connect_string);
  502. }
  503. /**
  504. * Get the connection string for Oracle (10g+ EasyConnect string)
  505. *
  506. * Note: 10g can also accept a service name (//$hostname:$port/$service_name)
  507. * 11g can also accept a server type and instance name (//$hostname:$port/$service_name:$server_type/$instance_name)
  508. * We don't currently support these options, though
  509. *
  510. * @param string $hostname The server hostname
  511. * @param string $port The server port on which Oracle accepts connections
  512. *
  513. * @return string The connection string used to connect to the database
  514. */
  515. private function getOracleConnectionString($hostname, $port = '')
  516. {
  517. $connect_string = '//' . $hostname;
  518. if (! empty($port)) {
  519. $connect_string .= ":{$port}";
  520. }
  521. return $connect_string;
  522. }
  523. /**
  524. * Stub method to handle ODBC Connection strings.
  525. *
  526. * Currently, the user will have to either setup a DSN connection and input the DSN name
  527. * or input the DSN-less connection string into the hostname field
  528. *
  529. * @param string $hostname The DSN name or DSN-less connection string
  530. *
  531. * @return string The connection string used to connect to the database
  532. */
  533. private function getOdbcConnectionString($hostname)
  534. {
  535. return $hostname;
  536. }
  537. /**
  538. * Stub method to handle PDO Connection strings.
  539. *
  540. * Currently, the user will have to enter the PDO driver as part of the hostname,
  541. * e.g. mysql:host
  542. *
  543. * @param string $hostname The driver and hostname (separated by a colon) or DSN
  544. * @param string $db_name The name of the database, if not specified in $hostname
  545. *
  546. * @return string The connection string used to connect to the database
  547. */
  548. private function getPdoConnectionString($hostname, $db_name = '')
  549. {
  550. $connect_string = $hostname;
  551. if (! empty($db_name)) {
  552. $connect_string .= ";dbname={$db_name}";
  553. }
  554. return $connect_string;
  555. }
  556. /**
  557. * Stub method to handle SQLSrv connection strings
  558. *
  559. * @param string $username User name
  560. * @param string $password Password
  561. * @param string $db_name The name of the database
  562. * @param string $char_set Character set
  563. * @param bool $pooling Connection pooling
  564. *
  565. * @return array The connection settings array used to connect to the database
  566. */
  567. private function getSqlsrvConnection($username, $password, $db_name, $char_set = 'UTF-8', $pooling = false)
  568. {
  569. $character_set = 0 === strcasecmp('utf8', $char_set) ? 'UTF-8' : $char_set;
  570. $connection = array(
  571. 'UID' => empty($username) ? '' : $username,
  572. 'PWD' => empty($password) ? '' : $password,
  573. 'Database' => $db_name,
  574. 'ConnectionPooling' => $pooling ? 1 : 0,
  575. 'CharacterSet' => $character_set,
  576. 'ReturnDatesAsStrings' => 1,
  577. );
  578. return $connection;
  579. }
  580. }