Solving Magento

Solutions for Magento E-Commerce Platform

by Oleg Ishenko

Magento Downloadable Product Type (Part 2)

In this post I will describe models and processes specific to Magento downloadable products. To find out about creating downloads and managing their front-end display see the first part of the Downloadable product type overview.

Downloadable product type configuration

Every product type must be configured in the Magento’s global configuration in node global/catalog/product/type. The Mage_Downloadable module’s config.xml extends this configuration with a downloadable node:

<config>
    <!-- code omitted for brevity -->
    <global>
        <!-- code omitted for brevity -->
        <catalog>
            <product>
                <type>
                    <downloadable translate="label" module="downloadable">
                        <label>Downloadable Product</label>
                        <model>downloadable/product_type</model>
                        <is_qty>1</is_qty>
                        <price_model>downloadable/product_price</price_model>
                        <index_data_retreiver>downloadable/catalogIndex_data_downloadable</index_data_retreiver>
                        <composite>0</composite>
                        <price_indexer>downloadable/indexer_price</price_indexer>
                        <can_use_qty_decimals>0</can_use_qty_decimals>
                    </downloadable>
                    <configurable>
                        <allow_product_types>
                            <downloadable/>
                        </allow_product_types>
                    </configurable>
                    <grouped>
                        <allow_product_types>
                            <downloadable/>
                        </allow_product_types>
                    </grouped>
                </type>
            </product>
        </catalog>
        <!-- code omitted for brevity -->
    </global>
    <!-- code omitted for brevity -->
</config>

Listing 3. Downloadable product type configuration, /app/code/core/Mage/Downlodable/etc/config.xml, line 150.

This configuration defines Downloadable as a non-composite (in contrast to Bundles, Grouped, and Configurables) product type whose quantity can’t be measured in decimal numbers. It also declares a price and a index data retriever model. The price model extends the standard product price class by implementing functionality required to calculate final prices for downloadable products whose links can be purchased separately. In this case the price can vary depending on the link selection – see Mage_Downloadable_Model_Product_Price::getFinalPrice() for implementation details. The retriever model Mage_Downloadable_Model_CatalogIndex_Data_Downloadable is used to access product data during indexing. The price indexer model Mage_Downloadable_Model_Resource_Indexer_Price extends the default price indexing class to take into account varying prices of downloadable products whose links can be purchased separately.

Note that this config.xml file also extends configurations of Grouped and Configurable product types. It adds Downloadable type to the list of product types that are allowed as child items to Grouped and Configurable products.

Link and Sample models

As we already know, Downlodable products can have Links and Samples. They are implemented in classes Mage_Downloadable_Model_Link and Mage_Downloadable_Model_Sample that are standard Magento models and as such have resource and collection classes attached to them.

Download Links’ primary table is downloadable_link. Links also use two additional tables: downloadable_link_title and downloadable_link_price. Link titles require a separate table because one link can have multiple titles – one per store. The downloadable_link_price table exists for a similar reason – link prices are defined on a website level.

In addition to title (store view scope) and price (website scope), Link models have the following global properties:

  • link_id – a unique link identifier,
  • product_id – an ID of a parent downloadable product,
  • sort_order – a value used to sort multiple download links of a product in ascending order,
  • number_of_downloads – the maximal number of permitted downloads per purchase, it is unlimited if set to zero,
  • shareable – ability of a link to be shared after a purchase: 0 for no, 1 for yes, 2 will use the default value set in the shop’s configuration,
  • link_url – stores a URL used to reference the downloadable file, it is empty if the file is uploaded to the shop’s server.
  • link_file – contains a path to the downloadable file; the file path is relative to the /media/downloadable/files/links directory,
  • link_type – is used to distinguish between two link types: file (uploaded to the shop server) and url (stored externally),
  • sample_url and sample_file – samples, just as actual downloadable files, can be either referenced by a URL or uploaded to the server; in the latter case the stored file path is relative to /media/downloadable/files/link_samples,
  • sample_type – same as link_type, only for samples.

I have already mentioned the Links can be purchased separately property. If enabled it allows setting link prices that affect the final price of a downloadable product. With this setting active the downloadable product’s required_options property is automatically set to 1, and the product becomes ineligible to be used as an item of a configurable or a grouped product. If the Links can be purchased separately is deactivated all existing link prices are ignored when a product is bought. In this case only the price of the product itself is used.

In terms of functionality Samples are basically a cut-down version of Links. They need not to be purchased and their files can be downloaded at any time. Just as with Links, files of Samples can be either referenced by a URL or uploaded to the shop server. In the latter case they are put to the /media/downloadable/files/samples directory. The data of Samples are stored in database tables downloadable_sample and downloadable_sample_title.

Downloadable product type class

Every product type must be defined in a class that extends the Mage_Catalog_Model_Product_Type_Abstract. Module Mage_Downloadable has such a class too: Mage_Downloadable_Product_Type. This class does not extend the abstract product type directly, instead, it has an intermediate parent. The parent class of the Downloadable product type model is Mage_Catalog_Model_Product_Type_Virtual. Both Downloadable and Virtual product types are “virtual” and do not require a shipping address to be sold – you will notice the absence of the respective step in the checkout (that is, of course, if your cart contains no physical products). Also, shipping costs do not apply to virtual products.

The functionality of the Downloadable product type can be grouped into the following categories:

  • Accessing Links and Samples. This group is composed of methods whose names are self-explanatory: hasLinks(), getLinks(), hasSamples(), getSamples()
  • Saving Downlodable products. The methods in this group are responsible for writing downloadable information to database whenever a product is saved. These methods are beforeSave() and save. The first method is called from Mage_Catalog_Model_Product::_beforeSave. Its task is to check if the product’s links can be purchased separately. If yes, the product will later get its property required_options set to 1. The second method, save, is fired from Mage_Catalog_Model_Product::_afterSave(). It ensures that the downloadable information posted from the product edit form is correctly processed. This information contains sample and link data. If any samples or links are marked for deletion – they get deleted. If there are any files uploaded – they get moved from a temporary storage where they’ve been posted to by the Flex uploader to their final storage directories.
  • Preparing Downloadable products to be added to cart. Methods in this category check if a downloadable product can be sold and prepare it to be converted to a cart item. Thus, the isSalable() function extends the standard “can be sold” check by making sure that a downloadable product has links. The checkProductBuyState() deals with a buying request object by checking if a customer has selected links for a downloadable product that requires link selection (the Links can be purchased separately setting enabled). If a buying request object contains no required selection, the method throws an exception warning the customer: “Please specify product link(s)”. When a product is about to be converted to a quote item, the system calls the _prepareProduct() method. This function also checks if at least one link is selected for a product that requires so, but this check is done only for front-end requests. If an order is composed in the back-end, all links of a downloadable product get selected automatically and no additional check is necessary. If the buying request has valid links, the ID of these are imploded into a string and added to the product’s custom option downloadable_link_ids. Later these IDs will be used to generate purchased link items that we will discuss in the next section.

Buying a downloadable product

Downloadable products require additional processing during checkout. Apart from the conversion of quote items to order items, the system must connect link information to order items and save it to database. This must be done so that every order containing downloadable items has data on which links were bought, by whom, and if there are any access restrictions to those links (limited number of downloads). The link processing is triggered by the sales_order_item_save_commit_after event. The observer method Mage_Downloadable_Model_Observer::saveDownloadableOrderItem() receives an order item object and checks if it is a downloadable item with purchased link IDs. If that is the case, the observer method creates a “link purchased” object (type Mage_Downloadable_Model_Link_Purchased), extends it with order and order item data (order ID, increment order ID, order item ID, created at, updated at, customer ID, product name, and product SKU) and saves it to table downloadable_link_purchased. A “purchased link” serves as a shell for purchased link items which there can be more that one per downloadable product bought. But there is always only one purchased link object per downloadable order item. Purchased link items are created in the same observer method immediately after a purchased link object is saved. For these link items the following data is stored in table downloadable_link_purchased_item:

  • purchased_id – a reference to the parent “purchased link” object,
  • order_item_id – a reference to the parent order item,
  • product_id – ID of the downloadable product,
  • link_hash – a unique value used in public URLs (in customer account or in emails) to identify a downloadable link item,
  • number_of_download_bought – for links with limited number of downloads this value equals the ordered quantity multiplied by the download limit; links with unlimited downloads have this property set to zero,
  • number_of_downloads_used – is the number of times a link was accessed by a customer, irrelevant for links with unlimited downloads,
  • link_id – a reference to a downloadable link entry in table downloadable_link with properties link_title, is_shareable, link_url, link_file, and link_type copying their respective values from that entry,
  • status – only available links can be downloaded, other stati are: pending, pending_payment, payment_review, and expired,
  • created_at and updated_at – date and time of the creation and the latest update of a link item.

Thus, after an order is created, each downloadable item has a purchased link and one or several purchased link items.

Delivering downloadable products

Customers can access their downloadable purchases either by logging into their customer account in the shop website or by clicking a link in an email (a new order or an invoice email). Email links are the only way guest customers can download their files and that is only if these links are shareable. A download link follows Magento’s link pattern and consists of module, controller, action, and parameter parts: https://www.yourshop.com/downloadable/download/link/id/MC42ODEwODMwMCAxMzc2NTEwNzExMjMwMTY4/.

Download requests are routed to the download controller’s linkAction(). This methods receives a GET-parameter id and processes the download request in the following stages.

  • Loading the purchased link item. The id parameter contains a hash value that is used to identify the link item:
    $id = $this->getRequest()->getParam('id', 0);
    $linkPurchasedItem = Mage::getModel('downloadable/link_purchased_item')
        ->load($id, 'link_hash');
    if (! $linkPurchasedItem->getId() ) {
        $this->_getCustomerSession()->addNotice(
            Mage::helper('downloadable')->__("Requested link does not exist.")
        );
        return $this->_redirect('*/customer/products');
    }
    

    Listing 4. Loading a link item, /app/code/core/Mage/Downloadable/controllers/DownloadController.php, line 156.

  • Checking if a customer is authenticated and if the purchased link belongs to him. For non-shareable links a customer authentication is required. Also, the system must compare the logged customer’s ID to the customer ID stored in the purchased link. If they don’t match, no download is possible:
    if (!Mage::helper('downloadable')->getIsShareable($linkPurchasedItem)) {
        $customerId = $this->_getCustomerSession()->getCustomerId();
        if (!$customerId) {
            /**
             *  code setting error messages to the session is omitted for brevity
             */
            return ;
        }
        $linkPurchased = Mage::getModel('downloadable/link_purchased')
            ->load($linkPurchasedItem->getPurchasedId());
        if ($linkPurchased->getCustomerId() != $customerId) {
            $this->_getCustomerSession()->addNotice(
                Mage::helper('downloadable')->__("Requested link does not exist.")
            );
            return $this->_redirect('*/customer/products');
        }
    }
    

    Listing 5. Checking the customer rights to access the requested link, /app/code/core/Mage/Downloadable/controllers/DownloadController.php, line 162.

  • Checking the link’s availability. The system must ensure that the link has been unlocked and, if there is a download limit, still has remaining downloads:
    $downloadsLeft = $linkPurchasedItem->getNumberOfDownloadsBought()
        - $linkPurchasedItem->getNumberOfDownloadsUsed();
    
    $status = $linkPurchasedItem->getStatus();
    if ($status == Mage_Downloadable_Model_Link_Purchased_Item::LINK_STATUS_AVAILABLE
        && ($downloadsLeft || $linkPurchasedItem->getNumberOfDownloadsBought() == 0)
    ) {
    /**
     *  code retrieving the requested files is omitted for brevity;
     */
    } elseif ($status == Mage_Downloadable_Model_Link_Purchased_Item::LINK_STATUS_EXPIRED) {
        $this->_getCustomerSession()->addNotice(
            Mage::helper('downloadable')->__('The link has expired.')
        );
    } elseif ($status == Mage_Downloadable_Model_Link_Purchased_Item::LINK_STATUS_PENDING
        || $status == Mage_Downloadable_Model_Link_Purchased_Item::LINK_STATUS_PAYMENT_REVIEW
    ) {
        $this->_getCustomerSession()->addNotice(
            Mage::helper('downloadable')->__('The link is not available.')
        );
    } else {
        $this->_getCustomerSession()->addError(
            Mage::helper('downloadable')->__(
                'An error occurred while getting the requested content. Please contact the store owner.'
            )
        );
    }
    

    Listing 6. Checking the link’s availability, /app/code/core/Mage/Downloadable/controllers/DownloadController.php, line 184.

    Link status is set by an observer method Mage_Downloadable_Model_Observer::setLinkStatus() that intercepts the sales_order_save_commit_after event. This method checks the current state of an order and sets link stati accordingly. For example, links are set to “expired” if the order is cancelled or refunded. Orders awaiting payment have their links marked as “pending payment”, just as orders under payment review have their links in status “payment review”.

  • Processing download. With all conditions met the system can now issue the requested download. The download resource can be either a URL to an external host or a path to a file on the local file system. This information is passed to method Mage_Downloadable_DownloadController::_processDownload($resource, $resourceType) that prepares the response by setting the response code and headers. One of the headers is “Content-Disposition” that is defined by a setting in the shop configuration I’ve mentioned before. Next the system fetches a download helper (Mage_Downloadable_Helper_Download). This helper is used to access the specified file resource and write its content to the HTTP response. To do that the helper needs a resource handle which it gets in its method _getHandle()). A URL resource handle is obtained by opening a socket connection to the host specified in the URL:
    try {
        $this->_handle = fsockopen($hostname, $port, $errno, $errstr);
    }
    catch (Exception $e) {
        throw $e;
    }
    
    if ($this->_handle === false) {
        Mage::throwException(Mage::helper('downloadable')->__(
            'Cannot connect to remote host, error: %s.', $errstr)
        );
    }
    $headers = 'GET ' . $path . $query . ' HTTP/1.0' . "\r\n"
        . 'Host: ' . $hostname . "\r\n"
        . 'User-Agent: Magento ver/' . Mage::getVersion() . "\r\n"
        . 'Connection: close' . "\r\n"
        . "\r\n";
    fwrite($this->_handle, $headers);
    

    Listing 7. Opening a socket connection to download an external file, /app/code/core/Mage/Downloadable/Helper/Download.php, line 123.

    Parameters $hostname, $port, $path, and $query are parsed from the URL of the download link.

    Files on the local filesystem are accessed using a Varien_Io_File object:

    $this->_handle = new Varien_Io_File();
    if (!is_file($this->_resourceFile)) {
        Mage::helper('core/file_storage_database')
            ->saveFileToFilesystem($this->_resourceFile);
    }
    $this->_handle->open(array('path'=>Mage::getBaseDir('var')));
    if (!$this->_handle->fileExists($this->_resourceFile, true)) {
        Mage::throwException(Mage::helper('downloadable')
            ->__('The file does not exist.'));
    }
    $this->_handle->streamOpen($this->_resourceFile, 'r');
    

    Listing 8. Retrieving a file uploaded to the shop server, line 166.

    Once the handle has been acquired, the helper uses its method output() to write the contents of the download file to the HTTP response:

    public function output()
    {
        $handle = $this->_getHandle();
        if ($this->_linkType == self::LINK_TYPE_FILE) {
            while ($buffer = $handle->streamRead()) {
                print $buffer;
            }
        }
        elseif ($this->_linkType == self::LINK_TYPE_URL) {
            while (!feof($handle)) {
                print fgets($handle, 1024);
            }
        }
     }
    

    Listing 9. Reading file contents, line 282.

    A similar process is used by the download controller to process requests for sample files. Methods sampleAction() and sampleLinkAction() also instantiate a download helper to access the sample file resources. Since simple downloads are available by definition to anyone, no user authentication or access rights verification is performed.

Conclusion

We have looked into implementation details of the Downloadable product type, the last of the six product types available in a standard Magento installation (others being Simple, Virtual, Configurable, Grouped, and Bundle). Mage_Downloadable is a sophisticated module that demonstrates how to trade virtual goods, and gives an example of how to build functionality necessary to store and deliver virtual products. In the next post I will present a tutorial in which I’ll build a simple extension on top of this module (see Magento Downloadable Product type tutorial).

3 thoughts on “Magento Downloadable Product Type (Part 2)

  1. Pingback: Magento Downloadable Product Type (Part 1) | Solving Magento

  2. I would like to skip the address requirements in the checkout form when only downloadable products are purchased. How do I do this? Most simple would be to make all fields in the checkout form optional so no red star indicating required, except the email address? Thanks for any tips.

  3. hello this my website there is problem that anywhere product select and enter the shopping cart but till when we check out the product then cart show empty and continues the shopping. So how to do when the released this problem. please replay fast. thank you

Leave a Reply

Your email address will not be published. Required fields are marked *

Theme: Esquire by Matthew Buchanan.