Apis Networks

One-Click Framework

 
Imagine a unified framework for installing, upgrading, and removing software packages. What if we could universally represent every package installation with three methods: install, patch, and configure? Gallery, Joomla!, RoundCube, SugarCRM, WordPress, you name it, they all have three steps in common, a pre-install configuration stage, installation, and post-install, upgrade stage. We wrote a framework to express these three fundamental stages and wrapped it into our own package called the One-Click Facility.

Installers sampled from popular control panels such as cPanel and Plesk share a chaotic setup routine without a shred of common ground between packages. What you see is a smorgasbord of commands strewn about the tapestry of code; it's not art, it is finding a way to make ends meet. apnscp esprit attempts to unify package installation and management in a clear, concise way that is easily extendable by the average user with a bit of PHP knowledge.
 
This feature has been frozen to allow us to focus on improving other aspects of the control panel. There is no ETA when this component will return.

A basic example

<?php
    /**
     * @class lucida_Install
     * @author Matt Saladna <msaladna@apisnetworks.com>
     *
     * Provides basic hooks for the Lucida application in esprit's one-click
     * installation system
     */
    final class lucida_Install extends One_Click_Generic {
 
        public function __construct($mApp) {

            parent::__construct($mApp);
        }
 
        /**
          * Pre configuration hooks.  This function is reserved for
          * ensuring that the user has the necessary features enabled for
          * the account in order to install the application fine.
          *
          * Use:
          * return false;
          * at the end of the function to prohibit the user from configuring
          * or installing the application.
          *
          * return true;
          * allows the user to proceed with configuration of the application.
          *
          * Example:
          *
          * if (!$this->sql_service_enabled('mysql'))
          *     return false; // MySQL is not enabled for the account
          *
          * Alternatively you can be a bit more verbose by returning an Exception
          * if (!$this->sql_service_enabled('mysql'))
          *     return new PermissionError("MySQL not enabled for account");
          */
        public function pre_configuration() {

            /**
             * Set our configuration options
             */
            $this->configuration_options =
                    array('showfooter'    => array('label' => 'Show footer',
                                                   'type' => bool,
                                                   'default' => true),
                          'debug'         => array('label' => 'Enable debugging',
                                                   'type' => bool,
                                                   'default' => false),
                          'title'         => array('label' => 'Album title',
                                                   'minlength' => 1,
                                                   'constraints' => OPT_REQUIRED,
                                                   'default' => 'Photos',
                                                   'type'    => string),
                          'adminuser'     => array('label' => 'Administrative username',
                                                   'type' => string,
                                                   'minlength' => 3,
                                                   'maxlength' => 12,
                                                   'constraints' => OPT_REQUIRED,
                                                   'help' => 'Administrative user to access the account',
                                                   'regex' => '!^[a-z0-9A-Z_-]+$!'),
                          'adminpassword' => array('label' => 'Administrative password',
                                                   'type' => string, 'default' => '',
                                                   'minlength' => 3, 'maxlength' => 12,
                                                   'constraints' => OPT_REQUIRED,
                                                   'desc' => 'Administrative password',
                                                   'regex' => '!^[a-z0-9A-Z_-]+$!'),
                          'defaultres'    => array('label' => 'Default photo resolution',
                                                   'type' => enum,
                                                   'default' => '800x600',
                                                   'values' => array('original', '640x480',
                                                                     '800x600',  '1024x768',
                                                                     '1280x1024','1600x1200'),
                                                   'help' => 'This is the default resolution that all photos will be displayed as.'));
            return true;
        }

 
        /**
         * Validate input.
         * Return a value of false to force the user to reconfigure.
         *
         * Return an exception of type ArgumentError to let the user know
         * what options are invalid.  Example:
         *
         * if (!preg_match('/^[a-z]+$/',$password))
         *      return new ArgumentError(array("password" => "Invalid password"));
         */
        public function post_configuration() {

            return true;
        }
 
        /**
         * Ensure that the filesystem meets the needs of the application.
         */
        public function pre_install() {

            return true;
        }
 
        /**
         * Perform the application installation process
         *
         * This includes copying files into the system and updating any databases
         */
        public function install() {

            /**
             * void parent::log_status(string $mMsg)
             * Writes a string to the status file displayed in the browser
             * instantaneously.  Useful for notifying the user of the progress
             * of the install.
             *
             * NOTE: Broken in IE; IE doesn't handle the interactive state correctly
             * and only displays data once the XMLHTTP instance hits state 4 (completed).
             */
            $this->log_status("Copying files...");
 
            /**
             * bool parent::copy_storehouse(string $mSrc, string $mDest)
             *
             * Recursively copy the storehouse install to the destination.
             * Actual paths are prepended to the $mSrc and $mDest variables,
             * thus "/www/" would refer to the directory under
             * var/storehouse/lucida/0.1/ in esprit.
             *
             * The file structure looks a bit like:
             *
             *  [storehouse/lucida/0.1]# ls -l
             *  total 44
             *  -r--r--r--    1 nobody   nobody        15423 Mar 26  2004 LICENSE
             *  -r--r--r--    1 nobody   nobody         2901 Mar 26  2004 README
             *  -r--r--r--    1 nobody   nobody          150 Mar 26  2004 TODO
             *  drwxr-xr-x    5 nobody   nobody         4096 Jan  9 02:22 www/
             *
             *  The second parameter is based upon the input from the user, e.g.
             *  subdomain installation would put the distribution under
             *  /home/SUBDOMAIN/public_html/ or /var/subdomain/SUBDOMAIN/ or
             *  /var/www/html/DIRECTORY/
             */
 

            /** Copy files over ... */
            try {
                $this->copy_storehouse("/www/", "/");
            } catch (Exception $e) {

                return $e;
            }
            /**
             * Now we'll make a temp file and write out the configuration.
             * Once that's done, copy the file to the application install
             *
             * int parent::get_site_id()
             * get_site_id() returns the site number of an account, which is
             * how it is stored on the filesystem.  When attempting to write
             * to files in a PHP script, the siteNN notation is required
             * as open_basedir is dumb in resolving the symlinks, i.e.:
             * /home/virtual/<DOMAIN>/ throws an error when trying to write to a
             * file... /home/virtual/site<NUM>/fst/ works fine.
             *
             * string parent::get_install_path()
             * Returns the chroot'd installation location for a script, e.g.
             * if a user elects to install it under the main document root with
             * a path value of "/myapplication", then the function will return
             * /var/www/html/myapplication
             *
             * If it's a subdomain, depending on whether it's a concrete subdomain,
             * such that the user created a new user with a subdomain or a
             * virtual subdomain -- the user created a new subdomain that links
             * to another directory -- then get_install_path() will return
             * that chroot'd path.
             */
            $tmpConfig = array ('photodir' => '/home/virtual/site'.$this->get_site_id().'/fst/'.
                                               ltrim($this->get_install_path(),'/'),
                                'cachedir' => '/home/virtual/site'.$this->get_site_id().'/fst/'.
                                               ltrim($this->get_install_path(),'/').'/data/cache',
                                'extensions' => 'bmp,gif,jng,jp2,jpc,jpeg,jpg,pcd,png,psd,tga,tif,'.
                                                'tiff,wmf,xcf,PNG,JPG,JPEG,GIF',
                                'defaultres' => $this->postback_options['defaultres'],
                                'validreslist' => 'original,640x480,800x600,1024x768,1280x1024,1600x1200',
                                'resizequality' => '75',
                                'debug' => (bool)$this->postback_options['debug'],
                                'adminuser' => $this->postback_options['adminuser'],
                                'adminpass' => $this->postback_options['adminpassword'],
                                'title' => $this->postback_options['title'],
                                'style' => 'lib/styles/lucida.css',
                                'gallerypreviews' => true,
                                'displayexifdata' => true,
                                'customexifstring' => '<b>EXIF Data:</b>

 
                              %[EXIF:*]',
                                'showfooter' => $this->postback_options['showfooter'],
                                'showbenchmark' => true,
                                'footertext' => 'Copyright &copy; 2004. All rights reserved.<br />

                              Images and content may not be distributed or reproduced without permission.',
                              );
            /**
             * Lucida stores its configuration in a serialized text file with
             * <?exit;?> at the beginning of the script.  We will replicate
             * this behavior by serializing its internal data structure
             * and then writing it to the file
             */
            $lucidaConfig = '<?exit;?>'.serialize($tmpConfig);
            $this->file_put_file_contents($this->get_install_path().'/data/galleries.lucida.php',
                                          /** Data must be base64 encoded */

                                          base64_encode($lucidaConfig),
                                          !file_exists('/home/virtual/'.$this->domain.'/'.
                                                       $this->get_install_path().
                                                       '/data/galleries.lucida.php'));
            try {

                /**
                 * Directory might exist if overriding an install, so let's catch the
                 * error is silently discard it.
                 */
                $this->file_create_directory($this->get_install_path().'/data/cache');
            } catch (FileError $e) { /** do nothing */ }

            /**
             * parent::fix_apache_perms(string $mPath, bool $mRecurse)
             * Call fix_apache_dir(string) on any directory that a PHP script
             * will write to.  This function does two things, first it sets the
             * sticky setgid bit on the directory to ensure all files created
             * within there will be under the site's quota.
             *
             * Secondly, it changes the owner to apache, allowing the Web server
             * to write to the file and then uses ACLs
             * {@link http://acl.bestbits.at/} to permit the main user to delete
             * files from the directory.
             */
 
            $this->fix_apache_perms('/data/cache/',true);
            $this->log_status("Done!");
            return true;
        }

 
        /**
         * Clean-up any remaining files, reinitialize variables, et cetera
         */
        public function post_install() {
            /**
             * We have nothing further to do, so just return true
             */
            return true;
        }

 
        /**
         * Double check files exist prior to patch
         */
        public function pre_patch() {
            return true;
        }

 
        /**
         * Call diff command, patch application, do anything else...
         */
        public function patch() {
            return true;
        }

 
        /**
         * Post-patch cleanup
         */
        public function post_patch() {
            return true;
        }

    }
?>
 
Lucida is always used as an example due to its simplicity. In two simple steps we present the user with configuration options, generated from lucida_Install::pre_configuration, then proceed with installation in lucida_Install::install. Our configuration options are derived from another class called Configuration_Driver. This presents the users with a set of form input objects (SELECT, INPUT, CHECKBOX, RADIO) and optional constraints. Installation? Well, you guessed it. Copy the files using a built-in esprit method, File_Module::copy_storehouse_backend and write the configuration file. All of this is built around esprit's API, which is easily accessible from your desktop.
 
Upgrades, not featured in our example, because Lucida never received a newer version, utilize patch to patch only the changed files. A backup of all modified files is created just in case you have performed heavy modifications to the software. Having an undo button is important and that's why we thought about it ahead of time, to save you the grief of an errant click. It's forward thinking that is what makes Apis unique.

Currently esprit serves one-clicks for the following applications:
For more information please be sure to see our wiki entry, Writing One-Clicks.
Black Friday Sale Save 50% use promo code BF2014 at checkout Offer valid only for new accounts. Excludes domain registrations. Discount valid for the life of the account. May not be combined with other offers. Offer expires November 30, 2014 at 11:59 PM PST.