1. <?php
  2.  
  3. /*
  4. * Plugin Name: WP-o-Matic
  5. * Description: Enables administrators to create posts automatically from RSS/Atom feeds.
  6. * Author: Guillermo Rauch
  7. * Plugin URI: http://devthought.com/wp-o-matic-the-wordpress-rss-agreggator/
  8. * Version: 1.0RC4-6
  9. * =======================================================================
  10.  
  11. Todo:
  12.  
  13. - 'View campaign' view, with stats, thus getting rid of Tools tab
  14. - Bulk actions in campaign list
  15. - 'Time ago' for 'Last active' (on hover) in campaign list
  16. - Image thumbnailing option
  17. - More advanced post templates
  18. - Advanced filters
  19. - Import drag and drop to current campaigns.
  20. - Export extended OPML to save WP-o-Matic options
  21. - Proper commenting
  22. - Upgrading support
  23. - Plugins support
  24.  
  25. Changelog:
  26. - 0.1beta
  27. WP-o-Matic released.
  28.  
  29. - 0.2beta:
  30. Fixed use of MagpieRSS legacy functions.
  31. Updated cron code to check every twenty minutes.
  32. Wordpress pseudocron disabled.
  33.  
  34. - 1.0RC1:
  35. Renamed everything to WPOMatic, instead of the previous WPRSS.
  36. Renamed "lib" to "inc"
  37. SimplePie updated to 1.0.1 (Razzleberry), relocated and server compatibility tests included.
  38. Static reusable functions moved to WPOTools class.
  39. Improved Unix detection for cron.
  40. Removed MooTools dependency for optimization reasons.
  41. Redesigned admin panel, now divided into sections.
  42. Logging now database-based.
  43. Posts are now saved in a WP-o-Matic table. They're later parsed and created as posts.
  44. Added a dashboard with quick stats and log display.
  45. Added campaign support to centralize options for multiple feeds.
  46. Added import/export support through OPML files
  47. Added image caching capabilities.
  48. Added word/phrase rewriting and relinking capabilities.
  49. Added nonce support
  50. Added i18n support with translation domain 'wpomatic'
  51. Added help throughout the system.
  52.  
  53. - 1.0RC2
  54. Added compatibility with Wordpress 2.3 and 2.4
  55. Added setup screen
  56. Stopped using simplepie get_id in favor of our own simpler hash generation
  57. Fixed setup screen bug
  58.  
  59. - 1.0RC3
  60. Now compatible with Wordpress 2.5
  61. Categories shown with indentation (parent > children now separated)
  62. SimplePie updated to 1.1.1
  63. Fixed broken cron command
  64. Fixed broken export on some systems
  65. Fixed broken redirect when resetting a campaign
  66. Everything now stored in GMT to avoid time issues. Gotten rid of NOW() functions in favor of WP time functions
  67. Fixed bug with validation upon deletion of feeds in existing campaigns
  68. Fixed bug with allow comments setting.
  69. Fixed bug with logs dates
  70. Fixed bug with double quote escaping (fixes campaign templates / rewrite html bugs)
  71. Username in options tab changed to a more handy select box.
  72. Interface now looks better on IE (d'oh)
  73. Added many help files
  74. Fixed annoying duplicates bug
  75. Fixed small bug in import with labels
  76. Fixed bug with categories in edit mode
  77. Fixed Tools post changes.
  78. Fixed issue with empty rewrite replacements
  79. Non-regex rewrite replacements now case insensitive.
  80. Fixed bugs with 'use feed date' option.
  81. Fixed footer copyright
  82. Fixed bad dates in 'view all' logs
  83. Log message field made text
  84. Fields changed to datetime format
  85. Clean logs function fixed
  86.  
  87. r6:
  88. str_ireplace now works with arrays
  89. queries adjusted to work on all server configurations
  90.  
  91. - 1.0RC4
  92. Tables not deleted anymore upon installation
  93. Fixed SimplePie error report.
  94. Fixed small post content bug (not hidden by default)
  95. Fixed cron url
  96. Removed inverted quotes from queries
  97. Fixed notices in debug mode
  98. No error showing for campaigns w/o feeds fixed
  99.  
  100. r1
  101. MySQL incompatibility with query solved
  102. r2
  103. Hostings with basedir restriction now don't show errors
  104. r3
  105. Fixed date issue that might have caused potential duplication problems
  106. r4
  107. Fixed cron job mysql query
  108. r5
  109. Unlimited max items bug fixed.
  110. r6
  111. Fixed cron job gmt times.
  112. */
  113.  
  114. # WP-o-Matic paths. With trailing slash.
  115. define('WPODIR', dirname(__FILE__) . '/');
  116. define('WPOINC', WPODIR . 'inc/');
  117. define('WPOTPL', WPOINC . 'admin/');
  118.  
  119. # Dependencies
  120. require_once( WPOINC . 'tools.class.php' );
  121.  
  122. class WPOMatic {
  123.  
  124. # Internal
  125. var $version = '1.0RC4-6';
  126.  
  127. var $newsetup = false; # true if this version introduces setup changes
  128.  
  129. var $sections = array('home', 'setup', 'list', 'add', 'edit', 'options', 'import', 'export',
  130. 'reset', 'delete', 'logs', 'testfeed', 'forcefetch');
  131.  
  132. var $campaign_structure = array('main' => array(), 'rewrites' => array(),
  133. 'categories' => array(), 'feeds' => array());
  134.  
  135. # __construct()
  136. function WPOMatic()
  137. {
  138. global $wpdb, $wp_version;
  139.  
  140. # Table names init
  141. $this->db = array(
  142. 'campaign' => $wpdb->prefix . 'wpo_campaign',
  143. 'campaign_category' => $wpdb->prefix . 'wpo_campaign_category',
  144. 'campaign_feed' => $wpdb->prefix . 'wpo_campaign_feed',
  145. 'campaign_word' => $wpdb->prefix . 'wpo_campaign_word',
  146. 'campaign_post' => $wpdb->prefix . 'wpo_campaign_post',
  147. 'log' => $wpdb->prefix . 'wpo_log'
  148. );
  149.  
  150. # Are we running the new admin panel (2.5+) ?
  151. $this->newadmin = version_compare($wp_version, '2.5.0', '>=');
  152.  
  153. # Is installed ?
  154. $this->installed = get_option('wpo_version');
  155. $this->setup = get_option('wpo_setup');
  156.  
  157. # Actions
  158. add_action('activate_wp-o-matic/wpomatic.php', array(&$this, 'activate')); # Plugin activated
  159. add_action('deactivate_wp-o-matic/wpomatic.php', array(&$this, 'deactivate')); # Plugin deactivated
  160. add_action('init', array(&$this, 'init')); # Wordpress init
  161. add_action('admin_head', array(&$this, 'adminHead')); # Admin head
  162. add_action('admin_footer', array(&$this, 'adminWarning')); # Admin footer
  163. add_action('admin_menu', array(&$this, 'adminMenu')); # Admin menu creation
  164.  
  165. # Ajax actions
  166. add_action('wp_ajax_delete-campaign', array(&$this, 'adminDelete'));
  167. add_action('wp_ajax_test-feed', array(&$this, 'adminTestfeed'));
  168.  
  169. # Filters
  170. add_action('the_permalink', array(&$this, 'filterPermalink'));
  171.  
  172. # WP-o-Matic URIs. Without trailing slash
  173. $this->optionsurl = get_option('siteurl') . '/wp-admin/options-general.php';
  174. $this->adminurl = $this->optionsurl . '?page=wpomatic.php';
  175. $this->pluginpath = get_option('siteurl') . '/wp-content/plugins/wp-o-matic';
  176. $this->helpurl = $this->pluginpath . '/help.php?item=';
  177. $this->tplpath = $this->pluginpath . '/inc/admin';
  178. $this->cachepath = ABSPATH . '/wp-content/uploads';
  179.  
  180. # Cron command / url
  181. $this->cron_url = $this->pluginpath . '/cron.php?code=' . get_option('wpo_croncode');
  182. $this->cron_command = attribute_escape('*/20 * * * * '. $this->getCommand() . ' ' . $this->cron_url);
  183. }
  184.  
  185. /**
  186. * Called when plugin is activated
  187. *
  188. *
  189. */
  190. function activate($force_install = false)
  191. {
  192. global $wpdb;
  193.  
  194. if(file_exists(ABSPATH . '/wp-admin/upgrade-functions.php'))
  195. require_once(ABSPATH . '/wp-admin/upgrade-functions.php');
  196. else
  197. require_once(ABSPATH . '/wp-admin/includes/upgrade.php');
  198.  
  199. # Options
  200. WPOTools::addMissingOptions(array(
  201. 'wpo_log' => array(1, 'Log WP-o-Matic actions'),
  202. 'wpo_log_stdout' => array(0, 'Output logs to browser while a campaign is being processed'),
  203. 'wpo_unixcron' => array(WPOTools::isUnix(), 'Use unix-style cron'),
  204. 'wpo_croncode' => array(substr(md5(time()), 0, 8), 'Cron job password.'),
  205. 'wpo_cacheimages' => array(0, 'Cache all images. Overrides campaign options'),
  206. 'wpo_cachepath' => array('cache', 'Cache path relative to wpomatic directory')
  207. ));
  208.  
  209. // only re-install if new version or uninstalled
  210. if($force_install || ! $this->installed || $this->installed != $this->version)
  211. {
  212. # wpo_campaign
  213. dbDelta( "CREATE TABLE {$this->db['campaign']} (
  214. id int(11) unsigned NOT NULL auto_increment,
  215. title varchar(255) NOT NULL default '',
  216. active tinyint(1) default '1',
  217. slug varchar(250) default '',
  218. template MEDIUMTEXT default '',
  219. frequency int(5) default '180',
  220. feeddate tinyint(1) default '0',
  221. cacheimages tinyint(1) default '1',
  222. posttype enum('publish','draft','private') NOT NULL default 'publish',
  223. authorid int(11) default NULL,
  224. comment_status enum('open','closed','registered_only') NOT NULL default 'open',
  225. allowpings tinyint(1) default '1',
  226. dopingbacks tinyint(1) default '1',
  227. max smallint(3) default '10',
  228. linktosource tinyint(1) default '0',
  229. count int(11) default '0',
  230. lastactive datetime NOT NULL default '0000-00-00 00:00:00',
  231. created_on datetime NOT NULL default '0000-00-00 00:00:00',
  232. PRIMARY KEY (id)
  233. );" );
  234.  
  235. # wpo_campaign_category
  236. dbDelta( "CREATE TABLE {$this->db['campaign_category']} (
  237. id int(11) unsigned NOT NULL auto_increment,
  238. category_id int(11) NOT NULL,
  239. campaign_id int(11) NOT NULL,
  240. PRIMARY KEY (id)
  241. );" );
  242.  
  243. # wpo_campaign_feed
  244. dbDelta( "CREATE TABLE {$this->db['campaign_feed']} (
  245. id int(11) unsigned NOT NULL auto_increment,
  246. campaign_id int(11) NOT NULL default '0',
  247. url varchar(255) NOT NULL default '',
  248. type varchar(255) NOT NULL default '',
  249. title varchar(255) NOT NULL default '',
  250. description varchar(255) NOT NULL default '',
  251. logo varchar(255) default '',
  252. count int(11) default '0',
  253. hash varchar(255) default '',
  254. lastactive datetime NOT NULL default '0000-00-00 00:00:00',
  255. PRIMARY KEY (id)
  256. );" );
  257.  
  258. # wpo_campaign_post
  259. dbDelta( "CREATE TABLE {$this->db['campaign_post']} (
  260. id int(11) unsigned NOT NULL auto_increment,
  261. campaign_id int(11) NOT NULL,
  262. feed_id int(11) NOT NULL,
  263. post_id int(11) NOT NULL,
  264. hash varchar(255) default '',
  265. PRIMARY KEY (id)
  266. );" );
  267.  
  268. # wpo_campaign_word
  269. dbDelta( "CREATE TABLE {$this->db['campaign_word']} (
  270. id int(11) unsigned NOT NULL auto_increment,
  271. campaign_id int(11) NOT NULL,
  272. word varchar(255) NOT NULL default '',
  273. regex tinyint(1) default '0',
  274. rewrite tinyint(1) default '1',
  275. rewrite_to varchar(255) default '',
  276. relink varchar(255) default '',
  277. PRIMARY KEY (id)
  278. );" );
  279.  
  280. # wpo_log
  281. dbDelta( "CREATE TABLE {$this->db['log']} (
  282. id int(11) unsigned NOT NULL auto_increment,
  283. message mediumtext NOT NULL default '',
  284. created_on datetime NOT NULL default '0000-00-00 00:00:00',
  285. PRIMARY KEY (id)
  286. );" );
  287.  
  288.  
  289. add_option('wpo_version', $this->version, 'Installed version log');
  290.  
  291. $this->installed = true;
  292. }
  293. }
  294.  
  295. /**
  296. * Called when plugin is deactivated
  297. *
  298. *
  299. */
  300. function deactivate()
  301. {
  302. }
  303.  
  304. /**
  305. * Uninstalls
  306. *
  307. *
  308. */
  309. function uninstall()
  310. {
  311. global $wpdb;
  312.  
  313. foreach($this->db as $table)
  314. $wpdb->query("DROP TABLE {$table} ");
  315.  
  316. // Delete options
  317. WPOTools::deleteOptions(array('wpo_log', 'wpo_log_stdout', 'wpo_unixcron', 'wpo_croncode', 'wpo_cacheimages', 'wpo_cachepath'));
  318. }
  319.  
  320. /**
  321. * Checks that WP-o-Matic tables exist
  322. *
  323. *
  324. */
  325. function tablesExist()
  326. {
  327. global $wpdb;
  328.  
  329. foreach($this->db as $table)
  330. {
  331. if(! $wpdb->query("SELECT * FROM {$table}"))
  332. return false;
  333. }
  334.  
  335. return true;
  336. }
  337.  
  338.  
  339. /**
  340. * Called when blog is initialized
  341. *
  342. *
  343. */
  344. function init()
  345. {
  346. global $wpdb;
  347.  
  348. if($this->installed)
  349. {
  350. if(! get_option('wpo_unixcron'))
  351. $this->processAll();
  352.  
  353. if(isset($_REQUEST['page']))
  354. {
  355. if(isset($_REQUEST['campaign_add']) || isset($_REQUEST['campaign_edit']))
  356. $this->adminCampaignRequest();
  357.  
  358. $this->adminExportProcess();
  359. $this->adminInit();
  360. }
  361. }
  362. }
  363.  
  364. /**
  365. * Saves a log message to database
  366. *
  367. *
  368. * @param string $message Message to save
  369. */
  370. function log($message)
  371. {
  372. global $wpdb;
  373.  
  374. if(get_option('wpo_log_stdout'))
  375. echo $message;
  376.  
  377. if(get_option('wpo_log'))
  378. {
  379. $message = $wpdb->escape($message);
  380. $time = current_time('mysql', true);
  381. $wpdb->query("INSERT INTO {$this->db['log']} (message, created_on) VALUES ('{$message}', '{$time}') ");
  382. }
  383. }
  384.  
  385. /**
  386. * Called by cron.php to update the site
  387. *
  388. *
  389. */
  390. function runCron($log = true)
  391. {
  392. $this->log('Running cron job');
  393. $this->processAll();
  394. }
  395.  
  396. /**
  397. * Finds a suitable command to run cron
  398. *
  399. * @return string command
  400. **/
  401. function getCommand()
  402. {
  403. $commands = array(
  404. @WPOTools::getBinaryPath('curl'),
  405. @WPOTools::getBinaryPath('wget'),
  406. @WPOTools::getBinaryPath('lynx', ' -dump'),
  407. @WPOTools::getBinaryPath('ftp')
  408. );
  409.  
  410. return WPOTools::pick($commands[0], $commands[1], $commands[2], $commands[3], '<em>{wget or similar command here}</em>');
  411. }
  412.  
  413. /**
  414. * Determines what the title has to link to
  415. *
  416. * @return string new text
  417. **/
  418. function filterPermalink($url)
  419. {
  420. // if from admin panel
  421. if($this->admin)
  422. return $url;
  423.  
  424. if(get_the_ID())
  425. {
  426. $campaignid = (int) get_post_meta(get_the_ID(), 'wpo_campaignid', true);
  427.  
  428. if($campaignid)
  429. {
  430. $campaign = $this->getCampaignById($campaignid);
  431. if($campaign->linktosource)
  432. return get_post_meta(get_the_ID(), 'wpo_sourcepermalink', true);
  433. }
  434.  
  435. return $url;
  436. }
  437. }
  438.  
  439. /**
  440. * Processes all campaigns
  441. *
  442. */
  443. function processAll()
  444. {
  445. @set_time_limit(0);
  446.  
  447. $campaigns = $this->getCampaigns('unparsed=1');
  448.  
  449. foreach($campaigns as $campaign)
  450. {
  451. $this->processCampaign($campaign);
  452. }
  453. }
  454.  
  455. /**
  456. * Processes a campaign
  457. *
  458. * @param object $campaign Campaign database object
  459. * @return integer Number of processed items
  460. */
  461. function processCampaign(&$campaign)
  462. {
  463. global $wpdb;
  464.  
  465. @set_time_limit(0);
  466. ob_implicit_flush();
  467.  
  468. // Get campaign
  469. $campaign = is_numeric($campaign) ? $this->getCampaignById($campaign) : $campaign;
  470.  
  471. // Log
  472. $this->log('Processing campaign ' . $campaign->title . ' (ID: ' . $campaign->id . ')');
  473.  
  474. // Get feeds
  475. $count = 0;
  476. $feeds = $this->getCampaignFeeds($campaign->id);
  477.  
  478. foreach($feeds as $feed)
  479. $count += $this->processFeed($campaign, $feed);
  480.  
  481. $wpdb->query(WPOTools::updateQuery($this->db['campaign'], array(
  482. 'count' => $campaign->count + $count,
  483. 'lastactive' => current_time('mysql', true)
  484. ), "id = {$campaign->id}"));
  485.  
  486. return $count;
  487. }
  488.  
  489. /**
  490. * Processes a feed
  491. *
  492. * @param $campaign object Campaign database object
  493. * @param $feed object Feed database object
  494. * @return The number of items added to database
  495. */
  496. function processFeed(&$campaign, &$feed)
  497. {
  498. global $wpdb;
  499.  
  500. @set_time_limit(0);
  501.  
  502. // Log
  503. $this->log('Processing feed ' . $feed->title . ' (ID: ' . $feed->id . ')');
  504.  
  505. // Access the feed
  506. $simplepie = $this->fetchFeed($feed->url, false, $campaign->max);
  507.  
  508. // Get posts (last is first)
  509. $items = array();
  510. $count = 0;
  511.  
  512. // Get all rows for this campaign all at once, BUT LIMITED to the last 30 posts
  513. $rows = $wpdb->get_results("SELECT * FROM {$this->db['campaign_post']} "
  514. . "WHERE campaign_id = {$campaign->id} AND feed_id = {$feed->id} ORDER BY post_id DESC LIMIT 30 ");
  515.  
  516. foreach($simplepie->get_items() as $item)
  517. {
  518. if($feed->hash == $this->getItemHash($item))
  519. {
  520. if($count == 0) $this->log('No new posts');
  521. break;
  522. }
  523.  
  524. // look to see if $item is a duplicate of an existing $rows row
  525.  
  526. foreach($rows as $row)
  527. {
  528. if($row->hash == $this->getItemHash($item))
  529. {
  530. $this->log('Filtering duplicate post');
  531. break;
  532. }
  533.  
  534. }
  535.  
  536. $count++;
  537. array_unshift($items, $item);
  538.  
  539. if($count == $campaign->max)
  540. {
  541. $this->log('Campaign fetch limit reached at ' . $campaign->max);
  542. break;
  543. }
  544. }
  545.  
  546. // Processes post stack
  547. foreach($items as $item)
  548. {
  549. $this->processItem($campaign, $feed, $item);
  550. $lasthash = $this->getItemHash($item);
  551. }
  552.  
  553. // If we have added items, let's update the hash
  554. if($count)
  555. {
  556. $wpdb->query(WPOTools::updateQuery($this->db['campaign_feed'], array(
  557. 'count' => $count,
  558. 'lastactive' => current_time('mysql', true),
  559. 'hash' => $lasthash
  560. ), "id = {$feed->id}"));
  561.  
  562. $this->log( $count . ' posts added' );
  563. }
  564.  
  565. return $count;
  566. }
  567.  
  568. /**
  569. * Processes an item
  570. *
  571. * @param $item object SimplePie_Item object
  572. */
  573. function getItemHash($item)
  574. {
  575. return sha1($item->get_title() . $item->get_permalink());
  576. }
  577.  
  578. /**
  579. * Processes an item
  580. *
  581. * @param $campaign object Campaign database object
  582. * @param $feed object Feed database object
  583. * @param $item object SimplePie_Item object
  584. */
  585. function processItem(&$campaign, &$feed, &$item)
  586. {
  587. global $wpdb;
  588.  
  589. $this->log('Processing item');
  590.  
  591. // Item content
  592. $content = $this->parseItemContent($campaign, $feed, $item);
  593.  
  594. // Item date
  595. if($campaign->feeddate && ($item->get_date('U') > (current_time('timestamp', 1) - $campaign->frequency) && $item->get_date('U') < current_time('timestamp', 1)))
  596. $date = $item->get_date('U');
  597. else
  598. $date = null;
  599.  
  600. // Categories
  601. $categories = $this->getCampaignData($campaign->id, 'categories');
  602.  
  603. // Meta
  604. $meta = array(
  605. 'wpo_campaignid' => $campaign->id,
  606. 'wpo_feedid' => $feed->id,
  607. 'wpo_sourcepermalink' => $item->get_permalink()
  608. );
  609.  
  610. // Create post
  611. $postid = $this->insertPost($wpdb->escape($item->get_title()), $wpdb->escape($content), $date, $categories, $campaign->posttype, $campaign->authorid, $campaign->allowpings, $campaign->comment_status, $meta);
  612.  
  613. // If pingback/trackbacks
  614. if($campaign->dopingbacks)
  615. {
  616. $this->log('Processing item pingbacks');
  617.  
  618. require_once(ABSPATH . WPINC . '/comment.php');
  619. pingback($content, $postid);
  620. }
  621.  
  622. // Save post to log database
  623. $wpdb->query(WPOTools::insertQuery($this->db['campaign_post'], array(
  624. 'campaign_id' => $campaign->id,
  625. 'feed_id' => $feed->id,
  626. 'post_id' => $postid,
  627. 'hash' => $this->getItemHash($item)
  628. )));
  629. }
  630.  
  631. /**
  632. * Processes an item
  633. *
  634. * @param $campaign object Campaign database object
  635. * @param $feed object Feed database object
  636. * @param $item object SimplePie_Item object
  637. */
  638. function isDuplicate(&$campaign, &$feed, &$item)
  639. {
  640. global $wpdb;
  641. $hash = $this->getItemHash($item);
  642. $row = $wpdb->get_row("SELECT * FROM {$this->db['campaign_post']} "
  643. . "WHERE campaign_id = {$campaign->id} AND feed_id = {$feed->id} AND hash = '$hash' ");
  644. return !! $row;
  645. }
  646.  
  647. /**
  648. * Writes a post to blog
  649. *
  650. *
  651. * @param string $title Post title
  652. * @param string $content Post content
  653. * @param integer $timestamp Post timestamp
  654. * @param array $category Array of categories
  655. * @param string $status 'draft', 'published' or 'private'
  656. * @param integer $authorid ID of author.
  657. * @param boolean $allowpings Allow pings
  658. * @param boolean $comment_status 'open', 'closed', 'registered_only'
  659. * @param array $meta Meta key / values
  660. * @return integer Created post id
  661. */
  662. function insertPost($title, $content, $timestamp = null, $category = null, $status = 'draft', $authorid = null, $allowpings = true, $comment_status = 'open', $meta = array())
  663. {
  664. $date = ($timestamp) ? gmdate('Y-m-d H:i:s', $timestamp + (get_option('gmt_offset') * 3600)) : null;
  665. $postid = wp_insert_post(array(
  666. 'post_title' => $title,
  667. 'post_content' => $content,
  668. 'post_content_filtered' => $content,
  669. 'post_category' => $category,
  670. 'post_status' => $status,
  671. 'post_author' => $authorid,
  672. 'post_date' => $date,
  673. 'comment_status' => $comment_status,
  674. 'ping_status' => $allowpings
  675. ));
  676.  
  677. foreach($meta as $key => $value)
  678. $this->insertPostMeta($postid, $key, $value);
  679.  
  680. return $postid;
  681. }
  682.  
  683. /**
  684. * insertPostMeta
  685. *
  686. *
  687. */
  688. function insertPostMeta($postid, $key, $value) {
  689. global $wpdb;
  690.  
  691. $result = $wpdb->query( "INSERT INTO $wpdb->postmeta (post_id,meta_key,meta_value ) "
  692. . " VALUES ('$postid','$key','$value') ");
  693.  
  694. return $wpdb->insert_id;
  695. }
  696.  
  697. /**
  698. * Parses an item content
  699. *
  700. * @param $campaign object Campaign database object
  701. * @param $feed object Feed database object
  702. * @param $item object SimplePie_Item object
  703. */
  704. function parseItemContent(&$campaign, &$feed, &$item)
  705. {
  706. $content = $item->get_content();
  707.  
  708. // Caching
  709. if(get_option('wpo_cacheimages') || $campaign->cacheimages)
  710. {
  711. $images = WPOTools::parseImages($content);
  712. $urls = $images[2];
  713.  
  714. if(sizeof($urls))
  715. {
  716. $this->log('Caching images');
  717.  
  718. foreach($urls as $url)
  719. {
  720. $newurl = $this->cacheRemoteImage($url);
  721. if($newurl)
  722. $content = str_replace($url, $newurl, $content);
  723. }
  724. }
  725. }
  726.  
  727. // Template parse
  728. $vars = array(
  729. '{content}',
  730. '{title}',
  731. '{permalink}',
  732. '{feedurl}',
  733. '{feedtitle}',
  734. '{feedlogo}',
  735. '{campaigntitle}',
  736. '{campaignid}',
  737. '{campaignslug}'
  738. );
  739.  
  740. $replace = array(
  741. $content,
  742. $item->get_title(),
  743. $item->get_link(),
  744. $feed->url,
  745. $feed->title,
  746. $feed->logo,
  747. $campaign->title,
  748. $campaign->id,
  749. $campaign->slug
  750. );
  751.  
  752. $content = str_ireplace($vars, $replace, ($campaign->template) ? $campaign->template : '{content}');
  753.  
  754. // Rewrite
  755. $rewrites = $this->getCampaignData($campaign->id, 'rewrites');
  756. foreach($rewrites as $rewrite)
  757. {
  758. $origin = $rewrite['origin']['search'];
  759.  
  760. if(isset($rewrite['rewrite']))
  761. {
  762. $reword = isset($rewrite['relink'])
  763. ? '<a href="'. $rewrite['relink'] .'">' . $rewrite['rewrite'] . '</a>'
  764. : $rewrite['rewrite'];
  765.  
  766. if($rewrite['origin']['regex'])
  767. {
  768. $content = preg_replace($origin, $reword, $content);
  769. } else
  770. $content = str_ireplace($origin, $reword, $content);
  771. } else if(isset($rewrite['relink']))
  772. $content = str_ireplace($origin, '<a href="'. $rewrite['relink'] .'">' . $origin . '</a>', $content);
  773. }
  774.  
  775. return $content;
  776. }
  777.  
  778. /**
  779. * Cache remote image
  780. *
  781. * @return string New url
  782. */
  783. function cacheRemoteImage($url)
  784. {
  785. $time = current_time('timestamp');
  786.  
  787. $y = date("Y", $time);
  788. $m = date("m", $time);
  789. $d = date("d", $time);
  790.  
  791. $subdir = "$y/$m/$d";
  792.  
  793. $contents = @file_get_contents($url);
  794. $filename = substr(md5(time()), 0, 5) . '_' . basename($url);
  795.  
  796. $cachepath = $this->cachepath;
  797.  
  798. $fullcachepath = $cachepath . '/' . $subdir;
  799.  
  800. if ( !file_exists ($fullcachepath) ) {
  801. mkdir("$fullcachepath", 0777, TRUE);
  802. }
  803. if(is_writable($cachepath) && $contents) {
  804. file_put_contents($cachepath . '/' . $subdir . '/' . $filename, $contents);
  805. chmod("$fullcachepath/$filename", 0755);
  806. chown("$fullcachepath/$filename", "astrodispatch");
  807. return get_option('siteurl') . '/wp-content/uploads/' . $subdir . '/' . $filename;
  808. }
  809.  
  810. return false;
  811. }
  812.  
  813. /**
  814. * Parses a feed with SimplePie
  815. *
  816. * @param boolean $stupidly_fast Set fast mode. Best for checks
  817. * @param integer $max Limit of items to fetch
  818. * @return SimplePie_Item Feed object
  819. **/
  820. function fetchFeed($url, $stupidly_fast = false, $max = 0)
  821. {
  822. # SimplePie
  823. if(! class_exists('SimplePie'))
  824. require_once( WPOINC . 'simplepie/simplepie.class.php' );
  825.  
  826. $feed = new SimplePie();
  827. $feed->enable_order_by_date(false); // thanks Julian Popov
  828. $feed->set_feed_url($url);
  829. $feed->set_item_limit($max);
  830. $feed->set_stupidly_fast($stupidly_fast);
  831. $feed->enable_cache(false);
  832. $feed->init();
  833. $feed->handle_content_type();
  834.  
  835. return $feed;
  836. }
  837.  
  838. /**
  839. * Returns all blog usernames (in form [user_login => display_name (user_login)] )
  840. *
  841. * @return array $usernames
  842. **/
  843. function getBlogUsernames()
  844. {
  845. $return = array();
  846. $users = get_users_of_blog();
  847.  
  848. foreach($users as $user)
  849. {
  850. if($user->display_name == $user->user_login)
  851. $return[$user->user_login] = "{$user->display_name}";
  852. else
  853. $return[$user->user_login] = "{$user->display_name} ({$user->user_login})";
  854. }
  855.  
  856. return $return;
  857. }
  858.  
  859. /**
  860. * Returns all data for a campaign
  861. *
  862. *
  863. */
  864. function getCampaignData($id, $section = null)
  865. {
  866. global $wpdb;
  867. $campaign = (array) $this->getCampaignById($id);
  868.  
  869. if($campaign)
  870. {
  871. $campaign_data = $this->campaign_structure;
  872.  
  873. // Main
  874. if(!$section || $section == 'main')
  875. {
  876. $campaign_data['main'] = array_merge($campaign_data['main'], $campaign);
  877. $userdata = get_userdata($campaign_data['main']['authorid']);
  878. $campaign_data['main']['author'] = $userdata->user_login;
  879. }
  880.  
  881. // Categories
  882. if(!$section || $section == 'categories')
  883. {
  884. $categories = $wpdb->get_results("SELECT * FROM {$this->db['campaign_category']} WHERE campaign_id = $id");
  885. foreach($categories as $category)
  886. $campaign_data['categories'][] = $category->category_id;
  887. }
  888.  
  889. // Feeds
  890. if(!$section || $section == 'feeds')
  891. {
  892. $campaign_data['feeds']['edit'] = array();
  893.  
  894. $feeds = $this->getCampaignFeeds($id);
  895. foreach($feeds as $feed)
  896. $campaign_data['feeds']['edit'][$feed->id] = $feed->url;
  897. }
  898.  
  899. // Rewrites
  900. if(!$section || $section == 'rewrites')
  901. {
  902. $rewrites = $wpdb->get_results("SELECT * FROM {$this->db['campaign_word']} WHERE campaign_id = $id");
  903. foreach($rewrites as $rewrite)
  904. {
  905. $word = array('origin' => array('search' => $rewrite->word, 'regex' => $rewrite->regex), 'rewrite' => $rewrite->rewrite_to, 'relink' => $rewrite->relink);
  906.  
  907. if(! $rewrite->rewrite) unset($word['rewrite']);
  908. if(empty($rewrite->relink)) unset($word['relink']);
  909.  
  910. $campaign_data['rewrites'][] = $word;
  911. }
  912. }
  913.  
  914. if($section)
  915. return $campaign_data[$section];
  916.  
  917. return $campaign_data;
  918. }
  919.  
  920. return false;
  921. }
  922.  
  923. /**
  924. * Retrieves logs from database
  925. *
  926. *
  927. */
  928. function getLogs($args = '')
  929. {
  930. global $wpdb;
  931. extract(WPOTools::getQueryArgs($args, array('orderby' => 'created_on',
  932. 'ordertype' => 'DESC',
  933. 'limit' => null,
  934. 'page' => null,
  935. 'perpage' => null)));
  936. if(!is_null($page))
  937. {
  938. if($page == 0) $page = 1;
  939. $page--;
  940.  
  941. $start = $page * $perpage;
  942. $end = $start + $perpage;
  943. $limit = "LIMIT {$start}, {$end}";
  944. }
  945.  
  946. return $wpdb->get_results("SELECT * FROM {$this->db['log']} ORDER BY $orderby $ordertype $limit");
  947. }
  948.  
  949. /**
  950. * Retrieves a campaign by its id
  951. *
  952. *
  953. */
  954. function getCampaignById($id)
  955. {
  956. global $wpdb;
  957.  
  958. $id = intval($id);
  959. return $wpdb->get_row("SELECT * FROM {$this->db['campaign']} WHERE id = $id");
  960. }
  961.  
  962. /**
  963. * Retrieves a feed by its id
  964. *
  965. *
  966. */
  967. function getFeedById($id)
  968. {
  969. global $wpdb;
  970.  
  971. $id = intval($id);
  972. return $wpdb->get_row("SELECT * FROM {$this->db['campaign_feed']} WHERE id = $id");
  973. }
  974.  
  975. /**
  976. * Retrieves campaigns from database
  977. *
  978. *
  979. */
  980. function getCampaigns($args = '')
  981. {
  982. global $wpdb;
  983. extract(WPOTools::getQueryArgs($args, array('fields' => '*',
  984. 'search' => '',
  985. 'orderby' => 'created_on',
  986. 'ordertype' => 'DESC',
  987. 'where' => '',
  988. 'unparsed' => false,
  989. 'limit' => null)));
  990.  
  991. if(! empty($search))
  992. $where .= " AND title LIKE '%{$search}%' ";
  993.  
  994. if($unparsed)
  995. $where .= " AND active = 1 AND (frequency + UNIX_TIMESTAMP(lastactive)) < ". (current_time('timestamp', true) - get_option('gmt_offset') * 3600) . " ";
  996.  
  997. $sql = "SELECT $fields FROM {$this->db['campaign']} WHERE 1 = 1 $where "
  998. . "ORDER BY $orderby $ordertype $limit";
  999.  
  1000. return $wpdb->get_results($sql);
  1001. }
  1002.  
  1003. /**
  1004. * Retrieves feeds for a certain campaign
  1005. *
  1006. * @param integer $id Campaign id
  1007. */
  1008. function getCampaignFeeds($id)
  1009. {
  1010. global $wpdb;
  1011. return $wpdb->get_results("SELECT * FROM {$this->db['campaign_feed']} WHERE campaign_id = $id");
  1012. }
  1013.  
  1014. /**
  1015. * Retrieves all WP posts for a certain campaign
  1016. *
  1017. * @param integer $id Campaign id
  1018. */
  1019. function getCampaignPosts($id)
  1020. {
  1021. global $wpdb;
  1022. return $wpdb->get_results("SELECT post_id FROM {$this->db['campaign_post']} WHERE campaign_id = $id ");
  1023. }
  1024.  
  1025. /**
  1026. * Adds a feed by url and campaign id
  1027. *
  1028. *
  1029. */
  1030. function addCampaignFeed($id, $feed)
  1031. {
  1032. global $wpdb;
  1033.  
  1034. $simplepie = $this->fetchFeed($feed, true);
  1035. $url = $wpdb->escape($simplepie->subscribe_url());
  1036.  
  1037. // If it already exists, ignore it
  1038. if(! $wpdb->get_var("SELECT id FROM {$this->db['campaign_feed']} WHERE campaign_id = $id AND url = '$url' "))
  1039. {
  1040. $wpdb->query(WPOTools::insertQuery($this->db['campaign_feed'],
  1041. array('url' => $url,
  1042. 'title' => $wpdb->escape($simplepie->get_title()),
  1043. 'description' => $wpdb->escape($simplepie->get_description()),
  1044. 'logo' => $wpdb->escape($simplepie->get_image_url()),
  1045. 'campaign_id' => $id)
  1046. ));
  1047.  
  1048. return $wpdb->insert_id;
  1049. }
  1050.  
  1051. return false;
  1052. }
  1053.  
  1054.  
  1055. /**
  1056. * Retrieves feeds from database
  1057. *
  1058. * @param mixed $args
  1059. */
  1060. function getFeeds($args = '')
  1061. {
  1062. global $wpdb;
  1063. extract(WPOTools::getQueryArgs($args, array('fields' => '*',
  1064. 'campid' => '',
  1065. 'join' => false,
  1066. 'orderby' => 'created_on',
  1067. 'ordertype' => 'DESC',
  1068. 'limit' => null)));
  1069.  
  1070. $sql = "SELECT $fields FROM {$this->db['campaign_feed']} cf ";
  1071.  
  1072. if(!empty($join))
  1073. $sql .= "INNER JOIN {$this->db['campaign']} camp ON camp.id = cf.campaign_id ";
  1074.  
  1075. if(!empty($campid))
  1076. $sql .= "WHERE cf.campaign_id = $campid";
  1077.  
  1078. return $wpdb->get_results($sql);
  1079. }
  1080.  
  1081. /**
  1082. * Returns how many seconds left till reprocessing
  1083. *
  1084. * @return seconds
  1085. **/
  1086. function getCampaignRemaining(&$campaign, $gmt = 0)
  1087. {
  1088. return mysql2date('U', $campaign->lastactive) + $campaign->frequency - current_time('timestamp', true) + ($gmt ? 0 : (get_option('gmt_offset') * 3600));
  1089. }
  1090.  
  1091. /**
  1092. * Called when WP-o-Matic admin pages initialize.
  1093. *
  1094. *
  1095. */
  1096. function adminInit()
  1097. {
  1098. auth_redirect();
  1099.  
  1100. // force display of a certain section
  1101. $this->section = ($this->setup) ? ((isset($_REQUEST['s']) && $_REQUEST['s']) ? $_REQUEST['s'] : $this->sections[0]) : 'setup';
  1102.  
  1103. if (isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false))
  1104. die('Please switch to Firefox / Safari');
  1105.  
  1106. wp_enqueue_script('prototype');
  1107. wp_enqueue_script('wpoadmin', $this->tplpath . '/admin.js', array('prototype'), $this->version);
  1108.  
  1109. if($this->section == 'list')
  1110. wp_enqueue_script('listman');
  1111.  
  1112. if(WPOTools::isAjax())
  1113. {
  1114. $this->admin();
  1115. exit;
  1116. }
  1117. }
  1118.  
  1119. /**
  1120. * Called by admin-header.php
  1121. *
  1122. * @return void
  1123. **/
  1124. function adminHead()
  1125. {
  1126. $this->admin = true;
  1127. }
  1128.  
  1129. /**
  1130. * Shows a warning box for setup
  1131. *
  1132. *
  1133. */
  1134. function adminWarning()
  1135. {
  1136. if(! $this->setup && $this->section != 'setup')
  1137. {
  1138. echo "<div id='wpo-warning' class='updated fade-ff0000'><p>" . sprintf(__('Please <a href="%s">click here</a> to setup and configure WP-o-Matic.', 'wpomatic'), $this->adminurl . '&amp;s=setup') . "</p></div>
  1139.  
  1140. <style type='text/css'>
  1141. " . ($this->newadmin
  1142. ?
  1143. "
  1144. #wpo-warning { position: absolute; top: 4em; right: 0 }
  1145. "
  1146. :
  1147. "
  1148. #adminmenu { margin-bottom: 5em; }
  1149. #wpo-warning { position: absolute; top: 6.8em; }
  1150. "
  1151. )
  1152. .
  1153. "
  1154. </style>
  1155. ";
  1156. }
  1157. }
  1158.  
  1159. /**
  1160. * Executes the current section method.
  1161. *
  1162. *
  1163. */
  1164. function admin()
  1165. {
  1166. if(in_array($this->section, $this->sections))
  1167. {
  1168. $method = 'admin' . ucfirst($this->section);
  1169. $this->$method();
  1170. }
  1171. }
  1172.  
  1173. /**
  1174. * Adds the WP-o-Matic item to menu
  1175. *
  1176. *
  1177. */
  1178. function adminMenu()
  1179. {
  1180. add_submenu_page('options-general.php', 'WP-o-Matic', 'WP-o-Matic', 10, basename(__FILE__), array(&$this, 'admin'));
  1181. }
  1182.  
  1183. /**
  1184. * Outputs the admin header in a template
  1185. *
  1186. *
  1187. */
  1188. function adminHeader()
  1189. {
  1190. $current = array();
  1191.  
  1192. foreach($this->sections as $s)
  1193. $current[$s] = ($s == $this->section) ? 'class="current"' : '';
  1194.  
  1195. include(WPOTPL . 'header.php');
  1196. }
  1197.  
  1198. /**
  1199. * Outputs the admin footer in a template
  1200. *
  1201. *
  1202. */
  1203. function adminFooter()
  1204. {
  1205. include(WPOTPL . 'footer.php');
  1206. }
  1207.  
  1208. /**
  1209. * Home section
  1210. *
  1211. *
  1212. */
  1213. function adminHome()
  1214. {
  1215. $logging = get_option('wpo_log');
  1216. $logs = $this->getLogs('limit=7');
  1217. $nextcampaigns = $this->getCampaigns('fields=id,title,lastactive,frequency&limit=5' .
  1218. '&where=active=1&orderby=UNIX_TIMESTAMP(lastactive)%2Bfrequency&ordertype=ASC');
  1219. $lastcampaigns = $this->getCampaigns('fields=id,title,lastactive,frequency&limit=5&where=UNIX_TIMESTAMP(lastactive)>0&orderby=lastactive');
  1220. $campaigns = $this->getCampaigns('fields=id,title,count&limit=5&orderby=count');
  1221.  
  1222. include(WPOTPL . 'home.php');
  1223. }
  1224.  
  1225.  
  1226. /**
  1227. * Setup admin
  1228. *
  1229. *
  1230. */
  1231. function adminSetup()
  1232. {
  1233. if(isset($_POST['dosetup']))
  1234. {
  1235. update_option('wpo_unixcron', isset($_REQUEST['option_unixcron']));
  1236. update_option('wpo_setup', 1);
  1237.  
  1238. $this->adminHome();
  1239. exit;
  1240. }
  1241.  
  1242. # Commands
  1243. $prefix = $this->getCommand();
  1244. $nocommand = ! file_exists($prefix);
  1245.  
  1246. $safe_mode = ini_get('safe_mode');
  1247.  
  1248. $command = $this->cron_command;
  1249. $url = $this->cron_url;
  1250.  
  1251. include(WPOTPL . 'setup.php');
  1252. }
  1253.  
  1254. /**
  1255. * List campaigns section
  1256. *
  1257. *
  1258. */
  1259. function adminList()
  1260. {
  1261. global $wpdb;
  1262.  
  1263. if(isset($_REQUEST['q']))
  1264. {
  1265. $q = $_REQUEST['q'];
  1266. $campaigns = $this->getCampaigns('search=' . $q);
  1267. } else
  1268. $campaigns = $this->getCampaigns('orderby=CREATED_ON');
  1269.  
  1270. include(WPOTPL . 'list.php');
  1271. }
  1272.  
  1273. /**
  1274. * Add campaign section
  1275. *
  1276. *
  1277. */
  1278. function adminAdd()
  1279. {
  1280. $data = $this->campaign_structure;
  1281.  
  1282. if(isset($_REQUEST['campaign_add']))
  1283. {
  1284. check_admin_referer('wpomatic-edit-campaign');
  1285.  
  1286. if($this->errno)
  1287. $data = $this->campaign_data;
  1288. else
  1289. $addedid = $this->adminProcessAdd();
  1290. }
  1291.  
  1292. $author_usernames = $this->getBlogUsernames();
  1293. $campaign_add = true;
  1294. include(WPOTPL . 'edit.php');
  1295. }
  1296.  
  1297. /**
  1298. * Edit campaign section
  1299. *
  1300. *
  1301. */
  1302. function adminEdit()
  1303. {
  1304. $id = intval($_REQUEST['id']);
  1305. if(!$id) die("Can't be called directly");
  1306.  
  1307. if(isset($_REQUEST['campaign_edit']))
  1308. {
  1309. check_admin_referer('wpomatic-edit-campaign');
  1310.  
  1311. $data = $this->campaign_data;
  1312. $submitted = true;
  1313.  
  1314. if(! $this->errno)
  1315. {
  1316. $this->adminProcessEdit($id);
  1317. $edited = true;
  1318. $data = $this->getCampaignData($id);
  1319. }
  1320. } else
  1321. $data = $this->getCampaignData($id);
  1322.  
  1323. $author_usernames = $this->getBlogUsernames();
  1324. $campaign_edit = true;
  1325.  
  1326. include(WPOTPL . 'edit.php');
  1327. }
  1328.  
  1329. function adminEditCategories(&$data, $parent = 0, $level = 0, $categories = 0)
  1330. {
  1331. if ( !$categories )
  1332. $categories = get_categories(array('hide_empty' => 0));
  1333.  
  1334. if(function_exists('_get_category_hierarchy'))
  1335. $children = _get_category_hierarchy();
  1336. elseif(function_exists('_get_term_hierarchy'))
  1337. $children = _get_term_hierarchy('category');
  1338. else
  1339. $children = array();
  1340.  
  1341. if ( $categories ) {
  1342. ob_start();
  1343. foreach ( $categories as $category ) {
  1344. if ( $category->parent == $parent) {
  1345. echo "\t" . _wpo_edit_cat_row($category, $level, $data);
  1346. if ( isset($children[$category->term_id]) )
  1347. $this->adminEditCategories($data, $category->term_id, $level + 1, $categories );
  1348. }
  1349. }
  1350. $output = ob_get_contents();
  1351. ob_end_clean();
  1352.  
  1353. echo $output;
  1354. } else {
  1355. return false;
  1356. }
  1357.  
  1358. }
  1359.  
  1360. /**
  1361. * Resets a campaign (sets post count to 0, forgets last parsed post)
  1362. *
  1363. *
  1364. * @todo Make it ajax-compatible here and add javascript code
  1365. */
  1366. function adminReset()
  1367. {
  1368. global $wpdb;
  1369.  
  1370. $id = intval($_REQUEST['id']);
  1371.  
  1372. if(! defined('DOING_AJAX'))
  1373. check_admin_referer('reset-campaign_'.$id);
  1374.  
  1375. // Reset count and lasactive
  1376. $wpdb->query(WPOTools::updateQuery($this->db['campaign'], array(
  1377. 'count' => 0,
  1378. 'lastactive' => 0
  1379. ), "id = $id"));
  1380.  
  1381. // Reset feeds hashes, count, and lasactive
  1382. foreach($this->getCampaignFeeds($id) as $feed)
  1383. {
  1384. $wpdb->query(WPOTools::updateQuery($this->db['campaign_feed'], array(
  1385. 'count' => 0,
  1386. 'lastactive' => 0,
  1387. 'hash' => ''
  1388. ), "id = {$feed->id}"));
  1389. }
  1390.  
  1391. if(defined('DOING_AJAX'))
  1392. die('1');
  1393. else
  1394. $this->adminList();
  1395. }
  1396.  
  1397. /**
  1398. * Deletes a campaign
  1399. *
  1400. *
  1401. */
  1402. function adminDelete()
  1403. {
  1404. global $wpdb;
  1405.  
  1406. $id = intval($_REQUEST['id']);
  1407.  
  1408. // If not called through admin-ajax.php
  1409. if(! defined('DOING_AJAX'))
  1410. check_admin_referer('delete-campaign_'.$id);
  1411.  
  1412. $wpdb->query("DELETE FROM {$this->db['campaign']} WHERE id = $id");
  1413. $wpdb->query("DELETE FROM {$this->db['campaign_feed']} WHERE campaign_id = $id");
  1414. $wpdb->query("DELETE FROM {$this->db['campaign_word']} WHERE campaign_id = $id");
  1415. $wpdb->query("DELETE FROM {$this->db['campaign_category']} WHERE campaign_id = $id");
  1416.  
  1417. if(defined('DOING_AJAX'))
  1418. die('1');
  1419. else
  1420. $this->adminList();
  1421. }
  1422.  
  1423. /**
  1424. * Options section
  1425. *
  1426. *
  1427. */
  1428. function adminOptions()
  1429. {
  1430.  
  1431. if(isset($_REQUEST['update']))
  1432. {
  1433. update_option('wpo_unixcron', isset($_REQUEST['option_unixcron']));
  1434. update_option('wpo_log', isset($_REQUEST['option_logging']));
  1435. update_option('wpo_log_stdout', isset($_REQUEST['option_logging_stdout']));
  1436. update_option('wpo_cacheimages', isset($_REQUEST['option_caching']));
  1437. update_option('wpo_cachepath', rtrim($_REQUEST['option_cachepath'], '/'));
  1438.  
  1439. $updated = 1;
  1440. }
  1441.  
  1442. if(!is_writable($this->cachepath))
  1443. $not_writable = true;
  1444.  
  1445. include(WPOTPL . 'options.php');
  1446. }
  1447.  
  1448. /**
  1449. * Import section
  1450. *
  1451. *
  1452. */
  1453. function adminImport()
  1454. {
  1455. global $wpdb;
  1456.  
  1457. @session_start();
  1458.  
  1459. if(!$_POST) unset($_SESSION['opmlimport']);
  1460.  
  1461. if(isset($_FILES['importfile']) || $_POST)
  1462. check_admin_referer('import-campaign');
  1463.  
  1464. if(!isset($_SESSION['opmlimport']))
  1465. {
  1466. if(isset($_FILES['importfile']))
  1467. {
  1468. if(is_uploaded_file($_FILES['importfile']['tmp_name']))
  1469. $file = $_FILES['importfile']['tmp_name'];
  1470. else
  1471. $file = false;
  1472. } else if(isset($_REQUEST['importurl']))
  1473. {
  1474. $fromurl = true;
  1475. $file = $_REQUEST['importurl'];
  1476. }
  1477. }
  1478.  
  1479. if(isset($file) || ($_POST && isset($_SESSION['opmlimport'])) )
  1480. {
  1481. require_once( WPOINC . 'xmlparser.class.php' );
  1482.  
  1483. $contents = (isset($file) ? @file_get_contents($file) : $_SESSION['opmlimport']);
  1484. $_SESSION['opmlimport'] = $contents;
  1485.  
  1486. # Get OPML data
  1487. $opml = new XMLParser($contents);
  1488. $opml = $opml->getTree();
  1489.  
  1490. # Check that it is indeed opml
  1491. if(is_array($opml) && isset($opml['OPML']))
  1492. {
  1493. $opml = $opml['OPML'][0];
  1494.  
  1495. $title = isset($opml['HEAD'][0]['TITLE'][0]['VALUE'])
  1496. ? $opml['HEAD'][0]['TITLE'][0]['VALUE']
  1497. : null;
  1498.  
  1499. $opml = $opml['BODY'][0];
  1500.  
  1501. $success = 1;
  1502.  
  1503. # Campaigns dropdown
  1504. $campaigns = array();
  1505. foreach($this->getCampaigns() as $campaign)
  1506. $campaigns[$campaign->id] = $campaign->title;
  1507. }
  1508. else
  1509. $import_error = 1;
  1510. }
  1511.  
  1512. $this->adminImportProcess();
  1513.  
  1514. include(WPOTPL . 'import.php');
  1515. }
  1516.  
  1517. /**
  1518. * Import process
  1519. *
  1520. *
  1521. */
  1522. function adminImportProcess()
  1523. {
  1524. global $wpdb;
  1525.  
  1526. if(isset($_REQUEST['add']))
  1527. {
  1528. if(!isset($_REQUEST['feed']))
  1529. $add_error = __('You must select at least one feed', 'wpomatic');
  1530. else
  1531. {
  1532. switch($_REQUEST['import_mode'])
  1533. {
  1534. // Several campaigns
  1535. case '1':
  1536. $created_campaigns = array();
  1537.  
  1538. foreach($_REQUEST['feed'] as $campaignid => $feeds)
  1539. {
  1540. if(!in_array($campaignid, $created_campaigns))
  1541. {
  1542. // Create campaign
  1543. $title = $_REQUEST['campaign'][$campaignid];
  1544. if(!$title) continue;
  1545.  
  1546. $slug = WPOTools::stripText($title);
  1547. $wpdb->query("INSERT INTO {$this->db['campaign']} (title, active, slug, lastactive, count) VALUES ('$title', 0, '$slug', 0, 0) ");
  1548. $created_campaigns[] = $wpdb->insert_id;
  1549.  
  1550. // Add feeds
  1551. foreach($feeds as $feedurl => $yes)
  1552. $this->addCampaignFeed($campaignid, urldecode($feedurl));
  1553.  
  1554. }
  1555. }
  1556.  
  1557. $this->add_success = __('Campaigns added successfully. Feel free to edit them', 'wpomatic');
  1558.  
  1559. break;
  1560.  
  1561. // All feeds into an existing campaign
  1562. case '2':
  1563. $campaignid = $_REQUEST['import_custom_campaign'];
  1564.  
  1565. foreach($_REQUEST['feed'] as $cid => $feeds)
  1566. {
  1567. // Add feeds
  1568. foreach($feeds as $feedurl => $yes)
  1569. $this->addCampaignFeed($campaignid, urldecode($feedurl));
  1570. }
  1571.  
  1572. $this->add_success = sprintf(__('Feeds added successfully. <a href="%s">Edit campaign</a>', 'wpomatic'), $this->adminurl . '&s=edit&id=' . $campaignid);
  1573.  
  1574. break;
  1575.  
  1576. // All feeds into new campaign
  1577. case '3':
  1578. $title = $_REQUEST['import_new_campaign'];
  1579. $slug = WPOTools::stripText($title);
  1580. $wpdb->query("INSERT INTO {$this->db['campaign']} (title, active, slug, lastactive, count) VALUES ('$title', 0, '$slug', 0, 0) ");
  1581. $campaignid = $wpdb->insert_id;
  1582.  
  1583. // Add feeds
  1584. foreach($_REQUEST['feed'] as $cid => $feeds)
  1585. {
  1586. // Add feeds
  1587. foreach($feeds as $feedurl => $yes)
  1588. $this->addCampaignFeed($campaignid, urldecode($feedurl));
  1589. }
  1590.  
  1591. $this->add_success = sprintf(__('Feeds added successfully. <a href="%s">Edit campaign</a>', 'wpomatic'), $this->adminurl . '&s=edit&id=' . $campaignid);
  1592.  
  1593. break;
  1594. }
  1595. }
  1596. }
  1597. }
  1598.  
  1599. /**
  1600. * Export
  1601. *
  1602. *
  1603. */
  1604. function adminExport()
  1605. {
  1606. if(isset($this->export_error))
  1607. $error = $this->export_error;
  1608.  
  1609. $campaigns = $this->getCampaigns();
  1610.  
  1611. include(WPOTPL . 'export.php');
  1612. }
  1613.  
  1614. /**
  1615. * Export process
  1616. *
  1617. *
  1618. */
  1619. function adminExportProcess()
  1620. {
  1621. if($_POST)
  1622. {
  1623. if(!isset($_REQUEST['export_campaign']))
  1624. {
  1625. $this->export_error = __('Please select at least one campaign', 'wpomatic');
  1626. } else
  1627. {
  1628. $campaigns = array();
  1629. foreach($_REQUEST['export_campaign'] as $cid)
  1630. {
  1631. $campaign = $this->getCampaignById($cid);
  1632. $campaign->feeds = (array) $this->getCampaignFeeds($cid);
  1633. $campaigns[] = $campaign;
  1634. }
  1635.  
  1636. header("Content-type: text/x-opml");
  1637. header('Content-Disposition: attachment; filename="wpomatic.opml"');
  1638.  
  1639. include(WPOTPL . 'export.opml.php');
  1640. exit;
  1641. }
  1642. }
  1643. }
  1644.  
  1645. /**
  1646. * Tests a feed
  1647. *
  1648. *
  1649. */
  1650. function adminTestfeed()
  1651. {
  1652. if(!isset($_REQUEST['url'])) return false;
  1653.  
  1654. $url = $_REQUEST['url'];
  1655. $feed = $this->fetchFeed($url, true);
  1656. $works = ! $feed->error(); // if no error returned
  1657.  
  1658. if(defined('DOING_AJAX')){
  1659. echo intval($works);
  1660. die();
  1661. } else
  1662. include(WPOTPL . 'testfeed.php');
  1663. }
  1664.  
  1665. /**
  1666. * Forcedfully processes a campaign
  1667. *
  1668. *
  1669. */
  1670. function adminForcefetch()
  1671. {
  1672. $cid = intval($_REQUEST['id']);
  1673.  
  1674. if(! defined('DOING_AJAX'))
  1675. check_admin_referer('forcefetch-campaign_'.$cid);
  1676.  
  1677. $this->forcefetched = $this->processCampaign($cid);
  1678.  
  1679. if(defined('DOING_AJAX'))
  1680. die('1');
  1681. else
  1682. $this->adminList();
  1683. }
  1684.  
  1685. /**
  1686. * Checks submitted campaign edit form for errors
  1687. *
  1688. *
  1689. * @return array errors
  1690. */
  1691. function adminCampaignRequest()
  1692. {
  1693. global $wpdb;
  1694.  
  1695. # Main data
  1696. $this->campaign_data = $this->campaign_structure;
  1697. $this->campaign_data['main'] = array(
  1698. 'title' => $_REQUEST['campaign_title'],
  1699. 'active' => isset($_REQUEST['campaign_active']),
  1700. 'slug' => $_REQUEST['campaign_slug'],
  1701. 'template' => (isset($_REQUEST['campaign_templatechk']))
  1702. ? $_REQUEST['campaign_template'] : null,
  1703. 'frequency' => intval($_REQUEST['campaign_frequency_d']) * 86400
  1704. + intval($_REQUEST['campaign_frequency_h']) * 3600
  1705. + intval($_REQUEST['campaign_frequency_m']) * 60,
  1706. 'cacheimages' => (int) isset($_REQUEST['campaign_cacheimages']),
  1707. 'feeddate' => (int) isset($_REQUEST['campaign_feeddate']),
  1708. 'posttype' => $_REQUEST['campaign_posttype'],
  1709. 'author' => sanitize_user($_REQUEST['campaign_author']),
  1710. 'comment_status' => $_REQUEST['campaign_commentstatus'],
  1711. 'allowpings' => (int) isset($_REQUEST['campaign_allowpings']),
  1712. 'dopingbacks' => (int) isset($_REQUEST['campaign_dopingbacks']),
  1713. 'max' => intval($_REQUEST['campaign_max']),
  1714. 'linktosource' => (int) isset($_REQUEST['campaign_linktosource'])
  1715. );
  1716.  
  1717. // New feeds
  1718. foreach($_REQUEST['campaign_feed']['new'] as $i => $feed)
  1719. {
  1720. $feed = trim($feed);
  1721.  
  1722. if(!empty($feed))
  1723. {
  1724. if(!isset($this->campaign_data['feeds']['new']))
  1725. $this->campaign_data['feeds']['new'] = array();
  1726.  
  1727. $this->campaign_data['feeds']['new'][$i] = $feed;
  1728. }
  1729. }
  1730.  
  1731. // Existing feeds to delete
  1732. if(isset($_REQUEST['campaign_feed']['delete']))
  1733. {
  1734. $this->campaign_data['feeds']['delete'] = array();
  1735.  
  1736. foreach($_REQUEST['campaign_feed']['delete'] as $feedid => $yes)
  1737. $this->campaign_data['feeds']['delete'][] = intval($feedid);
  1738. }
  1739.  
  1740. // Existing feeds.
  1741. if(isset($_REQUEST['id']))
  1742. {
  1743. $this->campaign_data['feeds']['edit'] = array();
  1744. foreach($this->getCampaignFeeds(intval($_REQUEST['id'])) as $feed)
  1745. $this->campaign_data['feeds']['edit'][$feed->id] = $feed->url;
  1746. }
  1747.  
  1748. // Categories
  1749. if(isset($_REQUEST['campaign_categories']))
  1750. {
  1751. foreach($_REQUEST['campaign_categories'] as $category)
  1752. {
  1753. $id = intval($category);
  1754. $this->campaign_data['categories'][] = $category;
  1755. }
  1756. }
  1757.  
  1758. # New categories
  1759. if(isset($_REQUEST['campaign_newcat']))
  1760. {
  1761. foreach($_REQUEST['campaign_newcat'] as $k => $on)
  1762. {
  1763. $catname = $_REQUEST['campaign_newcatname'][$k];
  1764. if(!empty($catname))
  1765. {
  1766. if(!isset($this->campaign_data['categories']['new']))
  1767. $this->campaign_data['categories']['new'] = array();
  1768.  
  1769. $this->campaign_data['categories']['new'][] = $catname;
  1770. }
  1771. }
  1772. }
  1773.  
  1774. // Rewrites
  1775. if(isset($_REQUEST['campaign_word_origin']))
  1776. {
  1777. foreach($_REQUEST['campaign_word_origin'] as $id => $origin_data)
  1778. {
  1779. $rewrite = isset($_REQUEST['campaign_word_option_rewrite'])
  1780. && isset($_REQUEST['campaign_word_option_rewrite'][$id]);
  1781. $relink = isset($_REQUEST['campaign_word_option_relink'])
  1782. && isset($_REQUEST['campaign_word_option_relink'][$id]);
  1783.  
  1784. if($rewrite || $relink)
  1785. {
  1786. $rewrite_data = trim($_REQUEST['campaign_word_rewrite'][$id]);
  1787. $relink_data = trim($_REQUEST['campaign_word_relink'][$id]);
  1788.  
  1789. // Relink data field can't be empty
  1790. if(($relink && !empty($relink_data)) || !$relink)
  1791. {
  1792. $regex = isset($_REQUEST['campaign_word_option_regex'])
  1793. && isset($_REQUEST['campaign_word_option_regex'][$id]);
  1794.  
  1795. $data = array();
  1796. $data['origin'] = array('search' => $origin_data, 'regex' => $regex);
  1797.  
  1798. if($rewrite)
  1799. $data['rewrite'] = $rewrite_data;
  1800.  
  1801. if($relink)
  1802. $data['relink'] = $relink_data;
  1803.  
  1804. $this->campaign_data['rewrites'][] = $data;
  1805. }
  1806. }
  1807. }
  1808. }
  1809.  
  1810. $errors = array('basic' => array(), 'feeds' => array(), 'categories' => array(),
  1811. 'rewrite' => array(), 'options' => array());
  1812.  
  1813. # Main
  1814. if(empty($this->campaign_data['main']['title']))
  1815. {
  1816. $errors['basic'][] = __('You have to enter a campaign title', 'wpomatic');
  1817. $this->errno++;
  1818. }
  1819.  
  1820. # Feeds
  1821. $feedscount = 0;
  1822.  
  1823. if(isset($this->campaign_data['feeds']['new'])) $feedscount += count($this->campaign_data['feeds']['new']);
  1824. if(isset($this->campaign_data['feeds']['edit'])) $feedscount += count($this->campaign_data['feeds']['edit']);
  1825. if(isset($this->campaign_data['feeds']['delete'])) $feedscount -= count($this->campaign_data['feeds']['delete']);
  1826.  
  1827. if(!$feedscount)
  1828. {
  1829. $errors['feeds'][] = __('You have to enter at least one feed', 'wpomatic');
  1830. $this->errno++;
  1831. } else {
  1832. if(isset($this->campaign_data['feeds']['new']))
  1833. {
  1834. foreach($this->campaign_data['feeds']['new'] as $feed)
  1835. {
  1836. $simplepie = $this->fetchFeed($feed, true);
  1837. if($simplepie->error())
  1838. {
  1839. $errors['feeds'][] = sprintf(__('Feed <strong>%s</strong> could not be parsed (SimplePie said: %s)', 'wpomatic'), $feed, $simplepie->error());
  1840. $this->errno++;
  1841. }
  1842. }
  1843. }
  1844. }
  1845.  
  1846. # Categories
  1847. if(! sizeof($this->campaign_data['categories']))
  1848. {
  1849. $errors['categories'][] = __('Select at least one category', 'wpomatic');
  1850. $this->errno++;
  1851. }
  1852.  
  1853. # Rewrite
  1854. if(sizeof($this->campaign_data['rewrites']))
  1855. {
  1856. foreach($this->campaign_data['rewrites'] as $rewrite)
  1857. {
  1858. if($rewrite['origin']['regex'])
  1859. {
  1860. if(false === @preg_match($rewrite['origin']['search'], ''))
  1861. {
  1862. $errors['rewrites'][] = __('There\'s an error with the supplied RegEx expression', 'wpomatic');
  1863. $this->errno++;
  1864. }
  1865. }
  1866. }
  1867. }
  1868.  
  1869. # Options
  1870. if(! get_userdatabylogin($this->campaign_data['main']['author']))
  1871. {
  1872. $errors['options'][] = __('Author username not found', 'wpomatic');
  1873. $this->errno++;
  1874. }
  1875.  
  1876. if(! $this->campaign_data['main']['frequency'])
  1877. {
  1878. $errors['options'][] = __('Selected frequency is not valid', 'wpomatic');
  1879. $this->errno++;
  1880. }
  1881.  
  1882. if(! ($this->campaign_data['main']['max'] === 0 || $this->campaign_data['main']['max'] > 0))
  1883. {
  1884. $errors['options'][] = __('Max items should be a valid number (greater than zero)', 'wpomatic');
  1885. $this->errno++;
  1886. }
  1887.  
  1888. if($this->campaign_data['main']['cacheimages'] && !is_writable($this->cachepath))
  1889. {
  1890. $errors['options'][] = sprintf(__('Cache path (in <a href="%s">Options</a>) must be writable before enabling image caching.', 'wpomatic'), $this->adminurl . '&s=options' );
  1891. $this->errno++;
  1892. }
  1893.  
  1894. $this->errors = $errors;
  1895. }
  1896.  
  1897. /**
  1898. * Creates a campaign, and runs processEdit. If processEdit fails, campaign is removed
  1899. *
  1900. * @return campaign id if created successfully, errors if not
  1901. */
  1902. function adminProcessAdd()
  1903. {
  1904. global $wpdb;
  1905.  
  1906. // Insert a campaign with dumb data
  1907. $wpdb->query(WPOTools::insertQuery($this->db['campaign'], array('lastactive' => 0, 'count' => 0)));
  1908. $cid = $wpdb->insert_id;
  1909.  
  1910. // Process the edit
  1911. $this->campaign_data['main']['lastactive'] = 0;
  1912. $this->adminProcessEdit($cid);
  1913. return $cid;
  1914. }
  1915.  
  1916. /**
  1917. * Cleans everything for the given id, then redoes everything
  1918. *
  1919. * @param integer $id The id to edit
  1920. */
  1921. function adminProcessEdit($id)
  1922. {
  1923. global $wpdb;
  1924.  
  1925. // If we need to execute a tool action we stop here
  1926. if($this->adminProcessTools()) return;
  1927.  
  1928. // Delete all to recreate
  1929. $wpdb->query("DELETE FROM {$this->db['campaign_word']} WHERE campaign_id = $id");
  1930. $wpdb->query("DELETE FROM {$this->db['campaign_category']} WHERE campaign_id = $id");
  1931.  
  1932. // Process categories
  1933. # New
  1934. if(isset($this->campaign_data['categories']['new']))
  1935. {
  1936. foreach($this->campaign_data['categories']['new'] as $category)
  1937. $this->campaign_data['categories'][] = wp_insert_category(array('cat_name' => $category));
  1938.  
  1939. unset($this->campaign_data['categories']['new']);
  1940. }
  1941.  
  1942. # All
  1943. foreach($this->campaign_data['categories'] as $category)
  1944. {
  1945. // Insert
  1946. $wpdb->query(WPOTools::insertQuery($this->db['campaign_category'],
  1947. array('category_id' => $category,
  1948. 'campaign_id' => $id)
  1949. ));
  1950. }
  1951.  
  1952. // Process feeds
  1953. # New
  1954. if(isset($this->campaign_data['feeds']['new']))
  1955. {
  1956. foreach($this->campaign_data['feeds']['new'] as $feed)
  1957. $this->addCampaignFeed($id, $feed);
  1958. }
  1959.  
  1960. # Delete
  1961. if(isset($this->campaign_data['feeds']['delete']))
  1962. {
  1963. foreach($this->campaign_data['feeds']['delete'] as $feed)
  1964. $wpdb->query("DELETE FROM {$this->db['campaign_feed']} WHERE id = $feed ");
  1965. }
  1966.  
  1967. // Process words
  1968. foreach($this->campaign_data['rewrites'] as $rewrite)
  1969. {
  1970. $wpdb->query(WPOTools::insertQuery($this->db['campaign_word'],
  1971. array('word' => $rewrite['origin']['search'],
  1972. 'regex' => $rewrite['origin']['regex'],
  1973. 'rewrite' => isset($rewrite['rewrite']),
  1974. 'rewrite_to' => isset($rewrite['rewrite']) ? $rewrite['rewrite'] : '',
  1975. 'relink' => isset($rewrite['relink']) ? $rewrite['relink'] : null,
  1976. 'campaign_id' => $id)
  1977. ));
  1978. }
  1979.  
  1980. // Main
  1981. $main = $this->campaign_data['main'];
  1982.  
  1983. // Fetch author id
  1984. $author = get_userdatabylogin($this->campaign_data['main']['author']);
  1985. $main['authorid'] = $author->ID;
  1986. unset($main['author']);
  1987.  
  1988. // Query
  1989. $query = WPOTools::updateQuery($this->db['campaign'], $main, 'id = ' . intval($id));
  1990. $wpdb->query($query);
  1991. }
  1992.  
  1993. /**
  1994. * Processes edit campaign tools actions
  1995. *
  1996. *
  1997. */
  1998. function adminProcessTools()
  1999. {
  2000. global $wpdb;
  2001.  
  2002. $id = intval($_REQUEST['id']);
  2003.  
  2004. if(isset($_REQUEST['tool_removeall']))
  2005. {
  2006. $posts = $this->getCampaignPosts($id);
  2007.  
  2008. foreach($posts as $post)
  2009. {
  2010. $wpdb->query("DELETE FROM {$wpdb->posts} WHERE ID = {$post->post_id} ");
  2011. }
  2012.  
  2013. // Delete log
  2014. $wpdb->query("DELETE FROM {$this->db['campaign_post']} WHERE campaign_id = {$id} ");
  2015.  
  2016. // Update feed and campaign posts count
  2017. $wpdb->query(WPOTools::updateQuery($this->db['campaign'], array('count' => 0), "id = {$id}"));
  2018. $wpdb->query(WPOTools::updateQuery($this->db['campaign_feed'], array('hash' => 0, 'count' => 0), "campaign_id = {$id}"));
  2019.  
  2020. $this->tool_success = __('All posts removed', 'wpomatic');
  2021. return true;
  2022. }
  2023.  
  2024. if(isset($_REQUEST['tool_changetype']))
  2025. {
  2026. $this->adminUpdateCampaignPosts($id, array(
  2027. 'post_status' => $wpdb->escape($_REQUEST['campaign_tool_changetype'])
  2028. ));
  2029.  
  2030. $this->tool_success = __('Posts status updated', 'wpomatic');
  2031. return true;
  2032. }
  2033.  
  2034. if(isset($_REQUEST['tool_changeauthor']))
  2035. {
  2036. $author = get_userdatabylogin($_REQUEST['campaign_tool_changeauthor']);
  2037.  
  2038. if($author)
  2039. {
  2040. $authorid = $author->ID;
  2041. $this->adminUpdateCampaignPosts($id, array('post_author' => $authorid));
  2042. } else {
  2043. $this->errno = 1;
  2044. $this->errors = array('tools' => array(sprintf(__('Author %s not found', 'wpomatic'), attribute_escape($_REQUEST['campaign_tool_changeauthor']))));
  2045. }
  2046.  
  2047. $this->tool_success = __('Posts status updated', 'wpomatic');
  2048. return true;
  2049. }
  2050.  
  2051. return false;
  2052. }
  2053.  
  2054. function adminUpdateCampaignPosts($id, $properties)
  2055. {
  2056. global $wpdb;
  2057.  
  2058. $posts = $this->getCampaignPosts($id);
  2059.  
  2060. foreach($posts as $post)
  2061. $wpdb->query(WPOTools::updateQuery($wpdb->posts, $properties, "ID = {$post->id}"));
  2062. }
  2063.  
  2064.  
  2065. /**
  2066. * Show logs
  2067. *
  2068. *
  2069. */
  2070. function adminLogs()
  2071. {
  2072. global $wpdb;
  2073.  
  2074. // Clean logs?
  2075. if(isset($_REQUEST['clean_logs']))
  2076. {
  2077. check_admin_referer('clean-logs');
  2078. $wpdb->query("DELETE FROM {$this->db['log']} WHERE 1=1 ");
  2079. }
  2080.  
  2081. // Logs to show per page
  2082. $logs_per_page = 20;
  2083.  
  2084. $page = isset($_REQUEST['p']) ? intval($_REQUEST['p']) : 0;
  2085. $total = $wpdb->get_var("SELECT COUNT(*) as cnt FROM {$this->db['log']} ");
  2086. $logs = $this->getLogs("page={$page}&perpage={$logs_per_page}");
  2087.  
  2088. $paging = paginate_links(array(
  2089. 'base' => $this->adminurl . '&s=logs&%_%',
  2090. 'format' => 'p=%#%',
  2091. 'total' => ceil($total / $logs_per_page),
  2092. 'current' => $page,
  2093. 'end_size' => 3
  2094. ));
  2095.  
  2096. include(WPOTPL . 'logs.php');
  2097. }
  2098. }
  2099.  
  2100. $wpomatic = & new WPOMatic();