{{tpl:lang List of feeds}}
+{{tpl:zcFeedsCount}} - {{tpl:zcFeedsEntriesCount}}
+{{tpl:zcFeedName encode_html="1"}}
+ +{{tpl:lang By}} {{tpl:zcFeedOwner}}
+ - {{tpl:lang source}}
+ - {{tpl:zcFeedLang full="1"}}
+ - {{tpl:zcFeedEntriesCount}}
+
diff --git a/README.md b/README.md
index 83286d4..cc8d994 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,27 @@
-# zoneclearFeedServer
-Mixer votre blog avec un planet
+# README
+
+## WHAT IS ZONECLEARFEEDSERVER ?
+
+"zoneclear Feeds server" is a plugin for the open-source
+web publishing software called Dotclear.
+
+It mix your blog with a feeds planet.
+
+## REQUIREMENTS
+
+ zoneclearFeedServer requires:
+
+ * admin perms for configuration
+ * admin perms to manager feeds
+ * Dotclear 2.7
+
+## USAGE
+
+First install zoneclearFeedServer, manualy from a zip package or from
+Dotaddict repository. (See Dotclear's documentation to know how do this)
+
+Go to ''plugins manager'', expand zoneclearFeedServer information then
+go to ''configure plugin'', fill in form.
+
+Once it's done you can manage your feeds from menu
+''Feeds server'' on sidebar or you can add dashboard icon.
diff --git a/_admin.php b/_admin.php
new file mode 100644
index 0000000..228e8fc
--- /dev/null
+++ b/_admin.php
@@ -0,0 +1,259 @@
+blog->settings->addNamespace('zoneclearFeedServer');
+
+# Check if latest version is installed
+if ($core->getVersion('zoneclearFeedServer') !=
+ $core->plugins->moduleInfo('zoneclearFeedServer', 'version')) {
+
+ return null;
+}
+
+# Widgets
+require_once dirname(__FILE__).'/_widgets.php';
+
+# Admin menu
+$_menu['Blog']->addItem(
+ __('Feeds server'),
+ 'plugin.php?p=zoneclearFeedServer',
+ 'index.php?pf=zoneclearFeedServer/icon.png',
+ preg_match(
+ '/plugin.php\?p=zoneclearFeedServer(&.*)?$/',
+ $_SERVER['REQUEST_URI']
+ ),
+ $core->auth->check('admin', $core->blog->id)
+);
+
+# Delete related info about feed post in meta table
+$core->addBehavior(
+ 'adminBeforePostDelete',
+ array('zcfsAdminBehaviors', 'adminBeforePostDelete')
+);
+
+if ($core->auth->check('admin', $core->blog->id)) {
+
+ # Dashboard icon
+ $core->addBehavior(
+ 'adminDashboardFavorites',
+ array('zcfsAdminBehaviors', 'adminDashboardFavorites')
+ );
+
+ # Add info about feed on post page sidebar
+ $core->addBehavior(
+ 'adminPostHeaders',
+ array('zcfsAdminBehaviors', 'adminPostHeaders')
+ );
+ $core->addBehavior(
+ 'adminPostFormItems',
+ array('zcfsAdminBehaviors', 'adminPostFormItems')
+ );
+}
+
+# Take care about tweakurls (thanks Mathieu M.)
+if (version_compare($core->plugins->moduleInfo('tweakurls', 'version'), '0.8', '>=')) {
+
+ $core->addbehavior(
+ 'zcfsAfterPostCreate',
+ array('zoneclearFeedServer', 'tweakurlsAfterPostCreate')
+ );
+}
+
+/**
+ * @ingroup DC_PLUGIN_ZONECLEARFEEDSERVER
+ * @brief Mix your blog with a feeds planet - admin methods.
+ * @since 2.6
+ */
+class zcfsAdminBehaviors
+{
+ /**
+ * Favorites.
+ *
+ * @param dcCore $core dcCore instance
+ * @param arrayObject $favs Array of favorites
+ */
+ public static function adminDashboardFavorites(dcCore $core, $favs)
+ {
+ $favs->register('zcfs', array(
+ 'title' => __('Feeds server'),
+ 'url' => 'plugin.php?p=zoneclearFeedServer',
+ 'small-icon' => 'index.php?pf=zoneclearFeedServer/icon.png',
+ 'large-icon' => 'index.php?pf=zoneclearFeedServer/icon-big.png',
+ 'permissions' => 'usage,contentadmin',
+ 'active_cb' => array(
+ 'zcfsAdminBehaviors',
+ 'adminDashboardFavoritesActive'
+ ),
+ 'dashboard_cb' => array(
+ 'zcfsAdminBehaviors',
+ 'adminDashboardFavoritesCallback'
+ )
+ ));
+ }
+
+ /**
+ * Favorites selection.
+ *
+ * @param string $request Requested page
+ * @param array $params Requested parameters
+ */
+ public static function adminDashboardFavoritesActive($request, $params)
+ {
+ return $request == 'plugin.php'
+ && isset($params['p'])
+ && $params['p'] == 'zoneclearFeedServer';
+ }
+
+ /**
+ * Favorites hack.
+ *
+ * @param dcCore $core dcCore instance
+ * @param arrayObject $fav Fav attributes
+ */
+ public static function adminDashboardFavoritesCallback(dcCore $core, $fav)
+ {
+ $zcfs = new zoneclearFeedServer($core);
+ $count = $zcfs->getFeeds(array(
+ 'feed_status' => '0'
+ ), true)->f(0);
+
+ if (!$count) {
+
+ return null;
+ }
+
+ $fav['title'] .= '
'.sprintf(
+ __('%s feed disabled', '%s feeds disabled', $count),
+ $count
+ );
+ $fav['url'] = 'plugin.php?p=zoneclearFeedServer&part=feeds'.
+ '&sortby=feed_status&order=asc';
+ $fav['large-icon'] = 'index.php?pf=zoneclearFeedServer'.
+ '/icon-big-update.png';
+ }
+
+ /**
+ * Add javascript for toggle to post edition page header.
+ *
+ * @return string Page header
+ */
+ public static function adminPostHeaders()
+ {
+ return dcPage::jsLoad(
+ 'index.php?pf=zoneclearFeedServer/js/post.js'
+ );
+ }
+
+ /**
+ * Add form to post sidebar.
+ *
+ * @param ArrayObject $main_items Main items
+ * @param ArrayObject $sidebar_items Sidebar items
+ * @param record $post Post record or null
+ */
+ public static function adminPostFormItems(ArrayObject $main_items, ArrayObject $sidebar_items, $post)
+ {
+ if ($post === null || $post->post_type != 'post') {
+
+ return null;
+ }
+
+ global $core;
+
+ $url = $core->meta->getMetadata(array(
+ 'post_id' => $post->post_id,
+ 'meta_type' => 'zoneclearfeed_url',
+ 'limit' => 1
+ ));
+ $url = $url->isEmpty() ? '' : $url->meta_id;
+
+ if (!$url) {
+
+ return null;
+ }
+
+ $author = $core->meta->getMetadata(array(
+ 'post_id' => $post->post_id,
+ 'meta_type' => 'zoneclearfeed_author',
+ 'limit' => 1
+ ));
+ $author = $author->isEmpty() ? '' : $author->meta_id;
+
+ $site = $core->meta->getMetadata(array(
+ 'post_id' => $post->post_id,
+ 'meta_type' => 'zoneclearfeed_site',
+ 'limit' => 1
+ ));
+ $site = $site->isEmpty() ? '' : $site->meta_id;
+
+ $sitename = $core->meta->getMetadata(array(
+ 'post_id' => $post->post_id,
+ 'meta_type' => 'zoneclearfeed_sitename',
+ 'limit' => 1
+ ));
+ $sitename = $sitename->isEmpty() ? '' : $sitename->meta_id;
+
+ $edit = '';
+ if ($core->auth->check('admin', $core->blog->id)) {
+ $fid = $core->meta->getMetadata(array(
+ 'post_id' => $post->post_id,
+ 'meta_type' => 'zoneclearfeed_id',
+ 'limit' => 1
+ ));
+ if (!$fid->isEmpty()) {
+ $edit =
+ '
'. + ''.__('feed URL').' - '. + ''.__('site URL').''. + '
'. + $edit. + ''. + __('Dotclear cache is not writable or not well configured!'). + '
'; + } + +echo ' + +'.__('View the public list of feeds').'
'; +} + +echo ' +'. +form::combo('post_status_new', $combo_status, $post_status_new).'
+ +'. +form::combo('feeduser', $combo_admins, $feeduser).'
+ +'. +form::combo('tag_case', $combo_tagcase, $tag_case).'
+ +'. +form::combo('bhv_pub_upd', $combo_pubupd, $bhv_pub_upd).'
+ +'. +form::field('update_limit', 6, 4, $update_limit).'
+ + + + + +'.__('Redirect to original post on:').'
'.__('Show full content on:').'
'. + sprintf(__('Original post on %s'), $url, $sitename). + '
'; + } + else { + $content = context::remove_html($content); + $content = context::cut_string($content,350); + $content = html::escapeHTML($content); + + return + ''.$content.'... '. + ''.__('Continue reading').'
'; + } + } + else { + + return $content; + } + } +} + +/** + * @ingroup DC_PLUGIN_ZONECLEARFEEDSERVER + * @brief Mix your blog with a feeds planet - url handler methods. + * @since 2.6 + */ +class zcfsUrlHandler extends dcUrlHandlers +{ + /** + * Feeds source page and update methods. + * + * @param array $args Page arguments + * @return mixed + */ + public static function zcFeedsPage($args) + { + global $core, $_ctx; + $s = $core->blog->settings->zoneclearFeedServer; + + # Not active + if (!$s->zoneclearFeedServer_active) { + self::p404(); + + return null; + } + + # Update feeds (from ajax or other post resquest) + if ($args == '/zcfsupd' + && 3 == $s->zoneclearFeedServer_bhv_pub_upd + ) { + $msg = ''; + if (!empty($_POST['blogId']) + && html::escapeJS($core->blog->id) == $_POST['blogId'] + ) { + try { + $zc = new zoneclearFeedServer($core); + if ($zc->checkFeedsUpdate()) { + $msg = '{{tpl:zcFeedsCount}} - {{tpl:zcFeedsEntriesCount}}
+{{tpl:lang By}} {{tpl:zcFeedOwner}}
+ - {{tpl:lang source}}
+ - {{tpl:zcFeedLang full="1"}}
+ - {{tpl:zcFeedEntriesCount}}
+
{{tpl:zcFeedsCount}} - {{tpl:zcFeedsEntriesCount}}
+{{tpl:lang By}} {{tpl:zcFeedOwner}}
+ - {{tpl:lang source}}
+ - {{tpl:zcFeedLang full="1"}}
+ - {{tpl:zcFeedEntriesCount}}
+
'. + __('Back to feeds list').'
'; + } + + public function endPage() + { + echo + ''; + } + + public function error(Exception $e) + { + $this->core->error->add($e->getMessage()); + $this->beginPage(dcPage::breadcrumb(array( + html::escapeHTML($this->core->blog->name) => '', + $this->getCallerTitle() => $this->getRedirection(true), + __('Feeds actions') => '' + ))); + $this->endPage(); + } + + protected function fetchEntries($from) + { + if (!empty($from['feeds'])) { + + $params['feed_id'] = $from['feeds']; + + $feeds = $this->zcfs->getFeeds($params); + while ($feeds->fetch()) { + $this->entries[$feeds->feed_id] = $feeds->feed_name; + } + $this->rs = $feeds; + } + else { + $this->rs = $this->core->con->select( + "SELECT blog_id FROM ". + $this->core->prefix."blog WHERE false" + ); + } + } +} + +/** + * @ingroup DC_PLUGIN_ZONECLEARFEEDSERVER + * @brief Feeds server - Default actions methods + * @since 2.6 + * @see dcDefaultPostsActionsPage for mor info + */ +class zcfsDefaultFeedsActions +{ + public static function zcfsFeedsActionsPage(dcCore $core, zcfsFeedsActionsPage $ap) + { + $ap->addAction( + array(__('Change category') => 'changecat'), + array('zcfsDefaultFeedsActions', 'doChangeCategory') + ); + $ap->addAction( + array(__('Change update interval') => 'changeint'), + array('zcfsDefaultFeedsActions', 'doChangeInterval') + ); + $ap->addAction( + array(__('Disable feed update') => 'disablefeed'), + array('zcfsDefaultFeedsActions', 'doEnableFeed') + ); + $ap->addAction( + array(__('Enable feed update') => 'enablefeed'), + array('zcfsDefaultFeedsActions', 'doEnableFeed') + ); + $ap->addAction( + array(__('Reset last update') => 'resetupdlast'), + array('zcfsDefaultFeedsActions', 'doResetUpdate') + ); + $ap->addAction( + array(__('Update (check) feed') => 'updatefeed'), + array('zcfsDefaultFeedsActions', 'doUpdateFeed') + ); + $ap->addAction( + array(__('Delete related posts') => 'deletepost'), + array('zcfsDefaultFeedsActions', 'doDeletePost') + ); + $ap->addAction( + array(__('Delete feed (without related posts)') => 'deletefeed'), + array('zcfsDefaultFeedsActions', 'doDeleteFeed') + ); + } + + public static function doEnableFeed(dcCore $core, zcfsFeedsActionsPage $ap, $post) + { + $enable = $ap->getAction() == 'enablefeed'; + $ids = $ap->getIDs(); + + if (empty($ids)) { + throw new Exception(__('No feeds selected')); + } + + foreach($ids as $id) { + $ap->zcfs->enableFeed($id, $enable); + } + + dcPage::addSuccessNotice(sprintf( + $enable ? + __( + '%d feed has been successfully enabled.', + '%d feeds have been successfully enabled.', + count($ids) + ) + : + __( + '%d feed has been successfully disabled.', + '%d feeds have been successfully disabled.', + count($ids) + ) + , + count($ids) + )); + $ap->redirect(true); + } + + public static function doDeletePost(dcCore $core, zcfsFeedsActionsPage $ap, $post) + { + $types = array( + 'zoneclearfeed_url', + 'zoneclearfeed_author', + 'zoneclearfeed_site', + 'zoneclearfeed_sitename', + 'zoneclearfeed_id' + ); + + $ids = $ap->getIDs(); + + if (empty($ids)) { + throw new Exception(__('No feeds selected')); + } + + foreach($ids as $id) { + + $posts = $ap->zcfs->getPostsByFeed(array( + 'feed_id' => $id + )); + + while($posts->fetch()) { + + $core->blog->delPost($posts->post_id); + $core->con->execute( + 'DELETE FROM '.$core->prefix.'meta '. + 'WHERE post_id = '.$posts->post_id.' '. + 'AND meta_type '.$core->con->in($types).' ' + ); + } + } + + dcPage::addSuccessNotice( + __('Entries have been successfully deleted.') + ); + $ap->redirect(true); + } + + public static function doDeleteFeed(dcCore $core, zcfsFeedsActionsPage $ap, $post) + { + $ids = $ap->getIDs(); + + if (empty($ids)) { + throw new Exception(__('No feeds selected')); + } + + foreach($ids as $id) { + $ap->zcfs->delFeed($id); + } + + dcPage::addSuccessNotice(sprintf( + __( + '%d feed has been successfully deleted.', + '%d feeds have been successfully deleted.', + count($ids) + ), + count($ids) + )); + $ap->redirect(true); + } + + public static function doUpdateFeed(dcCore $core, zcfsFeedsActionsPage $ap, $post) + { + $ids = $ap->getIDs(); + + if (empty($ids)) { + throw new Exception(__('No feeds selected')); + } + + foreach($ids as $id) { + $ap->zcfs->checkFeedsUpdate($id, true); + } + + dcPage::addSuccessNotice(sprintf( + __( + '%d feed has been successfully updated.', + '%d feeds have been successfully updated.', + count($ids) + ), + count($ids) + )); + $ap->redirect(true); + } + + public static function doResetUpdate(dcCore $core, zcfsFeedsActionsPage $ap, $post) + { + $ids = $ap->getIDs(); + + if (empty($ids)) { + throw new Exception(__('No feeds selected')); + } + + foreach($ids as $id) { + $cur = $ap->zcfs->openCursor(); + $cur->feed_upd_last = 0; + $ap->zcfs->updFeed($id, $cur); + $ap->zcfs->checkFeedsUpdate($id, true); + } + + dcPage::addSuccessNotice(sprintf( + __( + 'Last update of %s feed successfully reseted.', + 'Last update of %s feeds successfully reseted.', + count($ids) + ), + count($ids) + )); + $ap->redirect(true); + } + + public static function doChangeCategory(dcCore $core, zcfsFeedsActionsPage $ap, $post) + { + if (isset($post['upd_cat_id'])) { + $ids = $ap->getIDs(); + + if (empty($ids)) { + throw new Exception(__('No feeds selected')); + } + + $cat_id = abs((integer) $post['upd_cat_id']); + + foreach($ids as $id) { + $cur = $ap->zcfs->openCursor(); + $cur->cat_id = $cat_id == 0 ? null : $cat_id; + $ap->zcfs->updFeed($id, $cur); + } + + dcPage::addSuccessNotice(sprintf( + __( + 'Category of %s feed successfully changed.', + 'Category of %s feeds successfully changed.', + count($ids) + ), + count($ids) + )); + $ap->redirect(true); + } + else { + + $categories_combo = dcAdminCombos::getCategoriesCombo( + $core->blog->getCategories() + ); + + $ap->beginPage( + dcPage::breadcrumb( + array( + html::escapeHTML($core->blog->name) => '', + __('Feeds server') => '', + $ap->getCallerTitle() => $ap->getRedirection(true), + __('Change category for this selection') => '' + ))); + + echo + ''; + + $ap->endPage(); + } + } + + public static function doChangeInterval(dcCore $core, zcfsFeedsActionsPage $ap, $post) + { + if (isset($post['upd_upd_int'])) { + $ids = $ap->getIDs(); + + if (empty($ids)) { + throw new Exception(__('No feeds selected')); + } + + $upd_int = abs((integer) $post['upd_upd_int']); + + foreach($ids as $id) { + $cur = $ap->zcfs->openCursor(); + $cur->feed_upd_int = $upd_int; + $ap->zcfs->updFeed($id, $cur); + } + + dcPage::addSuccessNotice(sprintf( + __( + 'Update frequency of %s feed successfully changed.', + 'Update frequency of %s feeds successfully changed.', + count($ids) + ), + count($ids) + )); + $ap->redirect(true); + } + else { + + $ap->beginPage( + dcPage::breadcrumb( + array( + html::escapeHTML($core->blog->name) => '', + __('Feeds server') => '', + $ap->getCallerTitle() => $ap->getRedirection(true), + __('Change update frequency for this selection') => '' + ))); + + echo + ''; + + $ap->endPage(); + } + } +} \ No newline at end of file diff --git a/inc/class.zoneclear.feed.server.php b/inc/class.zoneclear.feed.server.php new file mode 100644 index 0000000..2a097c3 --- /dev/null +++ b/inc/class.zoneclear.feed.server.php @@ -0,0 +1,933 @@ +core = $core; + $this->con = $core->con; + $this->blog = $core->con->escape($core->blog->id); + $this->table = $core->prefix.'zc_feed'; + } + + /** + * Short openCursor. + * + * @return cursor cursor instance + */ + public function openCursor() + { + return $this->con->openCursor($this->table); + } + + /** + * Update feed record. + * + * @param integer $id Feed id + * @param cursor $cur cursor instance + */ + public function updFeed($id, cursor $cur) + { + $this->con->writeLock($this->table); + + try { + $id = (integer) $id; + + if ($id < 1) { + throw new Exception(__('No such ID')); + } + $cur->feed_upddt = date('Y-m-d H:i:s'); + + $cur->update(sprintf( + "WHERE feed_id = %s AND blog_id = '%s' ", + $id, + $this->blog + )); + $this->con->unlock(); + $this->trigger(); + } + catch (Exception $e) { + $this->con->unlock(); + throw $e; + } + + # --BEHAVIOR-- zoneclearFeedServerAfterUpdFeed + $this->core->callBehavior( + 'zoneclearFeedServerAfterUpdFeed', + $cur, + $id + ); + } + + /** + * Add feed record. + * + * @param cursor $cur cursor instance + */ + public function addFeed(cursor $cur) + { + $this->con->writeLock($this->table); + + try { + $cur->feed_id = $this->getNextId(); + $cur->blog_id = $this->blog; + $cur->feed_creadt = date('Y-m-d H:i:s'); + $cur->feed_upddt = date('Y-m-d H:i:s'); + + //add getFeedCursor here + + $cur->insert(); + $this->con->unlock(); + $this->trigger(); + } + catch (Exception $e) { + $this->con->unlock(); + throw $e; + } + + # --BEHAVIOR-- zoneclearFeedServerAfterAddFeed + $this->core->callBehavior( + 'zoneclearFeedServerAfterAddFeed', + $cur + ); + + return $cur->feed_id; + } + + /** + * Quick enable / disable feed. + * + * @param integer $id Feed Id + * @param boolean $enable Enable or disable feed + * @param integer $time Force update time + */ + public function enableFeed($id, $enable=true, $time=null) + { + try { + $id = (integer) $id; + + if ($id < 1) { + throw new Exception(__('No such ID')); + } + $cur = $this->openCursor(); + $this->con->writeLock($this->table); + + $cur->feed_upddt = date('Y-m-d H:i:s'); + + $cur->feed_status = (integer) $enable; + if (null !== $time) { + $cur->feed_upd_last = (integer) $time; + } + + $cur->update(sprintf( + "WHERE feed_id = %s AND blog_id = '%s' ", + $id, + $this->blog + )); + $this->con->unlock(); + $this->trigger(); + } + catch (Exception $e) { + $this->con->unlock(); + throw $e; + } + + # --BEHAVIOR-- zoneclearFeedServerAfterEnableFeed + $this->core->callBehavior( + 'zoneclearFeedServerAfterEnableFeed', + $id, + $enable, + $time + ); + } + + # + /** + * Delete record (this not deletes post). + * + * @param integer $id Feed Id + */ + public function delFeed($id) + { + $id = (integer) $id; + + if ($id < 1) { + throw new Exception(__('No such ID')); + } + + # --BEHAVIOR-- zoneclearFeedServerBeforeDelFeed + $this->core->callBehavior( + 'zoneclearFeedServerBeforeDelFeed', + $id + ); + + $this->con->execute(sprintf( + "DELETE FROM %s WHERE feed_id = %s AND blog_id = '%s' ", + $this->table, + $id, + $this->blog + )); + $this->trigger(); + } + + /** + * Get related posts. + * + * @param array $params Query params + * @param boolean $count_only Return only result count + * @return record record instance + */ + public function getPostsByFeed($params=array(), $count_only=false) + { + if (!isset($params['feed_id'])) { + + return null; + } + + $params['from'] = + 'LEFT JOIN '.$this->core->prefix.'meta F '. + 'ON P.post_id = F.post_id '; + $params['sql'] = + "AND P.blog_id = '".$this->blog."' ". + "AND F.meta_type = 'zoneclearfeed_id' ". + "AND F.meta_id = '".$this->con->escape($params['feed_id'])."' "; + + unset($params['feed_id']); + + return $this->core->blog->getPosts($params, $count_only); + } + + /** + * Get feed record. + * + * @param array $params Query params + * @param boolean $count_only Return only result count + * @return record record instance + */ + public function getFeeds($params=array(), $count_only=false) + { + if ($count_only) { + $strReq = 'SELECT count(Z.feed_id) '; + } + else { + $content_req = ''; + if (!empty($params['columns']) && is_array($params['columns'])) { + $content_req .= implode(', ',$params['columns']).', '; + } + + $strReq = + 'SELECT Z.feed_id, Z.feed_creadt, Z.feed_upddt, Z.feed_type, '. + 'Z.blog_id, Z.cat_id, '. + 'Z.feed_upd_int, Z.feed_upd_last, Z.feed_status, '. + $content_req. + 'LOWER(Z.feed_name) as lowername, Z.feed_name, Z.feed_desc, '. + 'Z.feed_url, Z.feed_feed, Z.feed_get_tags, '. + 'Z.feed_tags, Z.feed_owner, Z.feed_tweeter, Z.feed_lang, '. + 'Z.feed_nb_out, Z.feed_nb_in, '. + 'C.cat_title, C.cat_url, C.cat_desc '; + } + + $strReq .= + 'FROM '.$this->table.' Z '. + 'LEFT OUTER JOIN '.$this->core->prefix.'category C ON Z.cat_id = C.cat_id '; + + if (!empty($params['from'])) { + $strReq .= $params['from'].' '; + } + + $strReq .= "WHERE Z.blog_id = '".$this->blog."' "; + + if (isset($params['feed_type'])) { + $strReq .= "AND Z.feed_type = '".$this->con->escape($params['type'])."' "; + } + else { + $strReq .= "AND Z.feed_type = 'feed' "; + } + + if (!empty($params['feed_id'])) { + if (is_array($params['feed_id'])) { + array_walk($params['feed_id'],create_function('&$v,$k','if($v!==null){$v=(integer)$v;}')); + } + else { + $params['feed_id'] = array((integer) $params['feed_id']); + } + $strReq .= 'AND Z.feed_id '.$this->con->in($params['feed_id']); + } + + if (isset($params['feed_feed'])) { + $strReq .= "AND Z.feed_feed = '".$this->con->escape($params['feed_feed'])."' "; + } + if (isset($params['feed_url'])) { + $strReq .= "AND Z.feed_url = '".$this->con->escape($params['feed_url'])."' "; + } + if (isset($params['feed_status'])) { + $strReq .= "AND Z.feed_status = ".((integer) $params['feed_status'])." "; + } + + if (!empty($params['sql'])) { + $strReq .= $params['sql'].' '; + } + + if (!$count_only) { + if (!empty($params['order'])) { + $strReq .= 'ORDER BY '.$this->con->escape($params['order']).' '; + } + else { + $strReq .= 'ORDER BY Z.feed_upddt DESC '; + } + } + + if (!$count_only && !empty($params['limit'])) { + $strReq .= $this->con->limit($params['limit']); + } + + $rs = $this->con->select($strReq); + $rs->zc = $this; + + return $rs; + } + + /** + * Get next table id. + * + * @return record record instance + */ + private function getNextId() + { + return $this->con->select( + 'SELECT MAX(feed_id) FROM '.$this->table + )->f(0) + 1; + } + + /** + * Lock a file to see if an update is ongoing. + * + * @return boolean True if file is locked + */ + public function lockUpdate() + { + try { + # Need flock function + if (!function_exists('flock')) { + throw New Exception("Can't call php function named flock"); + } + # Cache writable ? + if (!is_writable(DC_TPL_CACHE)) { + throw new Exception("Can't write in cache fodler"); + } + # Set file path + $f_md5 = md5($this->blog); + $cached_file = sprintf( + '%s/%s/%s/%s/%s.txt', + DC_TPL_CACHE, + 'periodical', + substr($f_md5, 0, 2), + substr($f_md5, 2, 2), + $f_md5 + ); + # Real path + $cached_file = path::real($cached_file, false); + # Make dir + if (!is_dir(dirname($cached_file))) { + files::makeDir(dirname($cached_file), true); + } + # Make file + if (!file_exists($cached_file)) { + !$fp = @fopen($cached_file, 'w'); + if ($fp === false) { + throw New Exception("Can't create file"); + } + fwrite($fp, '1', strlen('1')); + fclose($fp); + } + # Open file + if (!($fp = @fopen($cached_file, 'r+'))) { + throw New Exception("Can't open file"); + } + # Lock file + if (!flock($fp, LOCK_EX)) { + throw New Exception("Can't lock file"); + } + $this->lock = $fp; + + return true; + } + catch (Exception $e) { + throw $e; + } + + return false; + } + + /** + * Unlock file of update process. + * + */ + public function unlockUpdate() + { + @fclose($this->lock); + $this->lock = null; + } + + /** + * Check and add/update post related to record if needed. + * + * @param integer $id Feed Id + * @param boolean $throw Throw exception or end silently + * @return boolean True if process succeed + */ + public function checkFeedsUpdate($id=null, $throw=false) + { + # Limit to one update at a time + try { + $this->lockUpdate(); + } + catch (Exception $e) { + if ($throw) { + throw $e; + } + + return false; + } + + dt::setTZ($this->core->blog->settings->system->blog_timezone); + $time = time(); + $s = $this->core->blog->settings->zoneclearFeedServer; + + # All feeds or only one (from admin) + $f = !$id ? + $this->getFeeds(array( + 'feed_status'=>1, + 'order'=>'feed_upd_last ASC' + )) : + $this->getFeeds(array( + 'feed_id'=>$id + )); + + # No feed + if ($f->isEmpty()) { + + return false; + } + + # Set feeds user + $this->enableUser($s->zoneclearFeedServer_user); + + $updates = false; + $loop_mem = array(); + + $limit = abs((integer) $s->zoneclearFeedServer_update_limit); + if ($limit < 1) { + $limit = 10; + } + $i = 0; + + $cur_post = $this->con->openCursor($this->core->prefix.'post'); + $cur_meta = $this->con->openCursor($this->core->prefix.'meta'); + + while($f->fetch()) { + # Check if feed need update + if ($id || $i < $limit && $f->feed_status == 1 + && $time > $f->feed_upd_last + $f->feed_upd_int + ) { + $i++; + $feed = self::readFeed($f->feed_feed); + + # Nothing to parse + if (!$feed) { + # Keep active empty feed or disable it ? + if (!$s->zoneclearFeedServer_keep_empty_feed) { + $this->enableFeed($f->feed_id, false); + } + else { + # Set update time of this feed + $this->enableFeed($f->feed_id, true, $time); + } + $i++; + } + # Not updated since last visit + elseif (!$id + && '' != $feed->pubdate + && strtotime($feed->pubdate) < $f->feed_upd_last + ) { + # Set update time of this feed + $this->enableFeed($f->feed_id, true, $time); + $i++; + } + else { + # Set update time of this feed + $this->enableFeed($f->feed_id, $f->feed_status, $time); + + $this->con->begin(); + + foreach ($feed->items as $item) { + $item_TS = $item->TS ? $item->TS : $time; + + // I found that mercurial atom feed did not repect standard + $item_link = @$item->link; + if (!$item_link) { + $item_link = @$item->guid; + } + # Unknow feed item link + if (!$item_link) { + continue; + } + + $item_link = $this->con->escape($item_link); + $is_new_published_entry = false; + + # Not updated since last visit + if (!$id && $item_TS < $f->feed_upd_last) { + continue; + } + + # Fix loop twin + if (in_array($item_link, $loop_mem)) { + continue; + } + $loop_mem[] = $item_link; + + # Check if entry exists + $old_post = $this->con->select( + 'SELECT P.post_id, P.post_status '. + 'FROM '.$this->core->prefix.'post P '. + 'INNER JOIN '.$this->core->prefix.'meta M '. + 'ON P.post_id = M.post_id '. + "WHERE blog_id='".$this->blog."' ". + "AND meta_type = 'zoneclearfeed_url' ". + "AND meta_id = '".$item_link."' " + ); + + # Prepare entry cursor + $cur_post->clean(); + $cur_post->post_dt = date('Y-m-d H:i:s', $item_TS); + if ($f->cat_id) { + $cur_post->cat_id = $f->cat_id; + } + $post_content = $item->content ? $item->content : $item->description; + $cur_post->post_content = html::absoluteURLs($post_content,$feed->link); + $cur_post->post_title = $item->title ? $item->title : text::cutString(html::clean($cur_post->post_content),60); + $creator = $item->creator ? $item->creator : $f->feed_owner; + + try { + # Create entry + if ($old_post->isEmpty()) { + # Post + $cur_post->user_id = $this->core->auth->userID(); + $cur_post->post_format = 'xhtml'; + $cur_post->post_status = (integer) $s->zoneclearFeedServer_post_status_new; + $cur_post->post_open_comment = 0; + $cur_post->post_open_tb = 0; + + # --BEHAVIOR-- zoneclearFeedServerBeforePostCreate + $this->core->callBehavior( + 'zoneclearFeedServerBeforePostCreate', + $cur_post + ); + + $post_id = $this->core->auth->sudo( + array($this->core->blog, 'addPost'), + $cur_post + ); + + # --BEHAVIOR-- zoneclearFeedServerAfterPostCreate + $this->core->callBehavior( + 'zoneclearFeedServerAfterPostCreate', + $cur_post, + $post_id + ); + + # Auto tweet new post + if ($cur_post->post_status == 1) { + $is_new_published_entry = true; + } + } + # Update entry + else { + $post_id = $old_post->post_id; + + # --BEHAVIOR-- zoneclearFeedServerBeforePostUpdate + $this->core->callBehavior( + 'zoneclearFeedServerBeforePostUpdate', + $cur_post, + $post_id + ); + + $this->core->auth->sudo( + array($this->core->blog, 'updPost'), + $post_id, + $cur_post + ); + + # Quick delete old meta + $this->con->execute( + 'DELETE FROM '.$this->core->prefix.'meta '. + 'WHERE post_id = '.$post_id.' '. + "AND meta_type LIKE 'zoneclearfeed_%' " + ); + # Delete old tags + $this->core->auth->sudo( + array($this->core->meta, 'delPostMeta'), + $post_id, + 'tag' + ); + + # --BEHAVIOR-- zoneclearFeedServerAfterPostUpdate + $this->core->callBehavior( + 'zoneclearFeedServerAfterPostUpdate', + $cur_post, + $post_id + ); + } + + # Quick add new meta + $meta = new ArrayObject(); + $meta->tweeter = $f->feed_tweeter; + + $cur_meta->clean(); + $cur_meta->post_id = $post_id; + $cur_meta->meta_type = 'zoneclearfeed_url'; + $cur_meta->meta_id = $meta->url = $item_link; + $cur_meta->insert(); + + $cur_meta->clean(); + $cur_meta->post_id = $post_id; + $cur_meta->meta_type = 'zoneclearfeed_author'; + $cur_meta->meta_id = $meta->author = $creator; + $cur_meta->insert(); + + $cur_meta->clean(); + $cur_meta->post_id = $post_id; + $cur_meta->meta_type = 'zoneclearfeed_site'; + $cur_meta->meta_id = $meta->site = $f->feed_url; + $cur_meta->insert(); + + $cur_meta->clean(); + $cur_meta->post_id = $post_id; + $cur_meta->meta_type = 'zoneclearfeed_sitename'; + $cur_meta->meta_id = $meta->sitename = $f->feed_name; + $cur_meta->insert(); + + $cur_meta->clean(); + $cur_meta->post_id = $post_id; + $cur_meta->meta_type = 'zoneclearfeed_id'; + $cur_meta->meta_id = $meta->id = $f->feed_id; + $cur_meta->insert(); + + # Add new tags + $tags = $this->core->meta->splitMetaValues($f->feed_tags); + if ($f->feed_get_tags) { + + # Some feed subjects contains more than one tag + foreach($item->subject as $subjects) { + $tmp = $this->core->meta->splitMetaValues($subjects); + $tags = array_merge($tags, $tmp); + } + $tags = array_unique($tags); + } + $formated_tags = array(); + foreach ($tags as $tag) { + + # Change tags case + switch((integer) $s->zoneclearFeedServer_tag_case) { + case 3: $tag = strtoupper($tag); break; + case 2: $tag = strtolower($tag); break; + case 1: $tag = ucfirst(strtolower($tag)); break; + default: /* do nothing */ break; + } + if (!in_array($tag, $formated_tags)) { + $formated_tags[] = $tag; + $this->core->auth->sudo( + array($this->core->meta, 'setPostMeta'), + $post_id, + 'tag', + dcMeta::sanitizeMetaID($tag) + ); + } + } + $meta->tags = $formated_tags; + + # --BEHAVIOR-- zoneclearFeedServerAfterFeedUpdate + $this->core->callBehavior( + 'zoneclearFeedServerAfterFeedUpdate', + $this->core, + $is_new_published_entry, + $cur_post, + $meta + ); + + } + catch (Exception $e) { + $this->con->rollback(); + $this->enableUser(false); + $this->unlockUpdate(); + throw $e; + } + $updates = true; + } + $this->con->commit(); + } + } + } + $this->enableUser(false); + $this->unlockUpdate(); + + return true; + } + + /** + * Set permission to update post table. + * + * @param boolean $enable Enable or disable perm + */ + public function enableUser($enable=false) + { + # Enable + if ($enable) { + if (!$this->core->auth->checkUser($enable)) { + throw new Exception('Unable to set user'); + } + } + # Disable + else { + $this->core->auth = null; + $this->core->auth = new dcAuth($this->core); + } + } + + /** + * Read and parse external feeds. + * + * @param string $f Feed URL + * @return arrayObject Parsed feed + */ + public static function readFeed($f) + { + try { + $feed_reader = new feedReader; + $feed_reader->setCacheDir(DC_TPL_CACHE); + $feed_reader->setTimeout(self::$nethttp_timeout); + $feed_reader->setMaxRedirects(self::$nethttp_maxredirect); + $feed_reader->setUserAgent(self::$nethttp_agent); + + return $feed_reader->parse($f); + } + catch (Exception $e) { + + return null; + } + } + + /** + * Trigger. + */ + private function trigger() + { + $this->core->blog->triggerBlog(); + } + + /** + * Check if an URL is well formed + * + * @param string $url URL + * @return Boolean True if URL is allowed + */ + public static function validateURL($url) + { + return false !== strpos($url, 'http://') + || false !== strpos($url, 'https://'); + } + + /** + * Get full URL. + * + * Know bugs: anchor is not well parsed. + * + * @param string $root Root URL + * @param string $url An URL + * @return string Parse URL + */ + public static function absoluteURL($root, $url) + { + $host = preg_replace( + '|^([a-z]{3,}://)(.*?)/(.*)$|', + '$1$2', + $root + ); + + $parse = parse_url($url); + + if (empty($parse['scheme'])) { + if (strpos($url,'/') === 0) { + $url = $host.$url; + } + elseif (strpos($url, '#') === 0) { + $url = $root.$url; + } + elseif (preg_match('|/$|', $root)) { + $url = $root.$url; + } + else { + $url = dirname($root).'/'.$url; + } + } + + return $url; + } + + /** + * Get list of feeds status. + * + * @return array List of names/values of feeds status + */ + public static function getAllStatus() + { + return array( + __('Disabled') => '0', + __('Enabled') => '1' + ); + } + + /** + * Get list of predefined interval. + * + * @return array List of Name/time of intervals + */ + public static function getAllUpdateInterval() + { + return array( + __('Every hour') => 3600, + __('Every two hours') => 7200, + __('Two times per day') => 43200, + __('Every day') => 86400, + __('Every two days') => 172800, + __('Every week') => 604800 + ); + } + + /** + * Get list of (super)admins of current blog. + * + * @return array List of UserCNs/UserIds + */ + public function getAllBlogAdmins() + { + $admins = array(); + + # Get super admins + $rs = $this->con->select( + 'SELECT user_id, user_super, user_name, user_firstname, user_displayname '. + 'FROM '.$this->con->escapeSystem($this->core->prefix.'user').' '. + 'WHERE user_super = 1 AND user_status = 1 ' + ); + + if (!$rs->isEmpty()) { + while ($rs->fetch()) { + $user_cn = dcUtils::getUserCN( + $rs->user_id, + $rs->user_name, + $rs->user_firstname, + $rs->user_displayname + ); + $admins[$user_cn.' (super admin)'] = $rs->user_id; + } + } + + # Get admins + $rs = $this->con->select( + 'SELECT U.user_id, U.user_super, U.user_name, U.user_firstname, U.user_displayname '. + 'FROM '.$this->con->escapeSystem($this->core->prefix.'user').' U '. + 'LEFT JOIN '.$this->con->escapeSystem($this->core->prefix.'permissions').' P '. + 'ON U.user_id=P.user_id '. + 'WHERE U.user_status = 1 '. + "AND P.blog_id = '".$this->blog."' ". + "AND P.permissions LIKE '%|admin|%' " + ); + + if (!$rs->isEmpty()) { + while ($rs->fetch()) { + $user_cn = dcUtils::getUserCN( + $rs->user_id, + $rs->user_name, + $rs->user_firstname, + $rs->user_displayname + ); + $admins[$user_cn.' (admin)'] = $rs->user_id; + } + } + + return $admins; + } + + /** + * Get list of urls where entries could be hacked. + * + * @param dcCore $core dcCore instance + * @return array List of names/types of URLs + */ + public static function getPublicUrlTypes(dcCore $core) + { + $types = array(); + + # --BEHAVIOR-- zoneclearFeedServerPublicUrlTypes + $core->callBehavior('zoneclearFeedServerPublicUrlTypes', $types); + + $types[__('Home page')] = 'default'; + $types[__('Entries pages')] = 'post'; + $types[__('Tags pages')] = 'tag'; + $types[__('Archives pages')] = 'archive'; + $types[__('Category pages')] = 'category'; + $types[__('Entries feed')] = 'feed'; + + return $types; + } + + /** + * Take care about plugin tweakurls (thanks Mathieu M.). + * + * @param cursor $cur cursor instance + * @param integer $id Post Id + */ + public static function tweakurlsAfterPostCreate(cursor $cur, $id) + { + global $core; + $cur->post_url = tweakUrls::tweakBlogURL($cur->post_url); + $core->auth->sudo(array($core->blog, 'updPost'), $id, $cur); + } +} \ No newline at end of file diff --git a/inc/lib.zcfs.list.php b/inc/lib.zcfs.list.php new file mode 100644 index 0000000..011a90d --- /dev/null +++ b/inc/lib.zcfs.list.php @@ -0,0 +1,204 @@ +rs->isEmpty()) { + + return ''.__('There is no feed').'
'; + } + + $pager = new dcPager($page, $this->rs_count ,$nb_per_page, 10); + + $pager->base_url = $url; + + $html_block = + ''.__('Name').' | '. + ''.__('Feed').' | '. + ''.__('Frequency').' | '. + ''.__('Last update').' | '. + ''.__('Entries').' | '. + ''.__('Status').' | '. + '
---|
'.__('No entry').'
'; + } + + $pager = new dcPager($page, $this->rs_count, $nb_per_page, 10); + $pager->base_url = $url; + $pager->html_prev = $this->html_prev; + $pager->html_next = $this->html_next; + $pager->var_page = 'page'; + + $html_block = + ''.__('Title').' | '. + ''.__('Date').' | '. + ''.__('Category').' | '. + ''.__('Author').' | '. + ''.__('Comments').' | '. + ''.__('Trackbacks').' | '. + ''.__('Status').' | '. + '
---|
'; + if ($prev_link) { + echo $prev_link; + } + if ($next_link && $prev_link) { + echo ' - '; + } + if ($next_link) { + echo $next_link; + } + echo '
'; + } + + echo ' +'. + ''. + __('New feed').'
'. + + ''. + + $feeds_list->feedsDisplay($page, $nb_per_page, $pager_base_url, + '' + ); +} + +echo +'+'.__('Configuration').' - +zoneclearFeedServer - '.$core->plugins->moduleInfo('zoneclearFeedServer', 'version').' + +
+'; \ No newline at end of file diff --git a/js/feedsfilter.js b/js/feedsfilter.js new file mode 100644 index 0000000..188882b --- /dev/null +++ b/js/feedsfilter.js @@ -0,0 +1,31 @@ +$(function(){ + $('.checkboxes-helpers').each(function(){dotclear.checkboxesHelpers(this);}); + + $filtersform = $('#filters-form'); + $filtersform.before(''+dotclear.msg.filter_posts_list+'
') + + if( dotclear.msg.show_filters == 'false' ) { + $filtersform.hide(); + } else { + $('#filter-control') + .addClass('open') + .text(dotclear.msg.cancel_the_filter); + } + + $('#filter-control').click(function() { + if( $(this).hasClass('open') ) { + if( dotclear.msg.show_filters == 'true' ) { + return true; + } else { + $filtersform.hide(); + $(this).removeClass('open') + .text(dotclear.msg.filter_posts_list); + } + } else { + $filtersform.show(); + $(this).addClass('open') + .text(dotclear.msg.cancel_the_filter); + } + return false; + }); +}); \ No newline at end of file diff --git a/js/post.js b/js/post.js new file mode 100644 index 0000000..4b3e492 --- /dev/null +++ b/js/post.js @@ -0,0 +1,7 @@ +$(function(){ + /* toogle admin form sidebar */ + $('#zcfs h5').toggleWithLegend( + $('#zcfs').children().not('h5'), + {cookie:'dcx_zcfs_admin_form_sidebar',legend_click:true} + ); +}); \ No newline at end of file diff --git a/js/postsfilter.js b/js/postsfilter.js new file mode 100644 index 0000000..2a12570 --- /dev/null +++ b/js/postsfilter.js @@ -0,0 +1,31 @@ +$(function(){ + $('.checkboxes-helpers').each(function(){dotclear.checkboxesHelpers(this);}); + + $filtersform = $('#filters-form'); + $filtersform.before(''+dotclear.msg.filter_posts_list+'
') + + if( dotclear.msg.show_filters == 'false' ) { + $filtersform.hide(); + } else { + $('#filter-control') + .addClass('open') + .text(dotclear.msg.cancel_the_filter); + } + + $('#filter-control').click(function() { + if( $(this).hasClass('open') ) { + if( dotclear.msg.show_filters == 'true' ) { + return true; + } else { + $filtersform.hide(); + $(this).removeClass('open') + .text(dotclear.msg.filter_posts_list); + } + } else { + $filtersform.show(); + $(this).addClass('open') + .text(dotclear.msg.cancel_the_filter); + } + return false; + }); +}); \ No newline at end of file diff --git a/locales/en/help/zoneclearFeedServer.html b/locales/en/help/zoneclearFeedServer.html new file mode 100644 index 0000000..2ae9478 --- /dev/null +++ b/locales/en/help/zoneclearFeedServer.html @@ -0,0 +1,15 @@ + + + +