<?php

namespace XQueue\Maileon\API\Mailings;

use XQueue\Maileon\API\AbstractMaileonService;
use XQueue\Maileon\API\MaileonAPIException;

/**
 * Facade that wraps the REST service for mailings.
 *
 * @author Marcus Ständer | Trusted Mails GmbH | marcus.staender@trusted-mails.com
 * @author Andreas Lange | XQueue GmbH | andreas.lange@xqueue.com
 */
class MailingsService extends AbstractMaileonService
{

    /**
     * Creates a new mailing.
     * @param string $name
     *  the name of the mailing
     * @param string $subject
     *  the subject of the mailing
     * @param bool $deprecatedParameter
     *  this parameter was never used by the API
     * @param string $type
     *  the type of the mailing, which can be one of 'doi', 'trigger', or 'regular'.
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function createMailing($name, $subject, $deprecatedParameter = false, $type = "regular")
    {
        $queryParameters = array(
            'name' => urlencode($name),
            'subject' => urlencode($subject),
            'type' => urlencode($type),
        );

        return $this->post('mailings', "", $queryParameters);
    }

    /**
     * Get the ID of a mailing by its name
     */
    function getMailingIdByName($mailingName)
    {
        return $this->get('mailings/name/' . urlencode($mailingName));
    }

    /**
     * Check if a mailing with the given name exists and return true or false
     */
    function checkIfMailingExistsByName($mailingName)
    {
        $response = $this->get('mailings/name/' . urlencode($mailingName));
        return ($response->isSuccess());
    }

    /**
     * Disable all QoS checks for a given mailing
     */
    function disableQosChecks($mailingId)
    {
        return $this->put('mailings/' . $mailingId . '/settings/disableQosChecks');
    }

    /**
     * Sets the dispatch logic for trigger mailings
     */
    function setTriggerDispatchLogic($mailingId, $logic)
    {
        $queryParameters = array();
        return $this->put('mailings/' . $mailingId . '/dispatching', $logic, $queryParameters);
    }

    /**
     * Used for DOI Mailings
     * */
    function setTriggerActive($mailingId)
    {
        return $this->post('mailings/' . $mailingId . '/dispatching/activate', "");
    }

    /**
     * Deletes an active trigger mailing.
     * @param integer $id
     *  the ID of the mailing to delete
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function deleteActiveTriggerMailing($mailingId)
    {
        return $this->delete("mailings/".$mailingId."/dispatching");
    }

    /**
     * Deletes a mailing by ID.
     * @param integer $id
     *  the ID of the mailing to delete
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function deleteMailing($id)
    {
        return $this->delete("mailings/".$id);
    }

    /**
     * Updates the HTML content of the mailing referenced by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string $html
     *  the new HTML content of the mailing
     * @param bool $doImageGrabbing
     *  specifies if image grabbing should be performed
     * @param bool $doLinkTracking
     *  specifies if link tracking should be performed
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setHTMLContent($mailingId, $html, $doImageGrabbing = true, $doLinkTracking = false)
    {
        $queryParameters = array(
            'doImageGrabbing' => ($doImageGrabbing == TRUE) ? "true" : "false",
            'doLinkTracking' => ($doLinkTracking == TRUE) ? "true" : "false"
        );
        return $this->post('mailings/' . $mailingId . '/contents/html', $html, $queryParameters, "text/html");
    }

    /**
     * Updates the TEXT content of the mailing referenced by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string $text
     *  the new TEXT content of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setTextContent($mailingId, $text)
    {
        return $this->post('mailings/' . $mailingId . '/contents/text', $text, array(), "text/plain");
    }

    /**
     * Fetches the HTML content of the mailing identified by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call, with the HTML content string of the mailing
     *  available at com_maileon_api_MaileonAPIResult::getResult()
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function getHTMLContent($mailingId)
    {
        return $this->get('mailings/' . $mailingId . '/contents/html', null, "text/html");
    }

    /**
     * Fetches the TEXT content of the mailing identified by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call, with the TEXT content string of the mailing
     *  available at com_maileon_api_MaileonAPIResult::getResult()
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function getTextContent($mailingId)
    {
        return $this->get('mailings/' . $mailingId . '/contents/text', null, "text/plain");
    }

    /**
     * Updates the target group id of the mailing referenced by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string $targetGroupId
     *  the ID of the target group to set
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setTargetGroupId($mailingId, $targetGroupId)
    {
        return $this->post('mailings/' . $mailingId . '/targetgroupid', "<targetgroupid>" . $targetGroupId . "</targetgroupid>");
    }

    /**
     * Fetches the target group id of the mailing identified by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call, with the target group id of the mailing
     *  available at com_maileon_api_MaileonAPIResult::getResult()
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function getTargetGroupId($mailingId)
    {
        return $this->get('mailings/' . $mailingId . '/targetgroupid', null);
    }

    /**
     * Updates the sender email address of the mailing referenced by the given ID.

     * Note: if not only the local part but also the domain is provided, make sure that is exists in Maileon.
     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string $email
     *  the ID of the target group to set
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setSender($mailingId, $email)
    {
        return $this->post('mailings/' . $mailingId . '/contents/sender', "<sender>" . $email . "</sender>");
    }

    /**
     * Fetches the sender email address of the mailing identified by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call, with the sender email address of the mailing
     *  available at com_maileon_api_MaileonAPIResult::getResult()
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function getSender($mailingId)
    {
        return $this->get('mailings/' . $mailingId . '/contents/sender');
    }

    /**
     * Updates the subject of the mailing referenced by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string $subject
     *  the subject of the mailing to set
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setSubject($mailingId, $subject)
    {
        return $this->post('mailings/' . $mailingId . '/contents/subject', "<subject>" . $subject . "</subject>");
    }

    /**
     * Fetches the subject of the mailing identified by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call, with the subject of the mailing
     *  available at com_maileon_api_MaileonAPIResult::getResult()
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function getSubject($mailingId)
    {
        return $this->get('mailings/' . $mailingId . '/contents/subject');
    }

    /**
     * Updates the senderalias of the mailing referenced by the given ID.

     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string $senderalias
     *  the sender alias to set
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setSenderAlias($mailingId, $senderalias)
    {
        return $this->post('mailings/' . $mailingId . '/contents/senderalias', "<senderalias>" . $senderalias . "</senderalias>");
    }

    /**
     * Updates the recipientalias of the mailing referenced by the given ID.

     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string $recipientalias
     *  the recipient alias to set
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setRecipientAlias($mailingId, $recipientalias)
    {
        return $this->post('mailings/' . $mailingId . '/contents/recipientalias', "<recipientalias>" . $recipientalias . "</recipientalias>");
    }

    /**
     * Fetches the reply-to address of the mailing identified by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call, with the reply-to address of the mailing
     *  available at com_maileon_api_MaileonAPIResult::getResult()
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function getReplyToAddress($mailingId)
    {
        return $this->get('mailings/' . $mailingId . '/settings/replyto');
    }

    /**
     * Sets the reply-to address of the mailing identified by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing
     * @param bool $auto (default = true)
     *  If true, the Maileon autorecognition will be used and emails will be saved within Maileon. If false, a custom email address can be passed which gets all mails forwarded.
     * @param string $customEmail (default = empty)
     *  If $auto is false, this email will be used for manual responses.
     * @return
     * @throws MaileonException
     */
    function setReplyToAddress($mailingId, $auto = true, $customEmail = null)
    {
        $queryParameters = array(
            'auto' => ($auto == TRUE) ? "true" : "false",
            'customEmail' => $customEmail
        );

        return $this->post('mailings/' . $mailingId . '/settings/replyto', null, $queryParameters);
    }

    /**
     * Types can be selected from 'doi','trigger', 'trigger_template' or 'regular'

     *

     * @see MailingFields
     *
     * @param string $scheduleTime
     *  This is a date and time string that defines the filter for a mailing. The mailings before and after that time can be queried, see beforeSchedulingTime. The format is the standard SQL date: yyyy-MM-dd HH:mm:ss
     * @param bool $beforeSchedulingTime (default = true)
     *  If true, the mailings before the given time will be returned, if false, the mailings at or after the given time will be returned.
     * @param string[] fields (default = empty)
     *  This list contains the fields that shall be returned with the result. If this list is empty, only the IDs will be returned. Valid fields are: state, type, name, and scheduleTime
     * @param number page_index (default = 1)
     *  The index of the result page. The index must be greater or equal to 1.
     * @param number page_size (default = 100)
     *    The maximum count of items in the result page. If provided, the value of page_size must be in the range 1 to 1000.
     * @param string orderBy (default = id)
     *    The field to order results by
     * @param string order (default = DESC)
     *    The order
     * @return
     * @throws MaileonException
     */
    function getMailingsBySchedulingTime($scheduleTime, $beforeSchedulingTime = true, $fields = array(), $page_index = 1, $page_size = 100, $orderBy = "id", $order = "DESC")
    {
        $queryParameters = array(
            'page_index' => $page_index,
            'page_size' => $page_size,
            'scheduleTime' => urlencode($scheduleTime),
            'beforeSchedulingTime' => ($beforeSchedulingTime == TRUE) ? "true" : "false",
            'orderBy' => $orderBy,
            'order' => $order
        );

        $queryParameters = $this->appendArrayFields($queryParameters, "fields", $fields);

        return $this->get('mailings/filter/scheduletime', $queryParameters);
    }

    /**
     * Types can be selected from 'doi','trigger', 'trigger_template' or 'regular'

     *

     * @see MailingFields
     *
     * @param string[] $types
     *  This is the list of types to filter for
     * @param string[] fields (default = empty)
     *  This list contains the fields that shall be returned with the result. If this list is empty, only the IDs will be returned. Valid fields are: state, type, name, and scheduleTime
     * @param number page_index (default = 1)
     *  The index of the result page. The index must be greater or equal to 1.
     * @param number page_size (default = 100)
     *    The maximum count of items in the result page. If provided, the value of page_size must be in the range 1 to 1000.
     * @return
     * @throws MaileonException
     */
    function getMailingsByTypes($types, $fields = array(), $page_index = 1, $page_size = 100)
    {
        $queryParameters = array(
            'page_index' => $page_index,
            'page_size' => $page_size,
            'order' => "DESC"
        );

        $queryParameters = $this->appendArrayFields($queryParameters, "types", $types);
        $queryParameters = $this->appendArrayFields($queryParameters, "fields", $fields);

        return $this->get('mailings/filter/types', $queryParameters);
    }

    /**
     * Schedules the mailing to be instantly sent
     *
     * @param number mailingId
     *  The ID of the mailing to send now
     * @return
     * @throws MaileonException
     */
    function sendMailingNow($mailingId)
    {
        return $this->post('mailings/' . $mailingId . '/sendnow');
    }

    /**
     * Fetches the DOI mailing key of the mailing identified by the given ID.
     *
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call, with the target group id of the mailing
     *  available at com_maileon_api_MaileonAPIResult::getResult()
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function getDoiMailingKey($mailingId)
    {
        return $this->get('mailings/' . $mailingId . '/settings/doi_key', null, "text/html");
    }

    /**
     * Sets the key of the DOI mailing identified by the given ID.
     *
     * @param number $mailingId
     *  the ID of the mailing
     * @param string $doiKey
     *  The new DOI key.
     * @return
     * @throws MaileonException
     */
    function setDoiMailingKey($mailingId, $doiKey)
    {
        return $this->post('mailings/' . $mailingId . '/settings/doi_key', "<doi_key>$doiKey</doi_key>");
    }

    /**
     * Deactivates a trigger mailing by ID.
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function deactivateTriggerMailing($mailingId)
    {
        return $this->delete("mailings/${mailingId}/dispatching");
    }

    /**
     * Get the dispatch data for a trigger mailing by mailing ID.
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function getTriggerDispatchLogic($mailingId)
    {
        return $this->get("mailings/${mailingId}/dispatching");
    }

    /**
     * Get the schedule for regular mailings by mailing ID.
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function getSchedule($mailingId)
    {
        return $this->get("mailings/${mailingId}/schedule");
    }

    /**
     * Get the archive url for the mailing ID.
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function getArchiveUrl($mailingId)
    {
        return $this->get("mailings/${mailingId}/archiveurl");
    }

    /**
     * Updates the name of the mailing referenced by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string $name
     *  the name of the mailing to set
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setName($mailingId, $name)
    {
        return $this->post('mailings/' . $mailingId . '/name', "<name>" . $name . "</name>");
    }

    /**
     * Get the name for the mailing by mailing ID.
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function getName($mailingId)
    {
        return $this->get("mailings/${mailingId}/name");
    }

    /**
     * Updates the tags of the mailing referenced by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param array $tags
     *  the tags
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setTags($mailingId, $tags)
    {
        return $this->post('mailings/' . $mailingId . '/settings/tags', "<tags>" . join("#", $tags) . "</tags>");
    }

    /**
     * Get the tags for the mailing identified by mailing ID.
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function getTags($mailingId)
    {
        return $this->get("mailings/${mailingId}/settings/tags");
    }

    /**
     * Updates the locale of the mailing referenced by the given ID.
     *
     * @param string $mailingId
     *  the ID of the mailing to update
     * @param string locale
     *  the locale in the form xx: e.g. de, en, fr, �
     * @return \em com_maileon_api_MaileonAPIResult
     *    the result object of the API call
     * @throws com_maileon_api_MaileonAPIException
     *  if there was a connection problem or a server error occurred
     */
    function setLocale($mailingId, $locale)
    {
        return $this->post('mailings/' . $mailingId . '/settings/locale', "<locale>$locale</locale>");
    }

    /**
     * Get the locale for the mailing identified by mailing ID in the form xx: e.g. de, en, fr, �
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function getLocale($mailingId)
    {
        return $this->get("mailings/${mailingId}/settings/locale");
    }

    /**
     * Execute the RSS SmartMailing functionality for mailings, i.e. fill all SmartMailing Tags from the described RSS-Feeds.
     * For more information about RSS-SmartMailing, please check our customer support at service@xqueue.com
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function fillRssSmartContentTags($mailingId)
    {
        return $this->post("mailings/${mailingId}/contents/smartmailing/rss");
    }

    /**
     * Copy the mailing with the given mailing ID.
     * @param number $mailingId
     *  the ID of the mailing
     * @return \em com_maileon_api_MaileonAPIResult
     *  the result of the operation
     */
    function copyMailing($mailingId)
    {
        return $this->post("mailings/${mailingId}/copy");
    }

    /**
     * Read a binary file from the file system and adds it as an attachment to this transaction.
     *
     * @param type $mailingId
     * @param type $filename
     * @param type $contentType
     * @param type $attachmentFileName Name of the file in the attachments
     *
     * @return com_maileon_api_MaileonAPIResult
     * @throws com_maileon_api_MaileonAPIException
     */
    function addAttachmentFromFile($mailingId, $filename, $contentType, $attachmentFileName = null) {
        $handle = fopen($filename, "rb");
        if (FALSE === $filename) {
            throw new MaileonAPIException("Cannot read file " . $filename . ".");
        }
        $contents = '';
        while (!feof($handle)) {
            $contents .= fread($handle, 8192);
        }
        fclose($handle);
        if ($attachmentFileName === null) {
            $attachmentFileName = basename($filename);
        }

        return $this->addAttachment($mailingId, $attachmentFileName, $contentType, $contents);
    }

    /**
     * Adds an attachment to the mailing with the provided id.
     *
     * @param int $mailingId The mailing id
     * @param string $filename Filename of the attachment to be displayed in sent emails. It is recommended to keep the filename short and to use an extension corresponding to the mime type of the attachment.
     * @param string $contentType The mime type of the attachment
     * @param string $contents The file content
     *
     * @return com_maileon_api_MaileonAPIResult
     */
    function addAttachment($mailingId, $filename, $contentType, $contents) {
        $queryParameters = array( 'filename' => $filename );
        return $this->post("mailings/${mailingId}/attachments", $contents, $queryParameters, null, null, $contentType, strlen($contents));
    }

    /**
     * Returns a list of the registered attachments for the mailing with the provided id.
     *
     * @param int $mailingId
     * @return com_maileon_api_MaileonAPIResult
     */
    function getAttachments($mailingId) {
        return $this->get("mailings/${mailingId}/attachments");
    }

    /**
     * Returns the attachment with the provided id as a file.
     *
     * @param int $mailingId
     * @param int $attachmentId
     * @return com_maileon_api_MaileonAPIResult
     */
    function getAttachment($mailingId, $attachmentId) {
        return $this->get("mailings/${mailingId}/attachments/${attachmentId}");
    }

    /**
     * Returns the count of available attachments in the mailing with the provided id.
     *
     * @param int $mailingId
     * @return com_maileon_api_MaileonAPIResult
     */
    function getAttachmentsCount($mailingId) {
        return $this->get("mailings/${mailingId}/attachments/count");
    }

    /**
     * Deletes all the attachments that belong to the mailing with the provided id. The mailing should not be sealed.
     *
     * @param int $mailingId
     * @return type
     */
    function deleteAttachments($mailingId) {
        return $this->delete("mailings/${mailingId}/attachments/");
    }

    /**
     * Deletes the attachment with the provided id from the mailing. The mailing should not be sealed.
     *
     * @param int $mailingId
     * @param int $attachmentId
     *
     * @return com_maileon_api_MaileonAPIResult
     * @throws com_maileon_api_MaileonAPIException
     */
    function deleteAttachment($mailingId, $attachmentId) {
        if(empty($attachmentId)) { throw new MaileonAPIException("no attachment id specified"); }

        return $this->delete("mailings/${mailingId}/attachments/${attachmentId}");
    }

    /**
     * Copies the attachments of a source mailing into a target mailing. Note that the target mailing should not be sealed and that the resulting total count of attachments in the target mailing should not exceed 10.
     *
     * @param int $mailingId
     * @param int $srcMailingId
     * @return com_maileon_api_MaileonAPIResult
     */
    function copyAttachments($mailingId, $srcMailingId) {
        $queryParameters = array( 'src_mailing_id' => $srcMailingId );

        return $this->put("mailings/${mailingId}/attachments", "", $queryParameters);
    }

    /**
     * Returns a list of custom properties for the mailing with the provided id.
     *
     * @param int $mailingId
     * @return com_maileon_api_MaileonAPIResult
     */
    function getCustomProperties($mailingId) {
        return $this->get("mailings/${mailingId}/settings/properties");
    }

    /**
     * Adds a list of custom properties to the mailing with the provided id.
     *
     * @param int $mailingId
     * @param array $properties Array of com_maileon_api_mailings_CustomProperty or single property
     * @return com_maileon_api_MaileonAPIResult
     */
    function addCustomProperties($mailingId, $properties) {

        $xml = new SimpleXMLElement("<?xml version=\"1.0\"?><properties></properties>");

        if (is_array($properties)) {
            foreach ($properties as $property) {
                $this->sxml_append($xml, $property->toXML());
            }
        } else {
            $this->sxml_append($xml, $properties->toXML());
        }

        return $this->post("mailings/${mailingId}/settings/properties", $xml->asXML());
    }

    /**
     * Updates a custom property of the mailing with the provided id.
     *
     * @param int $mailingId
     * @param com_maileon_api_mailings_CustomProperty $property
     * @return com_maileon_api_MaileonAPIResult
     */
    function updateCustomProperty($mailingId, $property) {

        $queryParameters = array(
            'name' => $property->key,
            'value' => $property->value
        );

        return $this->put("mailings/${mailingId}/settings/properties", "", $queryParameters);
    }

    /**
     * Deletes a custom property of the mailing with the provided id.
     *
     * @param int $mailingId
     * @param string $propertyName The name of the property to delete
     * @return com_maileon_api_MaileonAPIResult
     */
    function deleteCustomProperty($mailingId, $propertyName) {

        $queryParameters = array(
            'name' => $propertyName,
        );

        return $this->delete("mailings/${mailingId}/settings/properties", $queryParameters);
    }

    function sxml_append(SimpleXMLElement $to, SimpleXMLElement $from) {
        $toDom = dom_import_simplexml($to);
        $fromDom = dom_import_simplexml($from);
        $toDom->appendChild($toDom->ownerDocument->importNode($fromDom, true));
    }

}


<?php

namespace XQueue\Maileon\API\Mailings;

use XQueue\Maileon\API\XML\AbstractXMLWrapper;
use XQueue\Maileon\API\MaileonAPIException;

/**
 * The wrapper class for a Maileon attachment. This class wraps the XML structure.
 *
 * @author Viktor Balogh | Wanadis Kft. | balogh.viktor@maileon.hu
 */
class Attachment extends AbstractXMLWrapper
{
    public $id;
    public $filename;
    public $sizekb;
    public $mime_type;
    public $diagnosis;
    public $created;
    public $updated;

    /**
     * Constructor initializing default values.
     *
     * @param type $id
     * @param type $filename
     * @param type $sizekb
     * @param type $mime_type
     * @param type $diagnosis
     * @param type $created
     * @param type $updated
     */
    function __construct(
        $id = null,
        $filename = null,
        $sizekb = null,
        $mime_type = null,
        $diagnosis = null,
        $created = null,
        $updated = null)
    {
        $this->id = $id;
        $this->filename = $filename;
        $this->sizekb = $sizekb;
        $this->mime_type = $mime_type;
        $this->diagnosis = $diagnosis;
        $this->created = $created;
        $this->updated = $updated;
    }

    /**
     * Initialization of the attachment from a simple xml element.
     *
     * @param SimpleXMLElement $xmlElement
     *  The xml element that is used to parse the attachment from.
     */
    function fromXML($xmlElement)
    {
        if (isset($xmlElement->id)) { $this->id = (int)$xmlElement->id; }
        if (isset($xmlElement->filename)) { $this->filename = (string)$xmlElement->filename; }
        if (isset($xmlElement->sizekb)) { $this->sizekb = (int)$xmlElement->sizekb; }
        if (isset($xmlElement->mime_type)) { $this->mime_type = (string)$xmlElement->mime_type; }
        if (isset($xmlElement->diagnosis)) { $this->diagnosis = (string)$xmlElement->diagnosis; }
        if (isset($xmlElement->created)) { $this->created = (string)$xmlElement->created; }
        if (isset($xmlElement->updated)) { $this->updated = (string)$xmlElement->updated; }
    }

    /**
     * Creates the XML representation of an attachment
     *
     * @return \SimpleXMLElement
     */
    function toXML() {
        /*$xml = new SimpleXMLElement("<?xml version=\"1.0\"?><attachment></attachment>");

        $xml->addChild("id", $this->id);
        $xml->addChild("filename", $this->filename);
        $xml->addChild("sizekb", $this->sizekb);
        $xml->addChild("mime_type", $this->mime_type);
        $xml->addChild("diagnosis", $this->diagnosis);
        $xml->addChild("created", $this->created);
        $xml->addChild("updated", $this->updated);

        return $xml;*/
        throw new MaileonAPIException('not implemented');
    }

    /**
     * Human readable representation of this wrapper.
     *
     * @return \em string
     *  A human readable version of the mailing.
     */
    function toString()
    {
        return "Attachment [id=" . $this->id . ", "
                . "filename=" . $this->filename . ", "
                . "sizekb=" . $this->sizekb . ", "
                . "mime_type=" . $this->mime_type . ", "
                . "diagnosis=" . $this->diagnosis . ", "
                . "created=" . $this->created . ", "
                . "updated=" . $this->updated . "]";
    }
}


<?php

namespace PhpOffice\PhpSpreadsheet\Style;

use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;

class Protection extends Supervisor
{
    /** Protection styles */
    const PROTECTION_INHERIT = 'inherit';
    const PROTECTION_PROTECTED = 'protected';
    const PROTECTION_UNPROTECTED = 'unprotected';

    /**
     * Locked.
     *
     * @var string
     */
    protected $locked;

    /**
     * Hidden.
     *
     * @var string
     */
    protected $hidden;

    /**
     * Create a new Protection.
     *
     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
     *                                    Leave this value at default unless you understand exactly what
     *                                        its ramifications are
     * @param bool $isConditional Flag indicating if this is a conditional style or not
     *                                    Leave this value at default unless you understand exactly what
     *                                        its ramifications are
     */
    public function __construct($isSupervisor = false, $isConditional = false)
    {
        // Supervisor?
        parent::__construct($isSupervisor);

        // Initialise values
        if (!$isConditional) {
            $this->locked = self::PROTECTION_INHERIT;
            $this->hidden = self::PROTECTION_INHERIT;
        }
    }

    /**
     * Get the shared style component for the currently active cell in currently active sheet.
     * Only used for style supervisor.
     *
     * @return Protection
     */
    public function getSharedComponent()
    {
        return $this->parent->getSharedComponent()->getProtection();
    }

    /**
     * Build style array from subcomponents.
     *
     * @param array $array
     *
     * @return array
     */
    public function getStyleArray($array)
    {
        return ['protection' => $array];
    }

    /**
     * Apply styles from array.
     *
     * <code>
     * $spreadsheet->getActiveSheet()->getStyle('B2')->getLocked()->applyFromArray(
     *     [
     *         'locked' => TRUE,
     *         'hidden' => FALSE
     *     ]
     * );
     * </code>
     *
     * @param array $pStyles Array containing style information
     *
     * @throws PhpSpreadsheetException
     *
     * @return Protection
     */
    public function applyFromArray(array $pStyles)
    {
        if ($this->isSupervisor) {
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
        } else {
            if (isset($pStyles['locked'])) {
                $this->setLocked($pStyles['locked']);
            }
            if (isset($pStyles['hidden'])) {
                $this->setHidden($pStyles['hidden']);
            }
        }

        return $this;
    }

    /**
     * Get locked.
     *
     * @return string
     */
    public function getLocked()
    {
        if ($this->isSupervisor) {
            return $this->getSharedComponent()->getLocked();
        }

        return $this->locked;
    }

    /**
     * Set locked.
     *
     * @param string $pValue see self::PROTECTION_*
     *
     * @return Protection
     */
    public function setLocked($pValue)
    {
        if ($this->isSupervisor) {
            $styleArray = $this->getStyleArray(['locked' => $pValue]);
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
        } else {
            $this->locked = $pValue;
        }

        return $this;
    }

    /**
     * Get hidden.
     *
     * @return string
     */
    public function getHidden()
    {
        if ($this->isSupervisor) {
            return $this->getSharedComponent()->getHidden();
        }

        return $this->hidden;
    }

    /**
     * Set hidden.
     *
     * @param string $pValue see self::PROTECTION_*
     *
     * @return Protection
     */
    public function setHidden($pValue)
    {
        if ($this->isSupervisor) {
            $styleArray = $this->getStyleArray(['hidden' => $pValue]);
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
        } else {
            $this->hidden = $pValue;
        }

        return $this;
    }

    /**
     * Get hash code.
     *
     * @return string Hash code
     */
    public function getHashCode()
    {
        if ($this->isSupervisor) {
            return $this->getSharedComponent()->getHashCode();
        }

        return md5(
            $this->locked .
            $this->hidden .
            __CLASS__
        );
    }
}


<?php

namespace PhpOffice\PhpSpreadsheet\Shared;

use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\RichText\RichText;

class Font
{
    // Methods for resolving autosize value
    const AUTOSIZE_METHOD_APPROX = 'approx';
    const AUTOSIZE_METHOD_EXACT = 'exact';

    private static $autoSizeMethods = [
        self::AUTOSIZE_METHOD_APPROX,
        self::AUTOSIZE_METHOD_EXACT,
    ];

    /** Character set codes used by BIFF5-8 in Font records */
    const CHARSET_ANSI_LATIN = 0x00;
    const CHARSET_SYSTEM_DEFAULT = 0x01;
    const CHARSET_SYMBOL = 0x02;
    const CHARSET_APPLE_ROMAN = 0x4D;
    const CHARSET_ANSI_JAPANESE_SHIFTJIS = 0x80;
    const CHARSET_ANSI_KOREAN_HANGUL = 0x81;
    const CHARSET_ANSI_KOREAN_JOHAB = 0x82;
    const CHARSET_ANSI_CHINESE_SIMIPLIFIED = 0x86; //    gb2312
    const CHARSET_ANSI_CHINESE_TRADITIONAL = 0x88; //    big5
    const CHARSET_ANSI_GREEK = 0xA1;
    const CHARSET_ANSI_TURKISH = 0xA2;
    const CHARSET_ANSI_VIETNAMESE = 0xA3;
    const CHARSET_ANSI_HEBREW = 0xB1;
    const CHARSET_ANSI_ARABIC = 0xB2;
    const CHARSET_ANSI_BALTIC = 0xBA;
    const CHARSET_ANSI_CYRILLIC = 0xCC;
    const CHARSET_ANSI_THAI = 0xDD;
    const CHARSET_ANSI_LATIN_II = 0xEE;
    const CHARSET_OEM_LATIN_I = 0xFF;

    //  XXX: Constants created!
    /** Font filenames */
    const ARIAL = 'arial.ttf';
    const ARIAL_BOLD = 'arialbd.ttf';
    const ARIAL_ITALIC = 'ariali.ttf';
    const ARIAL_BOLD_ITALIC = 'arialbi.ttf';

    const CALIBRI = 'CALIBRI.TTF';
    const CALIBRI_BOLD = 'CALIBRIB.TTF';
    const CALIBRI_ITALIC = 'CALIBRII.TTF';
    const CALIBRI_BOLD_ITALIC = 'CALIBRIZ.TTF';

    const COMIC_SANS_MS = 'comic.ttf';
    const COMIC_SANS_MS_BOLD = 'comicbd.ttf';

    const COURIER_NEW = 'cour.ttf';
    const COURIER_NEW_BOLD = 'courbd.ttf';
    const COURIER_NEW_ITALIC = 'couri.ttf';
    const COURIER_NEW_BOLD_ITALIC = 'courbi.ttf';

    const GEORGIA = 'georgia.ttf';
    const GEORGIA_BOLD = 'georgiab.ttf';
    const GEORGIA_ITALIC = 'georgiai.ttf';
    const GEORGIA_BOLD_ITALIC = 'georgiaz.ttf';

    const IMPACT = 'impact.ttf';

    const LIBERATION_SANS = 'LiberationSans-Regular.ttf';
    const LIBERATION_SANS_BOLD = 'LiberationSans-Bold.ttf';
    const LIBERATION_SANS_ITALIC = 'LiberationSans-Italic.ttf';
    const LIBERATION_SANS_BOLD_ITALIC = 'LiberationSans-BoldItalic.ttf';

    const LUCIDA_CONSOLE = 'lucon.ttf';
    const LUCIDA_SANS_UNICODE = 'l_10646.ttf';

    const MICROSOFT_SANS_SERIF = 'micross.ttf';

    const PALATINO_LINOTYPE = 'pala.ttf';
    const PALATINO_LINOTYPE_BOLD = 'palab.ttf';
    const PALATINO_LINOTYPE_ITALIC = 'palai.ttf';
    const PALATINO_LINOTYPE_BOLD_ITALIC = 'palabi.ttf';

    const SYMBOL = 'symbol.ttf';

    const TAHOMA = 'tahoma.ttf';
    const TAHOMA_BOLD = 'tahomabd.ttf';

    const TIMES_NEW_ROMAN = 'times.ttf';
    const TIMES_NEW_ROMAN_BOLD = 'timesbd.ttf';
    const TIMES_NEW_ROMAN_ITALIC = 'timesi.ttf';
    const TIMES_NEW_ROMAN_BOLD_ITALIC = 'timesbi.ttf';

    const TREBUCHET_MS = 'trebuc.ttf';
    const TREBUCHET_MS_BOLD = 'trebucbd.ttf';
    const TREBUCHET_MS_ITALIC = 'trebucit.ttf';
    const TREBUCHET_MS_BOLD_ITALIC = 'trebucbi.ttf';

    const VERDANA = 'verdana.ttf';
    const VERDANA_BOLD = 'verdanab.ttf';
    const VERDANA_ITALIC = 'verdanai.ttf';
    const VERDANA_BOLD_ITALIC = 'verdanaz.ttf';

    /**
     * AutoSize method.
     *
     * @var string
     */
    private static $autoSizeMethod = self::AUTOSIZE_METHOD_APPROX;

    /**
     * Path to folder containing TrueType font .ttf files.
     *
     * @var string
     */
    private static $trueTypeFontPath = null;

    /**
     * How wide is a default column for a given default font and size?
     * Empirical data found by inspecting real Excel files and reading off the pixel width
     * in Microsoft Office Excel 2007.
     *
     * @var array
     */
    public static $defaultColumnWidths = [
        'Arial' => [
            1 => ['px' => 24, 'width' => 12.00000000],
            2 => ['px' => 24, 'width' => 12.00000000],
            3 => ['px' => 32, 'width' => 10.66406250],
            4 => ['px' => 32, 'width' => 10.66406250],
            5 => ['px' => 40, 'width' => 10.00000000],
            6 => ['px' => 48, 'width' => 9.59765625],
            7 => ['px' => 48, 'width' => 9.59765625],
            8 => ['px' => 56, 'width' => 9.33203125],
            9 => ['px' => 64, 'width' => 9.14062500],
            10 => ['px' => 64, 'width' => 9.14062500],
        ],
        'Calibri' => [
            1 => ['px' => 24, 'width' => 12.00000000],
            2 => ['px' => 24, 'width' => 12.00000000],
            3 => ['px' => 32, 'width' => 10.66406250],
            4 => ['px' => 32, 'width' => 10.66406250],
            5 => ['px' => 40, 'width' => 10.00000000],
            6 => ['px' => 48, 'width' => 9.59765625],
            7 => ['px' => 48, 'width' => 9.59765625],
            8 => ['px' => 56, 'width' => 9.33203125],
            9 => ['px' => 56, 'width' => 9.33203125],
            10 => ['px' => 64, 'width' => 9.14062500],
            11 => ['px' => 64, 'width' => 9.14062500],
        ],
        'Verdana' => [
            1 => ['px' => 24, 'width' => 12.00000000],
            2 => ['px' => 24, 'width' => 12.00000000],
            3 => ['px' => 32, 'width' => 10.66406250],
            4 => ['px' => 32, 'width' => 10.66406250],
            5 => ['px' => 40, 'width' => 10.00000000],
            6 => ['px' => 48, 'width' => 9.59765625],
            7 => ['px' => 48, 'width' => 9.59765625],
            8 => ['px' => 64, 'width' => 9.14062500],
            9 => ['px' => 72, 'width' => 9.00000000],
            10 => ['px' => 72, 'width' => 9.00000000],
        ],
    ];

    /**
     * Set autoSize method.
     *
     * @param string $pValue see self::AUTOSIZE_METHOD_*
     *
     * @return bool Success or failure
     */
    public static function setAutoSizeMethod($pValue)
    {
        if (!in_array($pValue, self::$autoSizeMethods)) {
            return false;
        }
        self::$autoSizeMethod = $pValue;

        return true;
    }

    /**
     * Get autoSize method.
     *
     * @return string
     */
    public static function getAutoSizeMethod()
    {
        return self::$autoSizeMethod;
    }

    /**
     * Set the path to the folder containing .ttf files. There should be a trailing slash.
     * Typical locations on variout some platforms:
     *

     *
C:/Windows/Fonts/

     *
/usr/share/fonts/truetype/

     *
~/.fonts/

     *
.
     *
     * @param string $pValue
     */
    public static function setTrueTypeFontPath($pValue)
    {
        self::$trueTypeFontPath = $pValue;
    }

    /**
     * Get the path to the folder containing .ttf files.
     *
     * @return string
     */
    public static function getTrueTypeFontPath()
    {
        return self::$trueTypeFontPath;
    }

    /**
     * Calculate an (approximate) OpenXML column width, based on font size and text contained.
     *
     * @param \PhpOffice\PhpSpreadsheet\Style\Font $font Font object
     * @param RichText|string $cellText Text to calculate width
     * @param int $rotation Rotation angle
     * @param null|\PhpOffice\PhpSpreadsheet\Style\Font $defaultFont Font object
     *
     * @return int Column width
     */
    public static function calculateColumnWidth(\PhpOffice\PhpSpreadsheet\Style\Font $font, $cellText = '', $rotation = 0, \PhpOffice\PhpSpreadsheet\Style\Font $defaultFont = null)
    {
        // If it is rich text, use plain text
        if ($cellText instanceof RichText) {
            $cellText = $cellText->getPlainText();
        }

        // Special case if there are one or more newline characters ("\n")
        if (strpos($cellText, "\n") !== false) {
            $lineTexts = explode("\n", $cellText);
            $lineWidths = [];
            foreach ($lineTexts as $lineText) {
                $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont);
            }

            return max($lineWidths); // width of longest line in cell
        }

        // Try to get the exact text width in pixels
        $approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX;
        if (!$approximate) {
            $columnWidthAdjust = ceil(self::getTextWidthPixelsExact('n', $font, 0) * 1.07);

            try {
                // Width of text in pixels excl. padding
                // and addition because Excel adds some padding, just use approx width of 'n' glyph
                $columnWidth = self::getTextWidthPixelsExact($cellText, $font, $rotation) + $columnWidthAdjust;
            } catch (PhpSpreadsheetException $e) {
                $approximate = true;
            }
        }

        if ($approximate) {
            $columnWidthAdjust = self::getTextWidthPixelsApprox('n', $font, 0);
            // Width of text in pixels excl. padding, approximation
            // and addition because Excel adds some padding, just use approx width of 'n' glyph
            $columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust;
        }

        // Convert from pixel width to column width
        $columnWidth = Drawing::pixelsToCellDimension($columnWidth, $defaultFont);

        // Return
        return round($columnWidth, 6);
    }

    /**
     * Get GD text width in pixels for a string of text in a certain font at a certain rotation angle.
     *
     * @param string $text
     * @param \PhpOffice\PhpSpreadsheet\Style\Font
     * @param int $rotation
     *
     * @throws PhpSpreadsheetException
     *
     * @return int
     */
    public static function getTextWidthPixelsExact($text, \PhpOffice\PhpSpreadsheet\Style\Font $font, $rotation = 0)
    {
        if (!function_exists('imagettfbbox')) {
            throw new PhpSpreadsheetException('GD library needs to be enabled');
        }

        // font size should really be supplied in pixels in GD2,
        // but since GD2 seems to assume 72dpi, pixels and points are the same
        $fontFile = self::getTrueTypeFontFileFromFont($font);
        $textBox = imagettfbbox($font->getSize(), $rotation, $fontFile, $text);

        // Get corners positions
        $lowerLeftCornerX = $textBox[0];
        $lowerRightCornerX = $textBox[2];
        $upperRightCornerX = $textBox[4];
        $upperLeftCornerX = $textBox[6];

        // Consider the rotation when calculating the width
        $textWidth = max($lowerRightCornerX - $upperLeftCornerX, $upperRightCornerX - $lowerLeftCornerX);

        return $textWidth;
    }

    /**
     * Get approximate width in pixels for a string of text in a certain font at a certain rotation angle.
     *
     * @param string $columnText
     * @param \PhpOffice\PhpSpreadsheet\Style\Font $font
     * @param int $rotation
     *
     * @return int Text width in pixels (no padding added)
     */
    public static function getTextWidthPixelsApprox($columnText, \PhpOffice\PhpSpreadsheet\Style\Font $font, $rotation = 0)
    {
        $fontName = $font->getName();
        $fontSize = $font->getSize();

        // Calculate column width in pixels. We assume fixed glyph width. Result varies with font name and size.
        switch ($fontName) {
            case 'Calibri':
                // value 8.26 was found via interpolation by inspecting real Excel files with Calibri 11 font.
                $columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText));
                $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size
                break;
            case 'Arial':
                // value 8 was set because of experience in different exports at Arial 10 font.
                $columnWidth = (int) (8 * StringHelper::countCharacters($columnText));
                $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size
                break;
            case 'Verdana':
                // value 8 was found via interpolation by inspecting real Excel files with Verdana 10 font.
                $columnWidth = (int) (8 * StringHelper::countCharacters($columnText));
                $columnWidth = $columnWidth * $fontSize / 10; // extrapolate from font size
                break;
            default:
                // just assume Calibri
                $columnWidth = (int) (8.26 * StringHelper::countCharacters($columnText));
                $columnWidth = $columnWidth * $fontSize / 11; // extrapolate from font size
                break;
        }

        // Calculate approximate rotated column width
        if ($rotation !== 0) {
            if ($rotation == -165) {
                // stacked text
                $columnWidth = 4; // approximation
            } else {
                // rotated text
                $columnWidth = $columnWidth * cos(deg2rad($rotation))
                                + $fontSize * abs(sin(deg2rad($rotation))) / 5; // approximation
            }
        }

        // pixel width is an integer
        return (int) $columnWidth;
    }

    /**
     * Calculate an (approximate) pixel size, based on a font points size.
     *
     * @param int $fontSizeInPoints Font size (in points)
     *
     * @return int Font size (in pixels)
     */
    public static function fontSizeToPixels($fontSizeInPoints)
    {
        return (int) ((4 / 3) * $fontSizeInPoints);
    }

    /**
     * Calculate an (approximate) pixel size, based on inch size.
     *
     * @param int $sizeInInch Font size (in inch)
     *
     * @return int Size (in pixels)
     */
    public static function inchSizeToPixels($sizeInInch)
    {
        return $sizeInInch * 96;
    }

    /**
     * Calculate an (approximate) pixel size, based on centimeter size.
     *
     * @param int $sizeInCm Font size (in centimeters)
     *
     * @return float Size (in pixels)
     */
    public static function centimeterSizeToPixels($sizeInCm)
    {
        return $sizeInCm * 37.795275591;
    }

    /**
     * Returns the font path given the font.
     *
     * @param \PhpOffice\PhpSpreadsheet\Style\Font $font
     *
     * @return string Path to TrueType font file
     */
    public static function getTrueTypeFontFileFromFont($font)
    {
        if (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath)) {
            throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified');
        }

        $name = $font->getName();
        $bold = $font->getBold();
        $italic = $font->getItalic();

        // Check if we can map font to true type font file
        switch ($name) {
            case 'Arial':
                $fontFile = (
                    $bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD)
                          : ($italic ? self::ARIAL_ITALIC : self::ARIAL)
                );

                break;
            case 'Calibri':
                $fontFile = (
                    $bold ? ($italic ? self::CALIBRI_BOLD_ITALIC : self::CALIBRI_BOLD)
                          : ($italic ? self::CALIBRI_ITALIC : self::CALIBRI)
                );

                break;
            case 'Courier New':
                $fontFile = (
                    $bold ? ($italic ? self::COURIER_NEW_BOLD_ITALIC : self::COURIER_NEW_BOLD)
                          : ($italic ? self::COURIER_NEW_ITALIC : self::COURIER_NEW)
                );

                break;
            case 'Comic Sans MS':
                $fontFile = (
                    $bold ? self::COMIC_SANS_MS_BOLD : self::COMIC_SANS_MS
                );

                break;
            case 'Georgia':
                $fontFile = (
                    $bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD)
                          : ($italic ? self::GEORGIA_ITALIC : self::GEORGIA)
                );

                break;
            case 'Impact':
                $fontFile = self::IMPACT;

                break;
            case 'Liberation Sans':
                $fontFile = (
                    $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD)
                          : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS)
                );

                break;
            case 'Lucida Console':
                $fontFile = self::LUCIDA_CONSOLE;

                break;
            case 'Lucida Sans Unicode':
                $fontFile = self::LUCIDA_SANS_UNICODE;

                break;
            case 'Microsoft Sans Serif':
                $fontFile = self::MICROSOFT_SANS_SERIF;

                break;
            case 'Palatino Linotype':
                $fontFile = (
                    $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD)
                          : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE)
                );

                break;
            case 'Symbol':
                $fontFile = self::SYMBOL;

                break;
            case 'Tahoma':
                $fontFile = (
                    $bold ? self::TAHOMA_BOLD : self::TAHOMA
                );

                break;
            case 'Times New Roman':
                $fontFile = (
                    $bold ? ($italic ? self::TIMES_NEW_ROMAN_BOLD_ITALIC : self::TIMES_NEW_ROMAN_BOLD)
                          : ($italic ? self::TIMES_NEW_ROMAN_ITALIC : self::TIMES_NEW_ROMAN)
                );

                break;
            case 'Trebuchet MS':
                $fontFile = (
                    $bold ? ($italic ? self::TREBUCHET_MS_BOLD_ITALIC : self::TREBUCHET_MS_BOLD)
                          : ($italic ? self::TREBUCHET_MS_ITALIC : self::TREBUCHET_MS)
                );

                break;
            case 'Verdana':
                $fontFile = (
                    $bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD)
                          : ($italic ? self::VERDANA_ITALIC : self::VERDANA)
                );

                break;
            default:
                throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file');

                break;
        }

        $fontFile = self::$trueTypeFontPath . $fontFile;

        // Check if file actually exists
        if (!file_exists($fontFile)) {
            throw new PhpSpreadsheetException('TrueType Font file not found');
        }

        return $fontFile;
    }

    /**
     * Returns the associated charset for the font name.
     *
     * @param string $name Font name
     *
     * @return int Character set code
     */
    public static function getCharsetFromFontName($name)
    {
        switch ($name) {
            // Add more cases. Check FONT records in real Excel files.
            case 'EucrosiaUPC':
                return self::CHARSET_ANSI_THAI;
            case 'Wingdings':
                return self::CHARSET_SYMBOL;
            case 'Wingdings 2':
                return self::CHARSET_SYMBOL;
            case 'Wingdings 3':
                return self::CHARSET_SYMBOL;
            default:
                return self::CHARSET_ANSI_LATIN;
        }
    }

    /**
     * Get the effective column width for columns without a column dimension or column with width -1
     * For example, for Calibri 11 this is 9.140625 (64 px).
     *
     * @param \PhpOffice\PhpSpreadsheet\Style\Font $font The workbooks default font
     * @param bool $pPixels true = return column width in pixels, false = return in OOXML units
     *
     * @return mixed Column width
     */
    public static function getDefaultColumnWidthByFont(\PhpOffice\PhpSpreadsheet\Style\Font $font, $pPixels = false)
    {
        if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) {
            // Exact width can be determined
            $columnWidth = $pPixels ?
                self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px']
                    : self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width'];
        } else {
            // We don't have data for this particular font and size, use approximation by
            // extrapolating from Calibri 11
            $columnWidth = $pPixels ?
                self::$defaultColumnWidths['Calibri'][11]['px']
                    : self::$defaultColumnWidths['Calibri'][11]['width'];
            $columnWidth = $columnWidth * $font->getSize() / 11;

            // Round pixels to closest integer
            if ($pPixels) {
                $columnWidth = (int) round($columnWidth);
            }
        }

        return $columnWidth;
    }

    /**
     * Get the effective row height for rows without a row dimension or rows with height -1
     * For example, for Calibri 11 this is 15 points.
     *
     * @param \PhpOffice\PhpSpreadsheet\Style\Font $font The workbooks default font
     *
     * @return float Row height in points
     */
    public static function getDefaultRowHeightByFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
    {
        switch ($font->getName()) {
            case 'Arial':
                switch ($font->getSize()) {
                    case 10:
                        // inspection of Arial 10 workbook says 12.75pt ~17px
                        $rowHeight = 12.75;

                        break;
                    case 9:
                        // inspection of Arial 9 workbook says 12.00pt ~16px
                        $rowHeight = 12;

                        break;
                    case 8:
                        // inspection of Arial 8 workbook says 11.25pt ~15px
                        $rowHeight = 11.25;

                        break;
                    case 7:
                        // inspection of Arial 7 workbook says 9.00pt ~12px
                        $rowHeight = 9;

                        break;
                    case 6:
                    case 5:
                        // inspection of Arial 5,6 workbook says 8.25pt ~11px
                        $rowHeight = 8.25;

                        break;
                    case 4:
                        // inspection of Arial 4 workbook says 6.75pt ~9px
                        $rowHeight = 6.75;

                        break;
                    case 3:
                        // inspection of Arial 3 workbook says 6.00pt ~8px
                        $rowHeight = 6;

                        break;
                    case 2:
                    case 1:
                        // inspection of Arial 1,2 workbook says 5.25pt ~7px
                        $rowHeight = 5.25;

                        break;
                    default:
                        // use Arial 10 workbook as an approximation, extrapolation
                        $rowHeight = 12.75 * $font->getSize() / 10;

                        break;
                }

                break;
            case 'Calibri':
                switch ($font->getSize()) {
                    case 11:
                        // inspection of Calibri 11 workbook says 15.00pt ~20px
                        $rowHeight = 15;

                        break;
                    case 10:
                        // inspection of Calibri 10 workbook says 12.75pt ~17px
                        $rowHeight = 12.75;

                        break;
                    case 9:
                        // inspection of Calibri 9 workbook says 12.00pt ~16px
                        $rowHeight = 12;

                        break;
                    case 8:
                        // inspection of Calibri 8 workbook says 11.25pt ~15px
                        $rowHeight = 11.25;

                        break;
                    case 7:
                        // inspection of Calibri 7 workbook says 9.00pt ~12px
                        $rowHeight = 9;

                        break;
                    case 6:
                    case 5:
                        // inspection of Calibri 5,6 workbook says 8.25pt ~11px
                        $rowHeight = 8.25;

                        break;
                    case 4:
                        // inspection of Calibri 4 workbook says 6.75pt ~9px
                        $rowHeight = 6.75;

                        break;
                    case 3:
                        // inspection of Calibri 3 workbook says 6.00pt ~8px
                        $rowHeight = 6.00;

                        break;
                    case 2:
                    case 1:
                        // inspection of Calibri 1,2 workbook says 5.25pt ~7px
                        $rowHeight = 5.25;

                        break;
                    default:
                        // use Calibri 11 workbook as an approximation, extrapolation
                        $rowHeight = 15 * $font->getSize() / 11;

                        break;
                }

                break;
            case 'Verdana':
                switch ($font->getSize()) {
                    case 10:
                        // inspection of Verdana 10 workbook says 12.75pt ~17px
                        $rowHeight = 12.75;

                        break;
                    case 9:
                        // inspection of Verdana 9 workbook says 11.25pt ~15px
                        $rowHeight = 11.25;

                        break;
                    case 8:
                        // inspection of Verdana 8 workbook says 10.50pt ~14px
                        $rowHeight = 10.50;

                        break;
                    case 7:
                        // inspection of Verdana 7 workbook says 9.00pt ~12px
                        $rowHeight = 9.00;

                        break;
                    case 6:
                    case 5:
                        // inspection of Verdana 5,6 workbook says 8.25pt ~11px
                        $rowHeight = 8.25;

                        break;
                    case 4:
                        // inspection of Verdana 4 workbook says 6.75pt ~9px
                        $rowHeight = 6.75;

                        break;
                    case 3:
                        // inspection of Verdana 3 workbook says 6.00pt ~8px
                        $rowHeight = 6;

                        break;
                    case 2:
                    case 1:
                        // inspection of Verdana 1,2 workbook says 5.25pt ~7px
                        $rowHeight = 5.25;

                        break;
                    default:
                        // use Verdana 10 workbook as an approximation, extrapolation
                        $rowHeight = 12.75 * $font->getSize() / 10;

                        break;
                }

                break;
            default:
                // just use Calibri as an approximation
                $rowHeight = 15 * $font->getSize() / 11;

                break;
        }

        return $rowHeight;
    }
}


<?php

namespace PhpOffice\PhpSpreadsheet\Writer\Ods;

use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;

class Styles extends WriterPart
{
    /**
     * Write styles.xml to XML format.
     *
     * @param Spreadsheet $spreadsheet
     *
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     *
     * @return string XML Output
     */
    public function write(Spreadsheet $spreadsheet = null)
    {
        $objWriter = null;
        if ($this->getParentWriter()->getUseDiskCaching()) {
            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
        } else {
            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
        }

        // XML header
        $objWriter->startDocument('1.0', 'UTF-8');

        // Content
        $objWriter->startElement('office:document-styles');
        $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
        $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
        $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
        $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
        $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0');
        $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
        $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
        $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
        $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0');
        $objWriter->writeAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
        $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0');
        $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0');
        $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0');
        $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0');
        $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML');
        $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0');
        $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0');
        $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
        $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer');
        $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc');
        $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events');
        $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report');
        $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2');
        $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
        $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#');
        $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table');
        $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/');
        $objWriter->writeAttribute('office:version', '1.2');

        $objWriter->writeElement('office:font-face-decls');
        $objWriter->writeElement('office:styles');
        $objWriter->writeElement('office:automatic-styles');
        $objWriter->writeElement('office:master-styles');
        $objWriter->endElement();

        return $objWriter->getData();
    }
}


<?php
/**
 * Command registry
 *
 * This file is part of periodic
 *
 * periodic is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; version 3 of the License.
 *
 * periodic is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with periodic; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @package Core
 * @version $Revision$
 * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
 */

namespace Arbit\Periodic;

use Arbit\XML;

/**
 * Command registry
 *
 * A simple static-only command registry, where commands can be registered with
 * their associated implementations.
 */
class CommandRegistry
{
    /**
     * List of commands with their associated class names of their
     * implementations.
     *
     * @var array
     */
    protected $commands = array();

    public function __construct()
    {
        // Register default commands
        $this->registerCommand( 'fs.copy', new Command\FileSystem\Copy() );
        $this->registerCommand( 'fs.remove', new Command\FileSystem\Remove() );
        $this->registerCommand( 'system.exec', new Command\System\Exec() );
    }

    /**
     * Register new command
     *
     * Register a new command, specified by its name and the associated
     * implementation specified by its class name.
     *
     * @param string $name
     * @param Command $command
     * @return void
     */
    public function registerCommand( $name, Command $command )
    {
        $this->commands[$name] = $command;
    }

    /**
     * Get command
     *
     * Returns the Command object, or false on failure.
     *
     * @param string $command
     * @param Logger $logger
     * @return Command
     */
    public function get( $command, Logger $logger )
    {
        if ( !isset( $this->commands[$command] ) )
        {
            $logger->log(
                "Unknown command '$command'.",
                Logger::ERROR
            );
            return false;
        }

        return $this->commands[$command];
    }
}


<?php

namespace PhpOffice\PhpSpreadsheet\Cell;

use PhpOffice\PhpSpreadsheet\Exception;

/**
 * Helper class to manipulate cell coordinates.
 *
 * Columns indexes and rows are always based on 1, **not** on 0. This match the behavior
 * that Excel users are used to, and also match the Excel functions `COLUMN()` and `ROW()`.
 */
abstract class Coordinate
{
    /**
     * Default range variable constant.
     *
     * @var string
     */
    const DEFAULT_RANGE = 'A1:A1';

    /**
     * Coordinate from string.
     *
     * @param string $pCoordinateString eg: 'A1'
     *
     * @throws Exception
     *
     * @return string[] Array containing column and row (indexes 0 and 1)
     */
    public static function coordinateFromString($pCoordinateString)
    {
        if (preg_match('/^([$]?[A-Z]{1,3})([$]?\\d{1,7})$/', $pCoordinateString, $matches)) {
            return [$matches[1], $matches[2]];
        } elseif (self::coordinateIsRange($pCoordinateString)) {
            throw new Exception('Cell coordinate string can not be a range of cells');
        } elseif ($pCoordinateString == '') {
            throw new Exception('Cell coordinate can not be zero-length string');
        }

        throw new Exception('Invalid cell coordinate ' . $pCoordinateString);
    }

    /**
     * Checks if a coordinate represents a range of cells.
     *
     * @param string $coord eg: 'A1' or 'A1:A2' or 'A1:A2,C1:C2'
     *
     * @return bool Whether the coordinate represents a range of cells
     */
    public static function coordinateIsRange($coord)
    {
        return (strpos($coord, ':') !== false) || (strpos($coord, ',') !== false);
    }

    /**
     * Make string row, column or cell coordinate absolute.
     *
     * @param string $pCoordinateString e.g. 'A' or '1' or 'A1'
     *                    Note that this value can be a row or column reference as well as a cell reference
     *
     * @throws Exception
     *
     * @return string Absolute coordinate        e.g. '$A' or '$1' or '$A$1'
     */
    public static function absoluteReference($pCoordinateString)
    {
        if (self::coordinateIsRange($pCoordinateString)) {
            throw new Exception('Cell coordinate string can not be a range of cells');
        }

        // Split out any worksheet name from the reference
        $worksheet = '';
        $cellAddress = explode('!', $pCoordinateString);
        if (count($cellAddress) > 1) {
            list($worksheet, $pCoordinateString) = $cellAddress;
        }
        if ($worksheet > '') {
            $worksheet .= '!';
        }

        // Create absolute coordinate
        if (ctype_digit($pCoordinateString)) {
            return $worksheet . '$' . $pCoordinateString;
        } elseif (ctype_alpha($pCoordinateString)) {
            return $worksheet . '$' . strtoupper($pCoordinateString);
        }

        return $worksheet . self::absoluteCoordinate($pCoordinateString);
    }

    /**
     * Make string coordinate absolute.
     *
     * @param string $pCoordinateString e.g. 'A1'
     *
     * @throws Exception
     *
     * @return string Absolute coordinate        e.g. '$A$1'
     */
    public static function absoluteCoordinate($pCoordinateString)
    {
        if (self::coordinateIsRange($pCoordinateString)) {
            throw new Exception('Cell coordinate string can not be a range of cells');
        }

        // Split out any worksheet name from the coordinate
        $worksheet = '';
        $cellAddress = explode('!', $pCoordinateString);
        if (count($cellAddress) > 1) {
            list($worksheet, $pCoordinateString) = $cellAddress;
        }
        if ($worksheet > '') {
            $worksheet .= '!';
        }

        // Create absolute coordinate
        list($column, $row) = self::coordinateFromString($pCoordinateString);
        $column = ltrim($column, '$');
        $row = ltrim($row, '$');

        return $worksheet . '$' . $column . '$' . $row;
    }

    /**
     * Split range into coordinate strings.
     *
     * @param string $pRange e.g. 'B4:D9' or 'B4:D9,H2:O11' or 'B4'
     *
     * @return array Array containing one or more arrays containing one or two coordinate strings
     *                                e.g. ['B4','D9'] or [['B4','D9'], ['H2','O11']]
     *                                        or ['B4']
     */
    public static function splitRange($pRange)
    {
        // Ensure $pRange is a valid range
        if (empty($pRange)) {
            $pRange = self::DEFAULT_RANGE;
        }

        $exploded = explode(',', $pRange);
        $counter = count($exploded);
        for ($i = 0; $i < $counter; ++$i) {
            $exploded[$i] = explode(':', $exploded[$i]);
        }

        return $exploded;
    }

    /**
     * Build range from coordinate strings.
     *
     * @param array $pRange Array containg one or more arrays containing one or two coordinate strings
     *
     * @throws Exception
     *
     * @return string String representation of $pRange
     */
    public static function buildRange(array $pRange)
    {
        // Verify range
        if (empty($pRange) || !is_array($pRange[0])) {
            throw new Exception('Range does not contain any information');
        }

        // Build range
        $imploded = [];
        $counter = count($pRange);
        for ($i = 0; $i < $counter; ++$i) {
            $pRange[$i] = implode(':', $pRange[$i]);
        }
        $imploded = implode(',', $pRange);

        return $imploded;
    }

    /**
     * Calculate range boundaries.
     *
     * @param string $pRange Cell range (e.g. A1:A1)
     *
     * @return array Range coordinates [Start Cell, End Cell]
     *                    where Start Cell and End Cell are arrays (Column Number, Row Number)
     */
    public static function rangeBoundaries($pRange)
    {
        // Ensure $pRange is a valid range
        if (empty($pRange)) {
            $pRange = self::DEFAULT_RANGE;
        }

        // Uppercase coordinate
        $pRange = strtoupper($pRange);

        // Extract range
        if (strpos($pRange, ':') === false) {
            $rangeA = $rangeB = $pRange;
        } else {
            list($rangeA, $rangeB) = explode(':', $pRange);
        }

        // Calculate range outer borders
        $rangeStart = self::coordinateFromString($rangeA);
        $rangeEnd = self::coordinateFromString($rangeB);

        // Translate column into index
        $rangeStart[0] = self::columnIndexFromString($rangeStart[0]);
        $rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]);

        return [$rangeStart, $rangeEnd];
    }

    /**
     * Calculate range dimension.
     *
     * @param string $pRange Cell range (e.g. A1:A1)
     *
     * @return array Range dimension (width, height)
     */
    public static function rangeDimension($pRange)
    {
        // Calculate range outer borders
        list($rangeStart, $rangeEnd) = self::rangeBoundaries($pRange);

        return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)];
    }

    /**
     * Calculate range boundaries.
     *
     * @param string $pRange Cell range (e.g. A1:A1)
     *
     * @return array Range coordinates [Start Cell, End Cell]
     *                    where Start Cell and End Cell are arrays [Column ID, Row Number]
     */
    public static function getRangeBoundaries($pRange)
    {
        // Ensure $pRange is a valid range
        if (empty($pRange)) {
            $pRange = self::DEFAULT_RANGE;
        }

        // Uppercase coordinate
        $pRange = strtoupper($pRange);

        // Extract range
        if (strpos($pRange, ':') === false) {
            $rangeA = $rangeB = $pRange;
        } else {
            list($rangeA, $rangeB) = explode(':', $pRange);
        }

        return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)];
    }

    /**
     * Column index from string.
     *
     * @param string $pString eg 'A'
     *
     * @return int Column index (A = 1)
     */
    public static function columnIndexFromString($pString)
    {
        //    Using a lookup cache adds a slight memory overhead, but boosts speed
        //    caching using a static within the method is faster than a class static,
        //        though it's additional memory overhead
        static $indexCache = [];

        if (isset($indexCache[$pString])) {
            return $indexCache[$pString];
        }
        //    It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array rather than use ord()
        //        and make it case insensitive to get rid of the strtoupper() as well. Because it's a static, there's no significant
        //        memory overhead either
        static $columnLookup = [
            'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, 'K' => 11, 'L' => 12, 'M' => 13,
            'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26,
            'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13,
            'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26,
        ];

        //    We also use the language construct isset() rather than the more costly strlen() function to match the length of $pString
        //        for improved performance
        if (isset($pString[0])) {
            if (!isset($pString[1])) {
                $indexCache[$pString] = $columnLookup[$pString];

                return $indexCache[$pString];
            } elseif (!isset($pString[2])) {
                $indexCache[$pString] = $columnLookup[$pString[0]] * 26 + $columnLookup[$pString[1]];

                return $indexCache[$pString];
            } elseif (!isset($pString[3])) {
                $indexCache[$pString] = $columnLookup[$pString[0]] * 676 + $columnLookup[$pString[1]] * 26 + $columnLookup[$pString[2]];

                return $indexCache[$pString];
            }
        }

        throw new Exception('Column string index can not be ' . ((isset($pString[0])) ? 'longer than 3 characters' : 'empty'));
    }

    /**
     * String from column index.
     *
     * @param int $columnIndex Column index (A = 1)
     *
     * @return string
     */
    public static function stringFromColumnIndex($columnIndex)
    {
        static $indexCache = [];

        if (!isset($indexCache[$columnIndex])) {
            $indexValue = $columnIndex;
            $base26 = null;
            do {
                $characterValue = ($indexValue % 26) ?: 26;
                $indexValue = ($indexValue - $characterValue) / 26;
                $base26 = chr($characterValue + 64) . ($base26 ?: '');
            } while ($indexValue > 0);
            $indexCache[$columnIndex] = $base26;
        }

        return $indexCache[$columnIndex];
    }

    /**
     * Extract all cell references in range.
     *
     * @param string $pRange Range (e.g. A1 or A1:C10 or A1:E10 A20:E25)
     *
     * @return array Array containing single cell references
     */
    public static function extractAllCellReferencesInRange($pRange)
    {
        // Returnvalue
        $returnValue = [];

        // Explode spaces
        $cellBlocks = explode(' ', str_replace('$', '', strtoupper($pRange)));
        foreach ($cellBlocks as $cellBlock) {
            // Single cell?
            if (!self::coordinateIsRange($cellBlock)) {
                $returnValue[] = $cellBlock;

                continue;
            }

            // Range...
            $ranges = self::splitRange($cellBlock);
            foreach ($ranges as $range) {
                // Single cell?
                if (!isset($range[1])) {
                    $returnValue[] = $range[0];

                    continue;
                }

                // Range...
                list($rangeStart, $rangeEnd) = $range;
                sscanf($rangeStart, '%[A-Z]%d', $startCol, $startRow);
                sscanf($rangeEnd, '%[A-Z]%d', $endCol, $endRow);
                ++$endCol;

                // Current data
                $currentCol = $startCol;
                $currentRow = $startRow;

                // Loop cells
                while ($currentCol != $endCol) {
                    while ($currentRow <= $endRow) {
                        $returnValue[] = $currentCol . $currentRow;
                        ++$currentRow;
                    }
                    ++$currentCol;
                    $currentRow = $startRow;
                }
            }
        }

        //    Sort the result by column and row
        $sortKeys = [];
        foreach (array_unique($returnValue) as $coord) {
            sscanf($coord, '%[A-Z]%d', $column, $row);
            $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
        }
        ksort($sortKeys);

        // Return value
        return array_values($sortKeys);
    }

    /**
     * Convert an associative array of single cell coordinates to values to an associative array
     * of cell ranges to values.  Only adjacent cell coordinates with the same
     * value will be merged.  If the value is an object, it must implement the method getHashCode().
     *
     * For example, this function converts:
     *
     *    [ 'A1' => 'x', 'A2' => 'x', 'A3' => 'x', 'A4' => 'y' ]
     *
     * to:
     *
     *    [ 'A1:A3' => 'x', 'A4' => 'y' ]
     *
     * @param array $pCoordCollection associative array mapping coordinates to values
     *
     * @return array associative array mapping coordinate ranges to valuea
     */
    public static function mergeRangesInCollection(array $pCoordCollection)
    {
        $hashedValues = [];
        $mergedCoordCollection = [];

        foreach ($pCoordCollection as $coord => $value) {
            if (self::coordinateIsRange($coord)) {
                $mergedCoordCollection[$coord] = $value;

                continue;
            }

            list($column, $row) = self::coordinateFromString($coord);
            $row = (int) (ltrim($row, '$'));
            $hashCode = $column . '-' . (is_object($value) ? $value->getHashCode() : $value);

            if (!isset($hashedValues[$hashCode])) {
                $hashedValues[$hashCode] = (object) [
                    'value' => $value,
                    'col' => $column,
                    'rows' => [$row],
                ];
            } else {
                $hashedValues[$hashCode]->rows[] = $row;
            }
        }

        ksort($hashedValues);

        foreach ($hashedValues as $hashedValue) {
            sort($hashedValue->rows);
            $rowStart = null;
            $rowEnd = null;
            $ranges = [];

            foreach ($hashedValue->rows as $row) {
                if ($rowStart === null) {
                    $rowStart = $row;
                    $rowEnd = $row;
                } elseif ($rowEnd === $row - 1) {
                    $rowEnd = $row;
                } else {
                    if ($rowStart == $rowEnd) {
                        $ranges[] = $hashedValue->col . $rowStart;
                    } else {
                        $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
                    }

                    $rowStart = $row;
                    $rowEnd = $row;
                }
            }

            if ($rowStart !== null) {
                if ($rowStart == $rowEnd) {
                    $ranges[] = $hashedValue->col . $rowStart;
                } else {
                    $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
                }
            }

            foreach ($ranges as $range) {
                $mergedCoordCollection[$range] = $hashedValue->value;
            }
        }

        return $mergedCoordCollection;
    }
}


<?php

namespace PhpOffice\PhpSpreadsheet\Writer\Pdf;

use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use PhpOffice\PhpSpreadsheet\Writer\Pdf;

class Dompdf extends Pdf
{
    /**
     * Gets the implementation of external PDF library that should be used.
     *
     * @return \Dompdf\Dompdf implementation
     */
    protected function createExternalWriterInstance()
    {
        return new \Dompdf\Dompdf();
    }

    /**
     * Save Spreadsheet to file.
     *
     * @param string $pFilename Name of the file to save as
     *
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     */
    public function save($pFilename)
    {
        $fileHandle = parent::prepareForSave($pFilename);

        //  Default PDF paper size
        $paperSize = 'LETTER'; //    Letter    (8.5 in. by 11 in.)

        //  Check for paper size and page orientation
        if ($this->getSheetIndex() === null) {
            $orientation = ($this->spreadsheet->getSheet(0)->getPageSetup()->getOrientation()
                == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
            $printPaperSize = $this->spreadsheet->getSheet(0)->getPageSetup()->getPaperSize();
        } else {
            $orientation = ($this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getOrientation()
                == PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P';
            $printPaperSize = $this->spreadsheet->getSheet($this->getSheetIndex())->getPageSetup()->getPaperSize();
        }

        $orientation = ($orientation == 'L') ? 'landscape' : 'portrait';

        //  Override Page Orientation
        if ($this->getOrientation() !== null) {
            $orientation = ($this->getOrientation() == PageSetup::ORIENTATION_DEFAULT)
                ? PageSetup::ORIENTATION_PORTRAIT
                : $this->getOrientation();
        }
        //  Override Paper Size
        if ($this->getPaperSize() !== null) {
            $printPaperSize = $this->getPaperSize();
        }

        if (isset(self::$paperSizes[$printPaperSize])) {
            $paperSize = self::$paperSizes[$printPaperSize];
        }

        //  Create PDF
        $pdf = $this->createExternalWriterInstance();
        $pdf->setPaper(strtolower($paperSize), $orientation);

        $pdf->loadHtml(
            $this->generateHTMLHeader(false) .
            $this->generateSheetData() .
            $this->generateHTMLFooter()
        );
        $pdf->render();

        //  Write to file
        fwrite($fileHandle, $pdf->output());

        parent::restoreStateAfterSave($fileHandle);
    }
}


<?php
/**
 * Peridoc executor
 *
 * This file is part of periodic
 *
 * periodic is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; version 3 of the License.
 *
 * periodic is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with periodic; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @package Core
 * @version $Revision$
 * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPL
 */

namespace Arbit\Periodic;

/**
 * Peridoc executor
 *
 * Manages the execution of scheduled tasks, as defined by the associated
 * crontab.
 */
class Executor
{
    /**
     * Crontable, which consists of a list of Cronjob objects.
     *
     * @var array(Cronjob)
     */
    protected $crontab;

    /**
     * Rescheduled tasks
     *
     * @var array
     */
    protected $rescheduled = array();

    /**
     * Logger class, which receives all log messages
     *
     * @var Logger
     */
    protected $logger;

    /**
     * Directory to store locak and last run information
     *
     * @var string
     */
    protected $lockDir;

    /**
     * Task factory
     *
     * @var TaskFactory
     */
    protected $taskFactory;

    /**
     * Constant indicating success of command run
     */
    const SUCCESS    = 1;

    /**
     * Constant indicating, that the task should be aborted because of the
     * current command.
     */
    const ABORT      = 2;

    /**
     * Constant indicating that the current command caused an error.
     */
    const ERROR      = 4;

    /**
     * Constant indicating a temporary failure, so that the task should be
     * rescheduled.
     */
    const RESCHEDULE = 8;

    /**
     * Construct the executor
     *
     * Construct the executor from the given cron table and a logger
     * implementing the Logger interface.
     *
     * @param string $crontab
     * @param Logger $logger
     * @param string $lockDir
     * @return void
     */
    public function __construct( $crontab, TaskFactory $taskFactory, Logger $logger, $lockDir )
    {
        $this->parseCrontab( $crontab );
        $this->taskFactory = $taskFactory;
        $this->logger      = $logger;
        $this->lockDir     = $lockDir;
    }

    /**
     * Parse given cron table
     *
     * All lines, which are not empty or start with a # or ; character are
     * considered cron lines parsed as such.
     *
     * @param string $crontab
     * @return void
     */
    protected function parseCrontab( $crontab )
    {
        $lines = preg_split( '(\r\n|\r|\n)', $crontab );
        $this->crontab = array();
        foreach ( $lines as $line )
        {
            $line = trim( $line );
            if ( !empty( $line ) &&
                 ( $line[0] !== '#' ) &&
                 ( $line[0] !== ';' ) )
            {
                $this->crontab[] = new Cronjob( $line );
            }
        }
    }

    /**
     * Execute executor
     *
     * This method will check if there are any tasks to be run since the last
     * check. If this is the first check no tasks will be executed.
     *
     * If there are tasks to execute the method will try to aquire a lock, to
     * ensure that not multiple executors try to execute the same task. If it
     * fails to aquire a lock it will exit and check again the next time
     * called.
     *
     * If the lock could successfully be aquired it will run all scheduled
     * tasks in order and release the lock afterwards.
     *
     * @return void
     */
    public function run()
    {
        if ( ( $lastRun = $this->getLastRun() ) === false )
        {
            $this->storeLastRun();
            $lastRun = 0;
        }

        $tasks = $this->getJobsSince( $lastRun );
        if ( count( $tasks ) &&
             $this->aquireLock() )
        {
            $this->storeLastRun();
            $this->executeTasks( $tasks );
            $this->releaseLock();
        }
    }

    /**
     * Get jobs since date
     *
     * Return an array with jobs which have been scheduled between the given
     * date and the current time.
     *
     * Each job is only returned once, even it occured multiple times in the
     * given timeframe. The jobs are returned in order of their first
     * scheduled execution.
     *
     * @param int $time
     * @return array
     */
    protected function getJobsSince( $time )
    {
        $now  = time();
        $jobs = array();

        // Find rescheduled tasks
        foreach ( $this->rescheduled as $scheduled => $cronjob )
        {
            if ( $scheduled <= $now )
            {
                $jobs[$scheduled][] = $cronjob;
                unset( $this->rescheduled[$scheduled] );
            }
        }

        // Find new jobs defined by their crontable entries
        foreach ( $this->crontab as $cronjob )
        {
            $cronjob->iterator->startTime = $time;

            if ( ( $scheduled = $cronjob->iterator->current() ) < $now )
            {
                $jobs[$scheduled][] = $cronjob;
            }
        }

        ksort( $jobs );
        return $jobs;
    }

    /**
     * Execute scheduled tasks
     *
     * Execute the given tasks.
     *
     * If a task has a group assigned, it may not be executed in parallel with
     * tasks from the same group.
     *
     * This basic task executor does not execute any tasks in parallel,
     * because it should not depend on any non-default extensions like
     * ext/pcntl.
     *
     * @param array $tasks
     * @return void
     */
    protected function executeTasks( array $tasks )
    {
        foreach ( $tasks as $scheduled => $taskList )
        {
            foreach ( $taskList as $cronjob )
            {
                if ( ( $task = $this->taskFactory->factory( $cronjob->task, $scheduled, $this->logger ) ) !== false )
                {
                    $this->logger->setTask( $task->getId() );
                    $this->logger->log( 'Start task execution.' );
                    $status = $task->execute();

                    switch ( $status )
                    {
                        case Executor::SUCCESS:
                            $this->logger->log( 'Finished task execution.' );
                            break;
                        case Executor::ERROR:
                            $this->logger->log( 'Error occured during task execution.', Logger::WARNING );
                            break;
                        case Executor::RESCHEDULE:
                            $this->logger->log( 'Task will be rescheduled for ' . $task->reScheduleTime . ' seconds.' );
                            $this->rescheduled[$scheduled + $task->reScheduleTime] = $cronjob;
                            break;
                        default:
                            $this->logger->log( 'Invalid status returned by task.', Logger::ERROR );
                            break;
                    }

                    $this->logger->setTask();
                }
            }
        }
    }

    /**
     * Get last run of executor
     *
     * Return the last run of the executor as a unix timestamp. If this the
     * first run of the executor return false.
     *
     * @return mixed
     */
    protected function getLastRun()
    {
        if ( !is_file( $path = $this->lockDir . '/lastRun' ) )
        {
            return false;
        }

        return (int) file_get_contents( $path );
    }

    /**
     * Store last run time
     *
     * Stores the time and date of the last run of the executor.
     *
     * @return void
     */
    protected function storeLastRun()
    {
        // Silence warnings, which might be caused by multiple possible
        // failures. We handle and log them anyways.
        if ( !@file_put_contents( $this->lockDir . '/lastRun', time() ) )
        {
            $this->logger->log(
                'Failure storing last run time: ' . ( isset( $php_errormsg ) ? $php_errormsg : 'Unknown error - enable the track_errors ini directive.' ),
                Logger::ERROR
            );
            return;
        }

        $this->logger->log( 'Stored last run time.', Logger::INFO );
    }

    /**
     * Try to aquire lock
     *
     * Try to aquire lock - if successful the method will return true - and
     * false otherwise.
     *
     * @return bool
     */
    protected function aquireLock()
    {
        $lockfile = $this->lockDir . '/lock';
        // Silence call, since PHP will issue a warning when the file exists.
        // But there is no other way to properly immediately create a lock file
        // only if it does not exist yet.
        $fp = @fopen( $lockfile, 'x' );

        if ( $fp === false )
        {
            // Aquiring the lock failed.
            $this->logger->log(
                sprintf(
                    'The lockfile %s does already exist.',
                    $lockfile
                ),
                Logger::WARNING
            );
            return false;
        }

        // Store the lock aquiring time in the lock file so this can be
        // debugged more easily and maybe automotically released after
        // stallement.
        fwrite( $fp, time() );
        fclose( $fp );

        $this->logger->log( 'Aquired lock.', Logger::INFO );
        return true;
    }

    /**
     * Release lock
     *
     * Method to release the aquired lock, after the work has been done.
     *
     * @return void
     */
    protected function releaseLock()
    {
        // Again silencing the file system operation, because there might be
        // multiple possible reasons to fail and we are handling the error
        // anyways.
        if ( !@unlink( $this->lockDir . '/lock' ) )
        {
            $this->logger->log(
                'Failure releasing lock: ' . ( isset( $php_errormsg ) ? $php_errormsg : 'Unknown error - enable the track_errors ini directive.' ),
                Logger::ERROR
            );
            return;
        }

        $this->logger->log( 'Released lock.', Logger::INFO );
    }
}