| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029 | <?php/** * @file * Contains SimplenewsSource interface and implementations. *//** * The source used to build a newsletter mail. * * @ingroup source */interface SimplenewsSourceInterface {  /**   * Returns the mail headers.   *   * @param $headers   *   The default mail headers.   *   * @return   *   Mail headers as an array.   */  function getHeaders(array $headers);  /**   * Returns the mail subject.   */  function getSubject();  /**   * Returns the mail body.   *   * The body should either be plaintext or html, depending on the format.   */  function getBody();  /**   * Returns the plaintext body.   */  function getPlainBody();  /**   * Returns the mail footer.   *   * The footer should either be plaintext or html, depending on the format.   */  function getFooter();  /**   * Returns the plain footer.   */  function getPlainFooter();  /**   * Returns the mail format.   *   * @return   *   The mail format as string, either 'plain' or 'html'.   */  function getFormat();  /**   * Returns the recipent of this newsletter mail.   *   * @return   *   The recipient mail address(es) of this newsletter as a string.   */  function getRecipient();  /**   * The language that should be used for this newsletter mail.   */  function getLanguage();  /**   * Returns an array of attachments for this newsletter mail.   *   * @return   *   An array of managed file objects with properties uri, filemime and so on.   */  function getAttachments();  /**   * Returns the token context to be used with token replacements.   *   * @return   *   An array of objects as required by token_replace().   */  function getTokenContext();  /**   * Returns the mail key to be used for drupal_mail().   *   * @return   *   The mail key, either test or node.   */  function getKey();  /**   * Returns the formatted from mail address.   */  function getFromFormatted();  /**   * Returns the plain mail address.   */  function getFromAddress();}/** * Source interface based on a node. * * This is the interface that needs to be implemented to be compatible with * the default simplenews spool implementation and therefore exposed in * hook_simplenews_source_cache_info(). * * @ingroup source */interface SimplenewsSourceNodeInterface extends SimplenewsSourceInterface {  /**   * Create a source based on a node and subscriber.   */  function __construct($node, $subscriber);  /**   * Returns the actually used node of this source.   */  function getNode();  /**   * Returns the subscriber object.   */  function getSubscriber();}/** * Interface for a simplenews source cache implementation. * * This is only compatible with the SimplenewsSourceNodeInterface interface. * * @ingroup source */interface SimplenewsSourceCacheInterface {  /**   * Create a new instance, allows to initialize based on the used   * source.   */  function __construct(SimplenewsSourceNodeInterface $source);  /**   * Return a cached element, if existing.   *   * Although group and key can be used to identify the requested cache, the   * implementations are responsible to create a unique cache key themself using   * the $source. For example based on the node id and the language.   *   * @param $group   *   Group of the cache key, which allows cache implementations to decide what   *   they want to cache. Currently used groups:   *     - data: Raw data, e.g. attachments.   *     - build: Built and themed content, before personalizations like tokens.   *     - final: The final returned data. Caching this means that newsletter   *       can not be personalized anymore.   * @param $key   *   Identifies the requested element, e.g. body, footer or attachments.   */  function get($group, $key);  /**   * Write an element to the cache.   *   * Although group and key can be used to identify the requested cache, the   * implementations are responsible to create a unique cache key themself using   * the $source. For example based on the node id and the language.   *   * @param $group   *   Group of the cache key, which allows cache implementations to decide what   *   they want to cache. Currently used groups:   *     - data: Raw data, e.g. attachments.   *     - build: Built and themed content, before personalizations like tokens.   *     - final: The final returned data. Caching this means that newsletter   *       can not be personalized anymore.   * @param $key   *   Identifies the requested element, e.g. body, footer or attachments.   * @param $data   *   The data to be saved in the cache.   */  function set($group, $key, $data);}/** * A Simplenews spool implementation is a factory for Simplenews sources. * * Their main functionility is to return a number of sources based on the passed * in array of mail spool rows. Additionally, it needs to return the processed * mail rows after a source was sent. * * @todo: Move spool functions into this interface. * * @ingroup spool */interface SimplenewsSpoolInterface {  /**   * Initalizes the spool implementation.   *   * @param $spool_list   *   An array of rows from the {simplenews_mail_spool} table.   */  function __construct($pool_list);  /**   * Returns a Simplenews source to be sent.   *   * A single source may represent any number of mail spool rows, e.g. by   * addressing them as BCC.   */  function nextSource();  /**   * Returns the processed mail spool rows, keyed by the msid.   *   * Only rows that were processed while preparing the previously returned   * source must be returned.   *   * @return   *   An array of mail spool rows, keyed by the msid. Can optionally have set   *   the following additional properties.   *     - actual_nid: In case of content translation, the source node that was   *       used for this mail.   *     - error: FALSE if the prepration for this row failed. For example set   *       when the corresponding node failed to load.   *     - status: A simplenews spool status to indicate the status.   */  function getProcessed();}/** * Simplenews Spool implementation. * * @ingroup spool */class SimplenewsSpool implements SimplenewsSpoolInterface {  /**   * Array with mail spool rows being processed.   *   * @var array   */  protected $spool_list;  /**   * Array of the processed mail spool rows.   */  protected $processed = array();  /**   * Implements SimplenewsSpoolInterface::_construct($spool_list);   */  public function __construct($spool_list) {    $this->spool_list = $spool_list;  }  /**   * Implements SimplenewsSpoolInterface::nextSource();   */  public function nextSource() {    // Get the current mail spool row and update the internal pointer to the    // next row.    $return = each($this->spool_list);    // If we're done, return false.    if (!$return) {      return FALSE;    }    $spool_data = $return['value'];    // Store this spool row as processed.    $this->processed[$spool_data->msid] = $spool_data;    $node = node_load($spool_data->nid);    if (!$node) {      // If node the load failed, set the processed status done and proceed with      // the next mail.      $this->processed[$spool_data->msid]->result = array(        'status' => SIMPLENEWS_SPOOL_DONE,        'error' => TRUE      );      return $this->nextSource();    }    if ($spool_data->data) {      $subscriber = $spool_data->data;    }    else {      $subscriber = simplenews_subscriber_load_by_mail($spool_data->mail);    }    if (!$subscriber) {      // If loading the subscriber failed, set the processed status done and      // proceed with the next mail.      $this->processed[$spool_data->msid]->result = array(        'status' => SIMPLENEWS_SPOOL_DONE,        'error' => TRUE      );      return $this->nextSource();    }    $source_class = $this->getSourceImplementation($spool_data);    $source = new $source_class($node, $subscriber);    // Set which node is actually used. In case of a translation set, this might    // not be the same node.    $this->processed[$spool_data->msid]->actual_nid = $source->getNode()->nid;    return $source;  }  /**   * Implements SimplenewsSpoolInterface::getProcessed();   */  function getProcessed() {    $processed = $this->processed;    $this->processed = array();    return $processed;  }  /**   * Return the Simplenews source implementation for the given mail spool row.   */  protected function getSourceImplementation($spool_data) {    return variable_get('simplenews_source', 'SimplenewsSourceNode');  }}/** * Simplenews source implementation based on nodes for a single subscriber. * * @ingroup source */class SimplenewsSourceNode implements SimplenewsSourceNodeInterface {  /**   * The node object.   */  protected $node;  /**   * The cached build render array.   */  protected $build;  /**   * The newsletter category.   */  protected $category;  /**   * The subscriber and therefore recipient of this mail.   */  protected $subscriber;  /**   * The mail key used for drupal_mail().   */  protected $key = 'test';  /**   * The simplenews newsletter.   */  protected $newsletter;  /**   * Cache implementation used for this source.   *   * @var SimplenewsSourceCacheInterface   */  protected $cache;  /**   * Implements SimplenewsSourceInterface::_construct();   */  public function __construct($node, $subscriber) {    $this->setSubscriber($subscriber);    $this->setNode($node);    $this->newsletter = simplenews_newsletter_load($node->nid);    $this->category = simplenews_category_load($this->newsletter->tid);    $this->initCache();  }  /**   * Set the node of this source.   *   * If the node is part of a translation set, switch to the node for the   * requested language, if existent.   */  public function setNode($node) {    $langcode = $this->getLanguage();    $nid = $node->nid;    if (module_exists('translation')) {      // If the node has translations and a translation is required      // the equivalent of the node in the required language is used      // or the base node (nid == tnid) is used.      if ($tnid = $node->tnid) {        if ($langcode != $node->language) {          $translations = translation_node_get_translations($tnid);          // A translation is available in the preferred language.          if ($translation = $translations[$langcode]) {            $nid = $translation->nid;            $langcode = $translation->language;          }          else {            // No translation found which matches the preferred language.            foreach ($translations as $translation) {              if ($translation->nid == $tnid) {                $nid = $tnid;                $langcode = $translation->language;                break;              }            }          }        }      }    }    // If a translation of the node is used, load this node.    if ($nid != $node->nid) {      $this->node = node_load($nid);    }    else {      $this->node = $node;    }  }  /**   * Initialize the cache implementation.   */  protected function initCache() {    $class = variable_get('simplenews_source_cache', 'SimplenewsSourceCacheBuild');    $this->cache = new $class($this);  }  /**   * Returns the corresponding category.   */  public function getCategory() {    return $this->category;  }  /**   * Set the active subscriber.   */  public function setSubscriber($subscriber) {    $this->subscriber = $subscriber;  }  /**   * Return the subscriber object.   */  public function getSubscriber() {    return $this->subscriber;  }  /**   * Implements SimplenewsSourceInterface::getHeaders().   */  public function getHeaders(array $headers) {    // If receipt is requested, add headers.    if ($this->category->receipt) {      $headers['Disposition-Notification-To'] = $this->getFromAddress();      $headers['X-Confirm-Reading-To'] = $this->getFromAddress();    }    // Add priority if set.    switch ($this->category->priority) {      case SIMPLENEWS_PRIORITY_HIGHEST:        $headers['Priority'] = 'High';        $headers['X-Priority'] = '1';        $headers['X-MSMail-Priority'] = 'Highest';        break;      case SIMPLENEWS_PRIORITY_HIGH:        $headers['Priority'] = 'urgent';        $headers['X-Priority'] = '2';        $headers['X-MSMail-Priority'] = 'High';        break;      case SIMPLENEWS_PRIORITY_NORMAL:        $headers['Priority'] = 'normal';        $headers['X-Priority'] = '3';        $headers['X-MSMail-Priority'] = 'Normal';        break;      case SIMPLENEWS_PRIORITY_LOW:        $headers['Priority'] = 'non-urgent';        $headers['X-Priority'] = '4';        $headers['X-MSMail-Priority'] = 'Low';        break;      case SIMPLENEWS_PRIORITY_LOWEST:        $headers['Priority'] = 'non-urgent';        $headers['X-Priority'] = '5';        $headers['X-MSMail-Priority'] = 'Lowest';        break;    }    // Add user specific header data.    $headers['From'] = $this->getFromFormatted();    $headers['List-Unsubscribe'] = '<' . token_replace('[simplenews-subscriber:unsubscribe-url]', $this->getTokenContext(), array('sanitize' => FALSE)) . '>';    // Add general headers    $headers['Precedence'] = 'bulk';    return $headers;  }  /**   * Implements SimplenewsSourceInterface::getTokenContext().   */  function getTokenContext() {    return array(      'category' => $this->getCategory(),      'simplenews_subscriber' => $this->getSubscriber(),      'node' => $this->getNode(),    );  }  /**   * Set the mail key.   */  function setKey($key) {    $this->key = $key;  }  /**   * Implements SimplenewsSourceInterface::getKey().   */  function getKey() {    return $this->key;  }  /**   * Implements SimplenewsSourceInterface::getFromFormatted().   */  function getFromFormatted() {    // Windows based PHP systems don't accept formatted email addresses.    if (drupal_substr(PHP_OS, 0, 3) == 'WIN') {      return $this->getFromAddress();    }    return '"' . addslashes(mime_header_encode($this->getCategory()->from_name)) . '" <' . $this->getFromAddress() . '>';  }  /**   * Implements SimplenewsSourceInterface::getFromAddress().   */  function getFromAddress() {    return $this->getCategory()->from_address;  }  /**   * Implements SimplenewsSourceInterface::getRecipient().   */  function getRecipient() {    return $this->getSubscriber()->mail;  }  /**   * Implements SimplenewsSourceInterface::getFormat().   */  function getFormat() {    return $this->getCategory()->format;  }  /**   * Implements SimplenewsSourceInterface::getLanguage().   */  function getLanguage() {    return $this->getSubscriber()->language;  }  /**   * Implements SimplenewsSourceSpoolInterface::getNode().   */  function getNode() {    return $this->node;  }  /**   * Implements SimplenewsSourceInterface::getSubject().   */  function getSubject() {    // Build email subject and perform some sanitizing.    $langcode = $this->getLanguage();    $language_list = language_list();    // Use the requested language if enabled.    $language = isset($language_list[$langcode]) ? $language_list[$langcode] : NULL;    $subject = token_replace($this->getCategory()->email_subject, $this->getTokenContext(), array('sanitize' => FALSE, 'language' => $language));    // Line breaks are removed from the email subject to prevent injection of    // malicious data into the email header.    $subject = str_replace(array("\r", "\n"), '', $subject);    return $subject;  }  /**   * Set up the necessary language and user context.   */  protected function setContext() {    // Switch to the user    if ($this->uid = $this->getSubscriber()->uid) {      simplenews_impersonate_user($this->uid);    }    // Change language if the requested language is enabled.    $language = $this->getLanguage();    $languages = language_list();    if (isset($languages[$language])) {      $this->original_language = $GLOBALS['language'];      $GLOBALS['language'] = $languages[$language];      $GLOBALS['language_url'] = $languages[$language];      // Overwrites the current content language for i18n_select.      if (module_exists('i18n_select')) {        $GLOBALS['language_content'] = $languages[$language];      }    }  }  /**   * Reset the context.   */  protected function resetContext() {    // Switch back to the previous user.    if ($this->uid) {      simplenews_revert_user();    }    // Switch language back.    if (!empty($this->original_language)) {      $GLOBALS['language'] = $this->original_language;      $GLOBALS['language_url'] = $this->original_language;      if (module_exists('i18n_select')) {        $GLOBALS['language_content'] = $this->original_language;      }    }  }  /**   * Build the node object.   *   * The resulting build array is cached as it is used in multiple places.   * @param $format   *   (Optional) Override the default format. Defaults to getFormat().   */  protected function build($format = NULL) {    if (empty($format)) {      $format = $this->getFormat();    }    if (!empty($this->build[$format])) {      return $this->build[$format];    }    // Build message body    // Supported view modes: 'email_plain', 'email_html', 'email_textalt'    $build = node_view($this->node, 'email_' . $format);    unset($build['#theme']);    foreach (field_info_instances('node', $this->node->type) as $field_name => $field) {      if (isset($build[$field_name])) {        $build[$field_name]['#theme'] = 'simplenews_field';      }    }    $this->build[$format] = $build;    return $this->build[$format];  }  /**   * Build the themed newsletter body.   *   * @param $format   *   (Optional) Override the default format. Defaults to getFormat().   */  protected function buildBody($format = NULL) {    if (empty($format)) {      $format = $this->getFormat();    }    if ($cache = $this->cache->get('build', 'body:' . $format)) {      return $cache;    }    $body = theme('simplenews_newsletter_body', array('build' => $this->build($format), 'category' => $this->getCategory(), 'language' => $this->getLanguage(), 'simplenews_subscriber' => $this->getSubscriber()));    $this->cache->set('build', 'body:' . $format, $body);    return $body;  }  /**   * Implements SimplenewsSourceInterface::getBody().   */  public function getBody() {    return $this->getBodyWithFormat($this->getFormat());  }  /**   * Implements SimplenewsSourceInterface::getBody().   */  public function getPlainBody() {    return $this->getBodyWithFormat('plain');  }   /**   * Get the body with the requested format.   *   * @param $format   *   Either html or plain.   *   * @return   *   The rendered mail body as a string.   */  protected function getBodyWithFormat($format) {    // Switch to correct user and language context.    $this->setContext();    if ($cache = $this->cache->get('final', 'body:' . $format)) {      return $cache;    }    $body = $this->buildBody($format);    // Build message body, replace tokens.    $body = token_replace($body, $this->getTokenContext(), array('sanitize' => FALSE));    if ($format == 'plain') {      // Convert HTML to text if requested to do so.      $body = simplenews_html_to_text($body, $this->getCategory()->hyperlinks);    }    $this->cache->set('final', 'body:' . $format, $body);    $this->resetContext();    return $body;  }  /**   * Builds the themed footer.   *   * @param $format   *   (Optional) Set the format of this footer build, overrides the default   *   format.   */  protected function buildFooter($format = NULL) {    if (empty($format)) {      $format = $this->getFormat();    }    if ($cache = $this->cache->get('build', 'footer:' . $format)) {      return $cache;    }    // Build and buffer message footer    $footer = theme('simplenews_newsletter_footer', array(      'build' => $this->build($format),      'category' => $this->getCategory(),      'context' => $this->getTokenContext(),      'key' => $this->getKey(),      'language' => $this->getLanguage(),      'format' => $format,    ));    $this->cache->set('build', 'footer:' . $format, $footer);    return $footer;  }  /**   * Implements SimplenewsSourceInterface::getFooter().   */  public function getFooter() {    return $this->getFooterWithFormat($this->getFormat());  }  /**   * Implements SimplenewsSourceInterface::getPlainFooter().   */  public function getPlainFooter() {    return $this->getFooterWithFormat('plain');  }  /**   * Get the footer in the specified format.   *   * @param $format   *   Either html or plain.   *   * @return   *   The footer for the requested format.   */  protected function getFooterWithFormat($format) {    // Switch to correct user and language context.    $this->setContext();    if ($cache = $this->cache->get('final', 'footer:' . $format)) {      return $cache;    }    $final_footer = token_replace($this->buildFooter($format), $this->getTokenContext(), array('sanitize' => FALSE));    $this->cache->set('final', 'footer:' . $format, $final_footer);    $this->resetContext();    return $final_footer;  }  /**   * Implements SimplenewsSourceInterface::getAttachments().   */  function getAttachments() {    if ($cache = $this->cache->get('data', 'attachments')) {      return $cache;    }    $attachments = array();    $build = $this->build();    $fids = array();    foreach (field_info_instances('node', $this->node->type) as $field_name => $field_instance) {      // @todo: Find a better way to support more field types.      // Only add fields of type file which are enabled for the current view      // mode as attachments.      $field = field_info_field($field_name);      if ($field['type'] == 'file' && isset($build[$field_name])) {        if ($items = field_get_items('node', $this->node, $field_name)) {          foreach ($items as $item) {            $fids[] = $item['fid'];          }        }      }    }    if (!empty($fids)) {      $attachments = file_load_multiple($fids);    }    $this->cache->set('data', 'attachments', $attachments);    return $attachments;  }}/** * Abstract implementation of the source caching that does static caching. * * Subclasses need to implement the abstract function isCacheable() to decide * what should be cached. * * @ingroup source */abstract class SimplenewsSourceCacheStatic implements SimplenewsSourceCacheInterface {  /**   * The simplenews source for which this cache is used.   *   * @var SimplenewsSourceNodeInterface   */  protected $source;  /**   * The cache identifier for the given source.   */  protected $cid;  /**   * The static cache.   */  protected static $cache = array();  /**   * Implements SimplenewsSourceNodeInterface::__construct().   */  public function __construct(SimplenewsSourceNodeInterface $source) {    $this->source = $source;    self::$cache = &drupal_static(__CLASS__, array());  }  /**   * Returns the cache identifier for the current source.   */  protected function getCid() {    if (empty($this->cid)) {      $this->cid = $this->source->getNode()->nid . ':' . $this->source->getLanguage();    }    return $this->cid;  }  /**   * Implements SimplenewsSourceNodeInterface::get().   */  public function get($group, $key) {    if (!$this->isCacheable($group, $key)) {      return;    }    if (isset(self::$cache[$this->getCid()][$group][$key])) {      return self::$cache[$this->getCid()][$group][$key];    }  }  /**   * Implements SimplenewsSourceNodeInterface::set().   */  public function set($group, $key, $data) {    if (!$this->isCacheable($group, $key)) {      return;    }    self::$cache[$this->getCid()][$group][$key] = $data;  }  /**   * Return if the requested element should be cached.   *   * @return   *   TRUE if it should be cached, FALSE otherwise.   */  abstract function isCacheable($group, $key);}/** * Cache implementation that does not cache anything at all. * * @ingroup source */class SimplenewsSourceCacheNone extends SimplenewsSourceCacheStatic {  /**   * Implements SimplenewsSourceCacheStatic::set().   */  public function isCacheable($group, $key) {    return FALSE;  }}/** * Source cache implementation that caches build and data element. * * @ingroup source */class SimplenewsSourceCacheBuild extends SimplenewsSourceCacheStatic {  /**   * Implements SimplenewsSourceCacheStatic::set().   */  function isCacheable($group, $key) {    // Only cache for anon users.    if (user_is_logged_in()) {      return FALSE;    }     // Only cache data and build information.    return in_array($group, array('data', 'build'));  }}/** * Example source implementation used for tests. * * @ingroup source */class SimplenewsSourceTest implements SimplenewsSourceInterface {  protected $format;  public function __construct($format) {    $this->format = $format;  }  public function getAttachments() {    return array(      array(        'uri' => 'example://test.png',        'filemime' => 'x-example',        'filename' => 'test.png',      ),    );  }  public function getBody() {    return $this->getFormat() == 'plain' ? $this->getPlainBody() : 'the body';  }  public function getFooter() {    return $this->getFormat() == 'plain' ? $this->getPlainFooter() : 'the footer';  }  public function getPlainFooter() {    return 'the plain footer';  }  public function getFormat() {    return $this->format;  }  public function getFromAddress() {    return 'test@example.org';  }  public function getFromFormatted() {    return 'Test <test@example.org>';  }  public function getHeaders(array $headers) {    $headers['X-Simplenews-Test'] = 'OK';    return $headers;  }  public function getKey() {    return 'node';  }  public function getLanguage() {    return 'en';  }  public function getPlainBody() {    return 'the plain body';  }  public function getRecipient() {    return 'recipient@example.org';  }  public function getSubject() {    return 'the subject';  }  public function getTokenContext() {    return array();  }}
 |