Search Blog

Author: Joe Meyer
Date: May 22, 2014

Goal: To upload a file to the web server, using HTML and PHP

Difficulty: Easy

Prerequisites: A good grasp on HTML forms, and a working knowledge of PHP is helpful for understanding the guide, but not necessary to get a working product (simply use the last code snippit).

There are many scripts available on the web to assist with uploading files to a server using a web browser. However, it isn't difficult to create a simple user interface that allows someone to upload files using a simple HTML form and PHP script.

In this example, I am going to use an HTML form, and it will submit to the same page. Lets start our file. We'll call it FileUpload.php.

FileUpload.php:

<form action="FileUpload.php" method="post" enctype="multipart/form-data">
    <label for="upload">File:</label>
    <input type="file" name="upload" id="upload"><br/>
    <input type="submit" name="submit" value="Upload">
</form>

 

If we look at the first line in the body, we see our typical html form construct. The action is indicating that we'll submit this form to FileUpload.php (aka this same file), we are using a post request, and the really important part, enctype="multipart/form-data is indicating that this form may contain binary data, which is crucial to allow for the file upload to be able to take place.
Now that we have our form, we can build a little PHP logic into this file to indicate whether or not we just submitted the form (and we'll indicate that the file was uploaded), or if we are waiting on the user to submit (in which case we'll show them the form we just built). It will look something like this:

<?php
    if (isset($_FILES['upload'])) {
        //todo: handle the uploaded file
        echo "Your file was uploaded successfully";
    } else {
    ?>
        <form action="FileUpload.php" method="post" enctype="multipart/form-data">
            <label for="upload">File:</label>
            <input type="file" name="upload" id="upload"><br/>
            <input type="submit" name="submit" value="Upload">
        </form>
    <?php
    }
?>

 

So the reason we're doing this check is because if they submitted a file, I didn't want to show them the form again. If you were submitting your file to a different page obviously you wouldn't need any of this logic, but since I kept it contained in a single file I needed to come up with the different use cases. So in order to do that I simply checked if the upload was submitted to the page or not by using the isset($_POST['upload']) check (to clarify we use "upload" because it is the name attribute on our file input). Now, we need to actually handle the file upload. Right now it's actually being placed in the web servers temporary file storage location which is defined in the php.ini file on the upload_tmp_dir setting. You can retrieve this location by using the function sys_get_temp_dir() for the generic directory, and if you want to know the file location you can use $_FILES['upload']['tmp_name'] (where upload is your form field name).

Moving on to handling the uploaded file. At this point, we're uploading a file, and it's stored in a temporary location on the server. This brings us great joy that we are able to upload files, but it's all for naught if we don't move the file to a more permanent location on disk. So lets get to it:

<?php
    if (isset($_FILES['upload'])) {
        $uploadDir = '/var/www/uploads/'; //path you wish to store you uploaded files
        $uploadedFile = $uploadDir . basename($_FILES['upload']['name']);
        if(move_uploaded_file($_FILES['upload']['tmp_name'], $uploadedFile)) {
            echo 'File was uploaded successfully.';
        } else {
            echo 'There was a problem saving the uploaded file';
        }
        echo '<br/><a href="FileUpload.php">Back to Uploader</a>';
    } else {
    ?>
        <form action="FileUpload.php" method="post" enctype="multipart/form-data">
            <label for="upload">File:</label>
            <input type="file" name="upload" id="upload"><br/>
            <input type="submit" name="submit" value="Upload">
            </form>
        <?php
    }
?>

 

At last, we have a working file uploader, that takes an uploaded file, and moves it over to our specified upload directory. This script should get most novices to file uploading started. There are of course many improvements that could be made on the script, and I'll list some of these considerations below but it'll be up to you to implement the code.

Additional Considerations & Tips for uploading files with PHP

  • PHP maximum upload size: php.ini settings will cause PHP to throw warings/errors when the limits are exceeded. These limit settings that should be adjusted are upload_max_filesize and post_max_size
  • Client side file size validation: This can be important to do, mainly because in the event where a file is to large you will have to first waste the users time as they upload a file which then gets rejected by the server, and to top that off they may end up with an ugly non-user friendly message on the screen.
  • Catching files that exceed the upload limits: can often be done in php by using a hidden input field with a name of MAX_FILE_SIZE which has a value that is equal to the number of bytes that the maximum upload size is. While this can be bypassed by a malicious user, those are the one's who you typically don't care if they see an ugly error message when the server finally gets around to handling it.
  • $_FILES['fieldName']['tmp_name'] is blank: - This can happen if your upload limits aren't set up to handle the size of the file you are trying to upload. Check the PHP.ini file settings again.
  • Other File Attributes: Some other $_FILE['fieldName'] attributes you might find useful include (taken from php.net):
    • $_FILES['fieldName']['name'] - The original name of the file when it was uploaded.
    • $_FILES['fieldName']['type'] - The mime type of the file, if the browser was able to provide this information. An example of this would be image/gif. This mime type is not checked server side so it may be better to implement your own mime type function to retrieve this info based on the file extension.
    • $_FILES['fieldName']['size'] - The size, in bytes, of the uploaded file.
    • $_FILES['fieldName']['tmp_name'] - The temporary path and filename where the uploaded file was stored on the server.
    • $_FILES['fieldName']['error'] - The error code associated with this file upload.
  • Paths must exists ahead of time: - Fair warning that the directory you are trying to move your upload to has to exist before you try and move the file there. This can often be done using the mkdir command.

Author: Joe Meyer
Date: May 7, 2014

Goal: Redirect to another URL using a PHP script

Difficulty: Easy

Prerequisites: A working PHP installation.

Have you ever needed to change the URL of one of your existing php pages? Perhaps you are looking for a way to redirect to another URL after someone has successfully logged in or logged out, or maybe some other kind of logic based url redirection. With PHP there are certainly ways to accomplish these things.

One way that we can achieve an immediate redirection is using the PHP header() function (Documentation here). This allows you to perform an immediate redirect. This function when used properly has the advantage of navigating to a url specified in your script, without any user interaction having to take place. Here's an example:

<?php
header('Location: http://example.com/redirecthere.php');
exit();

You may notice that I also included the exit(); below the header function, and you may be wondering if this is necessary. The short answer is no, it isn't required. However, best practices would dictate you do so and here's why. The header essentially sends a 302 redirect code back to whatever requested the page, however the script that is outputting this request will keep running until it has finished before it sends this 302. Why is this a concern? Well, perhaps your redirect was part of a conditional statement, and you wanted to redirect the user if they didn't have access to a page, but otherwise you would show a list of privileged information. Perhaps it looks something like this:

<?php
if(!$user->hasPermission()) {
    header('Location: http://example.com/redirecthere.php');
}

print_r($socialSecurityNumbers);

Now, you might think, well that looks ok, because if they don't have permission they will get redirected, right? WRONG! The php script will continue to execute all the way through, and it will include whatever that $socialSecurityNumbers variable had in it in the response. Now, most users will never notice this, because they will be immediately redirected and never even see the information display to the screen, but this is only because that is the behavior of most browsers, not because of your programming. This is why any time you are setting the location header, you should take special care to stop your script from executing any further.

Many frameworks already have a function built in that does this for you, if you are building your own scripts without a framework, I often like to write a nice little php redirect function that looks something like this:

<?php
function redirect($url) { header('Location: '.$url); exit(); } redirect('http://example.com'); //redirects to example.com and exits the script

Please note that this is a header request, meaning that if you already have something sitting in the output buffer, this may give you a warning of "Warning: Cannot modify header information". If you decide that you want to simply discard anything in the output buffer you can use the ob_clean(); function just prior to your header() function call. However, if you decide that you really want to send the user whatever was in the output buffer, you should use ob_flush() instead of ob_clean(). Documentation on the ob_ methods can be found on PHP.net.

Author: Joe Meyer
Date: April 25, 2014

For people unfamiliar with Object Oriented Programming (OOP), using classes / objects can be a bit of an endeavor your first time through. However, OOP can be extremely powerful in making your code dynamic and helping to minimize your efforts when developing. It's also an excellent method for sort of lumping your code into categories that make sense.

One of the most common mistakes I see, is that people use $this when they should really be using self::. To understand when we should use each, lets create an example.

 

So lets say that in some PHP script, I want to be able to create some "vehicle" objects. I do this because, I want an easy way to keep track of how many tires each vehicle has, what color paint it has, and I want a function that builds me an array of cars. So my class might look something like this:

<?php
class Vehicle {
    public $numberOfTires = 0;
    public $paintColor = null;

    public function __construct($tires, $color)
    {
        $this->numberOfTires = $tires;
        $this->paintColor = $color;
    }

    public function paint($color)
    {
        $this->paintColor = $color;
    }

    public static function buildCars($amount)
    {
        $cars = array();
        for($i = 0; $i < $amount; $i++)
        {
            $color = self::getRandomVehicleColor();
            $cars[] = new Vehicle(4, $color);
        }

        return $cars;
    }

    public static function getRandomVehicleColor()
    {
        $colors = array('black', 'blue', 'red');
        return $colors[mt_rand(0, count($colors)-1)];
    }
}

 

So now let's break down what all of that actually does. First we have our __constructor function. This is a kind of magic PHP function that is in every class. Even if you don't define it, it is there, just empty. This is what gets called when you use new Vehicle(4, 'black');. Notice, that inside of the constructor we are setting those public variables, defined at the top of the class typically. This is essentially creating our vehicle object. So that way in our main script we can do things like this:

<?php
$myCar = new Vehicle(4, 'black');
$myBicycle = new Vehicle(2, 'red');

echo $myBicycle->numberOfTires; //echo's the number 2
echo $myCar->paintColor; //echo's the string 'black'

 

So you noticed that when i used $this inside of the class, I was referring to that specific instance of the object, in this case vehicle. For added re-enforcement I created the paint() function so that I can show you how you can leverage this even further. Say you created your object of $myCar, but now you want to repaint your car. That's easy enough to do by creating a function (albeit a very simple one) and then calling that function on the object you want it to apply to. Like this:

<?php
$myCar = new Vehicle(4, 'black');
echo $myCar->paintColor; //echo's the string 'black'
$myCar->paint('blue');
echo $myCar->paintColor; //echo's the string 'blue' now

 

So we've covered that $this is used in order to apply functions to the instance of the object. But now we want a function to create us a bunch of instances. This is something that I would still classify as being related to the object, so it makes good sense for me to create a function inside of that class to do that for me. In this case, we want to create us a bunch of cars, which are vehicles with 4 tires. We'll do this by calling a static function we built in our class.

<?php
$cars = Vehicle::buildCars(25); //return an array of 25 vehicle objects;

 

So, now to talk about self:: You'll notice that inside of our buildCars() function we use self:: to call the getRandomVehicleColor() function. Notice that this is also a static function, in other words, I don't actually have to have a vehicle instance, to pick a random vehicle color. These are the instances when you use self and not $this. Bottom line, $this is referencing the current instance of the object and is applied to non-static functions, self:: is for static functions that are not dependent on having an instance of the object to use or if you want to call a function from the creating class.

Author: Joe Meyer
Date: April 19, 2014

Goal: To update Concrete5 using the Concrete5 built in automated updater

Difficulty: Easy

Prerequisites: Your PHP settings must support CURL requests. This guide assumes that you are an administrator of the concrete5 website you are trying to update. While no FTP access is directly required for the upgrade, in the event your upgrade fails you will need access to directly modify files. 

Note: These screenshots were taking from an instance of concrete5 running on version 5.5. If you are using Concrete5.4 or older the administration interface may appear drastically different and may require you to go through other upgrade steps before being able to update your site. Also, please note that recovering from a failed backup is not included in this tutorial, however it does prepare you for that scenario.

Introduction

One question we never hear people stop asking is "Should I upgrade my Concrete5 website to the latest version?" Our answer is almost always "It depends". We typically recommend that if it is feasible, you stay on the most current release that has been available for at least 3 weeks. Why this 3 week waiting period? Unfortunately, nobody is perfect, and this includes the developers working on the Concrete5 core. There have been countless times where the core has become corrupted or something was broken due to a human programming error, and that can cause your site to break. Waiting for a release to become a little more stable than bleeding edge is generally what we recommend to those clients of ours who don't have programming expertise to fix a bug themselves when it occurs. Typically, if a release has been available for 2-3 weeks and no new release has followed, I would consider that pretty stable. On the other hand, if you are having issues with your current version of Concrete5 that you know was fixed in a release, it may be beneficial for you to upgrade right away.

Regardless, we do think that keeping your site up to date and doing at least annual upgrades is a good idea. This helps to ensure that going forward you won't have to go through several iterations of upgrades in one shot. This is a bad thing to do because then if something does break, and you don't discover it right away, you have to try and deduce what actually caused the issue, was it the first upgrade? the second? the third? So there is a bit of a balancing act here with waiting and upgrading. If you have questions you can certainly reach out to our development staff or the Concrete5 community for expert advice or opinions.

Step One: Backing Up Your Concrete5 Website

I cannot stress enough how important it is to back up your Concrete5 website prior to doing any updates (including when you do add-on updates). While 99% of the time Concrete5 upgrades without any problems, there's always that 1% of the time where you find out that the database server crashed during the upgrade, or PHP ran out of memory halfway through, or perhaps you had a developer who thought they knew what they were doing and ended up modifying the Concrete5 core files, breaking any upgrade ability you might have had. Several of these reasons often happen on shared hosting due to lack of resources or conservative settings.

  1. The first step of backing up your Concrete5 website is to access the system section of the dashboard. This can be done by going to http://www.example.com/index.php/dashboard/system/. It can also be done by hovering over the "Dashboard" icon in the toolbar, then selecting the "System & Settings".
    C5 Dashboard Systems & Settings

  2. From the Systems & Settings page we now want to look under the "Backup & Restore" section, and select "Backup Database". You can access the backup database by using the following URL directly: http://www.example.com/index.php/dashboard/system/backup_restore/backup/
    Concrete5 Dashboard Systems & Settings - Backup Database

  3. Next click the "Run Backup" button.
    Concrete5 Systems & Settings Run Backup
    Note: As stated int he important information section, you should not keep these backups any longer than necessary. Having .sql backup files in your web root is generally a bad idea in general due to possible security implications. Also, these files can take up a lot of disk space depending on the size of your website, adding to your site backup times and possibly exhausting your disk quota with your web host.

  4. Once this completes you should now have your backup listed in the "Existing Backups" section.

Previously I stated that you do not really need to back up your file system, this is because Concrete5 doesn't overwrite any "core" files when the upgrade happens. However, it does modify your files in the /config/ folder on the root of your site. It may not be a bad idea to back these files up, since in the event you need to revert your site you would essentially just need to restore your database and the files in that directory. That said, if you don't it's usually not overly difficult to rebuild those configuration files assuming they weren't completely deleted.

Step Two: Downloading the Next Version

  1. Back on the Dashboard "Systems & Settings" page, you should select the "Update Concrete5" link. This can be accessed via http://www.example.com/index.php/dashboard/system/backup_restore/update/.
    Concrete5 Systems & Settings Update Concrete5

  2. This page may look slightly different to everyone depending on what version you are on, and what future versions are available. However, you should have a "Check for Updates" button and a "Download" button if updates are available. In this case we will select the "Download" button.
    Concrete5 Systems & Settings Download Update
    Note: Downloading is not the same as upgrading, you can download your files and come back later to do the upgrade. This is often a good idea if you don't have time to commit immediately to doing testing right after you do the upgrade, especially if your web server does not have a high amount of bandwidth available or you are testing from your home connection. If you do wait for a period of time after downloading the update, please make a new database backup just in case something changed.

    Also, on this page there are often release note highlights indicating what has changed and a link to more detailed notes. These are often a good thing to read through if you are considering moving to the most current version of Concrete5.

Step Three: Running the Updater

  1. Finally, we're to the updating part of things. After you downloaded the update, it should have redirected you to http://www.example.com/index.php/dashboard/system/backup_restore/update/. Which looks like the screenshot below. If not you can use the link posted above, or go back into the Systems & Settings page, and click on "Update Concrete5" again.
    Concrete5 Install Local Update

  2. Next, click the "Update" button showing on screen. Afterwards you should be presented with a screen saying that the update is completed. At this point, you should review your website to make sure everything is working as expected.
    Concrete5 Upgrade Complete

    Note: In the event that you received a PHP memory error or other odd error, you can re-run the update by going to http://www.example.com/index.php/tools/required/upgrade?force=1.

Step Four: Cleanup

Please make sure that post-upgrade you clear your cache. 

  1. This can be done by going to the "Systems & Settings" section of the Dashboard and clicking on "Clear Cache" option under the optimization.
    Concrete5 Dashboard Clear Cache

  2. Next you will be presented with a simple interface screen. Click the "Clear Cache" button and you're all set to start using your newly updated Concrete5.

  3. Once you are comfortable your install is working properly, go back to the "System & Settings" page and into the "Backup Database" page. Here you should make certain that you DELETE your backup. Alternatively, if you really want to keep this around, you should download the file, save it into some nice safe spot on your computer, and then delete it off the server.

Need Additional Help?

In the event that you need additional help with your update there are several resources available to you.

  • First and foremost the Concrete5 community. Create an account on www.concrete5.org and post to the forums with any problems you might run across. This helps alert the developers of problems people are having so that the product can be made better or more user friendly in the future.
  • Secondly, contact your web host. Often times the installer may error due to php not being configured with enough memory or perhaps it's not running the right version. These are things that your web host should be able to help you address.
  • Thirdly, please feel free to contact ExchangeCore's development team. I list us as the last option because while we know what we're doing, but we also come at a price and we hate to see you throw money away when there are other options. If you do need our help, we can be reached by visiting our Development Contact Page.

Author: Joe Meyer
Date: April 15, 2014

Goal: To install Concrete5 Manually on my cPanel web hosting account

Difficulty: Easy

Prerequisites: This guide shows you how to add files and databases using cPanel. If you are not on a hosting account where cPanel is available to you, you may have other alternatives such as FTP for file transfers and other database creation utilities. If you don't already have a web host, check out our hosting offers at our web hosting page to get started.

Concrete5 is a great user friendly content management system. Installing Concrete5 can be a bit intimidating if you don't have any experience installing websites and the hope is that by following this step by step guide, even someone who is completely unfamiliar with what a database is, or how to unzip a file, you will be able to get Concrete5 installed and off to working on your latest site. So let's get started...

Just one more quick note. Throughout this guide you see me using "c5help" in many areas. This variable simply happens to be the sub-domain I am installing to. You do not need to have any part of this when you set up your site.

Step One: Setting Up a Database

Concrete5 uses a MySQL database in order to store the dynamic content on your website. This means that you are going to need a MySQL database, a MySQL username, and a MySQL password. Here's how you will set those up if you are using a host that offers cPanel. Note: Your screen may look slightly different based on the cPanel theme you are using, the examples below were all taken from the x3 theme.

  1. Log in to your cPanel account. Usually this can be done by going to www.example.com/cpanel (where example.com is your domain name).
  2. Find the "Databases" group and click on "MySQL Databases"
    cPanel MySQL Databases

  3. Next create a short and meaningful database name. Usually, I try to make my database names resemble my website names in some way. This way it is easy to identify which database belongs to which site later on if I decide to add another to my account. Do this by entering the database name and clicking "Create Database".
    cPanel Create Database

  4. Next, you will want to create a MySQL user. I strongly recommend using the password generator and here is my reasoning: 1) The password generator will ensure you have a very strong password and 2) The password generator will help ensure that you are not re-using a password you used before. This second part is important since this password will be stored in clear text in your Concrete5 configuration file. This means that any developer or anyone with FTP access to your site has access to this username and password, and it would be horrible for them to have a password that you used elsewhere. Also, you only ever have to type this password when you install Concrete5 in a later step so there's no sense in making this password something memorable. Click "Create User" once you are finished with this section.
    cPanel create MySQL user

  5. Finally, we want to assign the MySQL user we just created to our MySQL database we just created. This is a pretty straightforward process of picking them from the select lists and clicking "Add".
    cPanel add mysql user to database
    Then we click the "All Privileges" checkbox and finally the "Make Changes" button.
    cPanel MySQL Account Maintenance

Step Two: Preparing Concrete5 Files for Installation

 This next section is all about setting up the necessary files and file security for Concrete5 to work. These files will work in tandem with the database once installed to give you the powerful CMS of Concrete5.

  1. The first step to this is to download a zip of the latest version of Concrete5. You can obtain a copy of this from http://www.concrete5.org/developers/downloads/. Then look for the heading of "Latest Stable Version" and click the download link.
    Concrete5 Download Page

  2. Once you have that downloaded go back to your cPanel home page and find the "File Manager". We will be using this to upload the zip file we just downloaded from Concrete5's website.
    cPanel File Manager

  3. At the Directory Selection prompt pick the "Document Root for:" and your website name there. Also, make sure that you have the "Show Hidden Files (dotfiles)." checkbox selected.
    cPanel File Manager Directory Selection

  4. Next, pick the "Upload" button from the upper left hand corner of the file manager.
    cPanel Upload File with File Manager

  5. A new window will open, select the "Choose File" button and pick your Concrete5 zip file you downloaded from their website. (Odds are this is probably in your downloads folder).
    cPanel File Uploader Choose File
    Once you have picked the file to be uploaded it will start uploading immediately and you will see an uploading indicator at the bottom of your browser window. Wait until this has finished to move on.
    cPanel File Uploader Status
  6. Once the status is finished (and it doesn't appear like the page is doing any more loading) you can close this browser window and go back to the file manager.
  7. Next, we need to extract the files using the file manager. Select the zip file, then click the "Extract" button in the upper right corner.
    cPanel File Manager Extract
    Choose to extract these files to the root of your website (this should be the default location if you picked the document root as instructed in the previous step). Once it's extracted it will give you some results screens and other stuff you can ignore.

  8. At this point you should now have an unzipped set of files. (You should have a folder named similar to your zip file, if so you can at this point delete the zip file).

  9. Go into the unzipped folder by either using the tree view on the left side of the file manager, or by double clicking on the folder. Inside it should appear to be the root of what a Concrete5 website looks like. (See screenshot below). Click the "Select all" checkbox at the top of the file list.
    Concrete5 File capture

  10. After you have selected all the files, we now need to move them up to the parent directory so that they are sitting at the root of your site. Do this by clicking the "Move File" option in the upper left hand corner.
    cPanel File Manager Move File

    Then remove the concrete folder from the path you want to move them to. In my case I changed the path from
    /public_html/c5help/concrete5.5.2.1
    to be the root of my site:
    /public_html/c5help

    cPanel File Manager Move File2

  11. Now change back to your site root directory (using the tree on the left or the Up One Level icon) and delete the old concrete5.x.x.x folder (DO NOT REMOVE THE "concrete" FOLDER)
  12. Next we need to change the permissions of the config folder and the files folder to make them writable. First click on the config folder, then select "Change Permissions" on the top menu bar (make sure only config is selected)
    cPanel File Manager Change Permissions Config
    For the config folder ensure the permissions are set to 755 (Shown below)
    cPanel File Manager Chmod Config

    Next do the same thing for the "files" directory except this time use 777 for permissions. (shown below)
    cPanel File Manager Chmod Files

  13. Next we need to create a .htaccess file for the site. To do this use the "New File" button in the upper left hand corner of the File Manager. 
    cPanel File Manager New File

    The new file .htaccess file should be placed on the root of your site.
    cPanel File Manager New .htaccess File

  14. Now we need to edit the contents of this .htaccess file. Do this by selecting the file, then selecting the "Edit" button near the top of your screen. Note: If you did not check the box to show hidden files when you opened the file manager, you will not see the .htaccess file appear even though it exists.

    Once you have the editor open please populate it with the contents below and save changes:

    DirectoryIndex index.php
    
    RewriteEngine On
    RewriteBase /
    
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    
    RewriteRule ^(.*)$ index.php/$1 [L]
    
    cPanel File Manager Edit .htaccess

  15. Now we are done setting up the Concrete5 files. The file manager can be closed.

 Step Three: The Install

Now we are finally ready to run the Concrete5 installer.

  1. Visit the path you installed your concrete5 site to. In this example I have put everything on the root of my subdomain so I will simply use that: http://c5help.exchangecore.com/ but you may have put yours on the root of your domain or possibly even in a sub folder such as www.example.com/concrete/. When you visit you should immediately be presented with a requirements page. If you have any requirements not met, you should contact your web host to find out if they are available to you.
    Concrete5 Requirements Check

  2. Click the "Continue to Installation" button.
  3. You are now on a page asking you for some information. Here's some info about each field for you:
    Site Name -  you should pick something that is less than 60 characters, since this will be used as part of the title on all of your pages and can have SEO implications.

    Administrator Information - This information is for setting up the super admin to the site. The password is something you can make up that should be easy for you to remember, since this is the account you will most likely use for making changes to the site.

    Database Information
    Server
    : Usually this will be "localhost" unless your web host is using a remote MySQL server. You will have to contact them if this is the case to get the appropriate MySQL server host name or IP address.

    MySQL Username: If you recall, way long ago back under the first section we created a MySQL user. That is what this should be. In my example this is exchange_c5help

    MySQL Password: Again, something we created back in the first section. This is the one and only time you really need to know that password you generated or created for the MySQL user. Once this step is done you can clear that notepad file or toss that post-it note.

    Database Name: Again, set up back in the first section. It just so happens that I named mine the same as my MySQL user, exchange_c5help. 

    Sample Content
    This part is entirely up to you. If you know what you're doing a blank site should suit you well. Otherwise, per C5's recommendation, I'll also recommend you start with the sample site.

    Here's a screenshot of what my final install page looks like:
    Concrete5 Install Page

  4. Click the "Install concrete5" button and wait for the installer to finish. Once it has (hopefully without error) you should be logged in as the admin and you can now start building your website!

Need Additional Help?

In the event that you need additional help with your installation there are several resources available to you.

  • First and foremost the Concrete5 community. Create an account on www.concrete5.org and post to the forums with any problems you might run across. This helps alert the developers of problems people are having so that the product can be made better or more user friendly in the future.
  • Secondly, contact your web host. Often times the installer may error due to php not being configured with enough memory or perhaps it's not running the right version. These are things that your web host should be able to help you address.
  • Thirdly, please feel free to contact ExchangeCore's development team. I list us as the last option because while we know what we're doing, we also come at a price and we hate to see you throw money away when there are other options. If you do need our help, we can be reached by visiting our Development Contact Page.

Author: Joe Meyer
Date: April 14, 2014

I had the need in one of my applications to take associative PHP arrays and generate CSV files for the users to download from them. More specifically, I was dumping data from my database to CSV files. However, another thing I wanted to avoid doing was that I did not want to save a file to the web server then have to delete it when the request was done.

This function takes in an array of associative arrays (associative arrays must contain all the same keys) and outputs it to a CSV file using the first row's key values as the headers for the CSV file.

The function looks something like this: 

/**
 * Takes in a filename and an array associative data array and outputs a csv file
 * @param string $fileName
 * @param array $assocDataArray     
 */
public function outputCsv($fileName, $assocDataArray)
{
    ob_clean();
    header('Pragma: public');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header('Cache-Control: private', false);
    header('Content-Type: text/csv');
    header('Content-Disposition: attachment;filename=' . $fileName);    
    if(isset($assocDataArray['0'])){
        $fp = fopen('php://output', 'w');
        fputcsv($fp, array_keys($assocDataArray['0']));
        foreach($assocDataArray AS $values){
            fputcsv($fp, $values);
        }
        fclose($fp);
    }
    ob_flush();
}

And here's an example of the function call in use:

$data = array(
    array( 'item' => 'Server', 'cost' => 10000, 'approved by' => 'Joe'),
    array( 'item' => 'Mt Dew', 'cost' => 1.25, 'approved by' => 'John')
    array( 'item' => 'IntelliJ IDEA', 'cost' => 500, 'approved by' => 'James'),
);

outputCsv('expenses.csv', $data);

Hope you guys find this helpful. I know it has become one of my most universal scripts that I put in my applications and it also doubles as an excellent debugging tool for var dumping large arrays into an easy format to read, search, and sort.

Author: Joe Meyer
Date: January 22, 2014

Goal: Create a Login page that works with Active Directory using the Yii Framework

Difficulty: Medium

Prerequisites: Yii Framework, Basic understanding of Yii Configurations, Basic PHP Knowledge is helpful

 

Yii is a very powerful and extendable framework. In a many corporate environments where Active Directory is used as the primary means of authentication, it makes sense to integrate that same authentication form into your website. This helps to eliminate the need to maintain another username/password set, makes enforcing password policies a breeze, and makes life a whole lot easier in general.

The scripts below are relatively simple, and yet relatively robust for the small amount of time it takes to implement them. This method of integrating active directory with your Yii php instance supports restricting login to a specific organizational units or domains. It allows you to specify multiple OUs or Domains in case you want to allow users from one domain in OU Users to join and users from another domain. This PHP script also supports naming multiple domain controllers such that if one goes offline it will fail over to the next one in the list.

So lets get started implementing. The first thing we'll do is to create our UserIdentity file. This is how you do authentication logic for logging in with Yii. This file you can copy and paste and you should not need to make any changes to this file directly. This is where all of the LDAP stuff actually happens, so for those of you finding this just looking for a way to authenticate in PHP this file is probably the only one you really care about (and maybe a bit of the configuration file).

/protected/components/UserIdentity.php

<?php

/**
 * UserIdentity represents the data needed to identity a user.
 * It contains the authentication method that checks if the provided
 * data can identity the user.
 */
class UserIdentity extends CUserIdentity
{

    const ERROR_NO_DOMAIN_CONTROLLER_AVAILABLE = 1001; // could not bind anonymously to any domain controllers
    const ERROR_INVALID_CREDENTIALS = 1002; // could not bind with user's credentials
    const ERROR_NOT_PERMITTED = 1003; //user was not found in search criteria

    private $_options;

    private $_domain;
    private $_email;
    private $_firstName;
    private $_lastName;
    private $_securityGroups;

    private $_loginEmail = false;


    public function __construct($username = null,$password = null)
    {
        $this->_options = Yii::app()->params['ldap'];

        $this->username = $username;
        $this->password = $password;

        if(strpos($username,'@') !== false){
            $this->_loginEmail = $username;
            $exploded = explode('@',$username);
            $this->username = $exploded[0];
        }

        $slashPos = strpos($this->username, "\\");
        if($slashPos !== false){
            $this->username = substr($this->username, $slashPos+1);
            $this->_domain = substr($this->username, 0, $slashPos);
        }else{
            $this->_domain = $this->_options['defaultDomain'];
        }
    }

	public function authenticate()
	{
        $this->errorCode = self::ERROR_NONE;
        if($this->username != '' && $this->password != ''){

            $bind = false;
            $connected = false;
            $ldap = false;

            //connect to the first available domain controller in our list
            foreach($this->_options['servers'] AS $server){
                $ldap = ldap_connect($server);
                if($ldap !== false){
                    ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
                    ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
                    $connected = @ldap_bind($ldap); //test if we can connect to ldap using anonymous bind
                    if($connected){
                        if($this->_loginEmail === false){
                            $ldaprdn = $this->_domain . "\\" . $this->username;
                        }else{
                            $ldaprdn = $this->_loginEmail;
                        }
                        $bind = @ldap_bind($ldap, $ldaprdn, $this->password);
                        break; //we connected to one successfully
                    }
                }
            }

            //were we able to connect to any domain controller?
            if(!$connected){
                $this->errorCode = self::ERROR_NO_DOMAIN_CONTROLLER_AVAILABLE;
            }else{
                //were we able to authenticate to a domain controller as our user?
                if ($bind) {
                    //if we can bind to active directory we must have valid AD credentials
                    $filter='(sAMAccountName='.$this->username.')';

                    $conn=array();
                    for($i = 0; $i < count($this->_options['search']); $i++){
                        $conn[] = $ldap;
                    }
                    $results = ldap_search($conn,$this->_options['search'],$filter);
                    $foundInSearch = false;
                    foreach($results AS $result){
                        $info = ldap_get_entries($ldap, $result);
                        if($info['count'] > 0){
                            $this->_firstName = (isset($info['0']['givenname']['0']))?($info['0']['givenname']['0']):('');
                            $this->_lastName = (isset($info['0']['sn']['0']))?($info['0']['sn']['0']):('');
                            $this->_email = (isset($info['0']['mail']['0']))?($info['0']['mail']['0']):('');

                            $this->_securityGroups = array();
                            foreach($info['0']['memberof'] AS $sg){
                                preg_match('/CN=(.*?),/',$sg, $matches);
                                if(isset($matches[1])){
                                    $this->_securityGroups[] = $matches['1'];
                                }
                            }
                            sort($this->_securityGroups);

                            $foundInSearch = true;
                            break;
                        }
                    }

                    if(!$foundInSearch){
                        $this->errorCode = self::ERROR_NOT_PERMITTED;
                    }
                }else{
                    //if we can't bind to active directory it means that the username / password was invalid
                    $this->errorCode = self::ERROR_INVALID_CREDENTIALS;
                }
            }
        }else{
            //if username or password is blank don't even try to authenticate
            $this->errorCode = self::ERROR_INVALID_CREDENTIALS;
        }

        switch($this->errorCode){
            case self::ERROR_INVALID_CREDENTIALS :
                $this->errorMessage = 'Invalid Credentials.';
                break;
            case self::ERROR_NO_DOMAIN_CONTROLLER_AVAILABLE :
                $this->errorMessage = 'No domain controller available.';
                break;
            case self::ERROR_NOT_PERMITTED:
                $this->errorMessage = 'Not permitted in application.';
                break;
            case self::ERROR_NONE :
                $this->setState('firstName', $this->_firstName);
                $this->setState('lastName', $this->_lastName);
                $this->setState('email', $this->_email);
                $this->setState('adSecurityGroups', $this->_securityGroups);
                break;
            default : $this->errorMessage = 'Unable to Authenticate';
        }


		return !$this->errorCode;
	}

    public function getName(){
        return $this->_firstName.' '.$this->_lastName;
    }
}

Now we need to create the validation logic for the forms. In Yii we can do some pretty fancy stuff using the models so I'll use that here. Keep in mind though that you could just as easily leave out the model and build your form manually with the view files and put your logic in your controller.

/protected/models/LoginForm.php

<?php

/**
 * LoginForm class.
 * LoginForm is the data structure for keeping
 * user login form data. It is used by the 'login' action of 'SiteController'.
 */
class LoginForm extends CFormModel
{
	public $username;
	public $password;
	public $rememberMe;

	private $_identity;

	/**
	 * Declares the validation rules.
	 * The rules state that username and password are required,
	 * and password needs to be authenticated.
	 */
	public function rules()
	{
		return array(
			// username and password are required
			array('username, password', 'required'),
			// rememberMe needs to be a boolean
			array('rememberMe', 'boolean'),
			// password needs to be authenticated
			array('password', 'authenticate'),
		);
	}

	/**
	 * Declares attribute labels.
	 */
	public function attributeLabels()
	{
		return array(
			'rememberMe'=>'Remember me next time',
		);
	}

	/**
	 * Authenticates the password.
	 * This is the 'authenticate' validator as declared in rules().
	 */
	public function authenticate($attribute,$params)
	{
		if(!$this->hasErrors())
		{
			$this->_identity=new UserIdentity($this->username,$this->password);
			if(!$this->_identity->authenticate())
				$this->addError('password',$this->_identity->errorMessage);
		}
	}

	/**
	 * Logs in the user using the given username and password in the model.
	 * @return boolean whether login is successful
	 */
	public function login()
	{
		if($this->_identity===null)
		{
			$this->_identity=new UserIdentity($this->username,$this->password);
			$this->_identity->authenticate();
		}
		if($this->_identity->errorCode===UserIdentity::ERROR_NONE)
		{
			$duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days
			Yii::app()->user->login($this->_identity,$duration);
			return true;
		}
		else
			return false;
	}
}

Next we'll set up our action for the login page. I put this in my site controller. Again it should just be a straight copy and paste, no edits necessary.

/protected/controllers/SiteController.php segment


	/**
	 * Displays the login page
	 */
	public function actionLogin()
	{
		$model=new LoginForm;

		// if it is ajax validation request
		if(isset($_POST['ajax']) && $_POST['ajax']==='login-form')
		{
			echo CActiveForm::validate($model);
			Yii::app()->end();
		}

		// collect user input data
		if(isset($_POST['LoginForm']))
		{
			$model->attributes=$_POST['LoginForm'];
			// validate user input and redirect to the previous page if valid
			if($model->validate() && $model->login())
				$this->redirect('/');
		}
		// display the login form
		$this->render('login',array('model'=>$model));
	}

Now we need to create the view file for our form. Again, no changes required here though you can style it to your own liking. When I set this up I used the default Yii Classic template.

/protected/views/site/login.php

<?php
/* @var $this SiteController */
/* @var $model LoginForm */
/* @var $form CActiveForm  */

$this->pageTitle=Yii::app()->name . ' - Login';
$this->breadcrumbs=array(
	'Login',
);
?>

<h1>Login</h1>

<p>Please fill out the following form with your login credentials:</p>

<div class="form">
<?php $form=$this->beginWidget('CActiveForm', array(
	'id'=>'login-form',
	'enableClientValidation'=>true,
	'clientOptions'=>array(
		'validateOnSubmit'=>true,
	),
)); ?>

	<p class="note">Fields with <span class="required">*</span> are required.</p>

	<div class="row">
		<?php echo $form->labelEx($model,'username'); ?>
		<?php echo $form->textField($model,'username'); ?>
		<?php echo $form->error($model,'username'); ?>
	</div>

	<div class="row">
		<?php echo $form->labelEx($model,'password'); ?>
		<?php echo $form->passwordField($model,'password'); ?>
		<?php echo $form->error($model,'password'); ?>
		<p class="hint">
			Hint: You may login with <kbd>demo</kbd>/<kbd>demo</kbd> or <kbd>admin</kbd>/<kbd>admin</kbd>.
		</p>
	</div>

	<div class="row rememberMe">
		<?php echo $form->checkBox($model,'rememberMe'); ?>
		<?php echo $form->label($model,'rememberMe'); ?>
		<?php echo $form->error($model,'rememberMe'); ?>
	</div>

	<div class="row buttons">
		<?php echo CHtml::submitButton('Login'); ?>
	</div>

<?php $this->endWidget(); ?>
</div><!-- form -->

Finally, we get around to configuring our LDAP configurations. I have set up all the configuration options as part of a ldap parameter. You should add these parameters to your Yii configuration file and tweak as necessary. Here is an outline of what each config option is used for:

servers - a list of IP addresses or DNS names which you want to try and authenticate against. The first one in the list will always be tried first, it will only move on to the next one if it fails to bind to the ldap port.

defaultDomain - a default domain which you want to authenticate to if the user does not try logging in using an email address or using the DOMAIN\USERNAME format.

search - an array of LDAP filters which the user authenticating will be searched for in. If the user does not belong to any of the filters applied, they will receive an access denied message when they try to authenticate. In my example I am only including the Faculty and Staff OUs so if someone in the students OU tries logging in they will be denied access.

Sample Params in Config File: /protected/config/main.php segment

'params'=>array(		
        'ldap' => array (
            'servers' => array(
                'dc1.example.com',
                'dc2.example.com',
                '10.1.0.3',
            ),
            'defaultDomain' => 'EXAMPLE',
            'search'=>array(
                'ou=Students,dc=EXAMPLE,dc=net',
                'ou=Faculty,dc=EXAMPLE,dc=net',
            ),
        ),
),

For those of you who want a quick start I have attached a zip file containing the necessary files to make this work with a stock Yii setup. Just drop the files in, tweak the config, and you're ready to authenticate against your active directory servers!

Yii_Active_Directory_LDAP.zip