add_help_tab( array( 'id' => 'tp_import_publication_sources_help', 'title' => esc_html__('Sources'), 'content' => '

' . esc_html__('Publication sources') . '

' . esc_html__("Additional publication sources to scan regularly.",'teachpress') . '

', ) ); } /** * Auxiliary function to get source url from dict. */ function tp_get_source_url($source) { return trim($source['src_url']); } /** * The controller for the import page of teachPress * @since 9.0.0 */ function tp_show_publication_sources_page() { if ( isset($_POST['tp_sources_save']) ) { TP_Publication_Sources_Page::sources_actions($_POST); } TP_Publication_Sources_Page::sources_tab(); } /** * This function is the REST call implementation for updating sources. * @since 9.0.0 */ function tp_rest_update_sources() { sleep(2); // insert small delay in case of repeated calls return new WP_REST_Response(TP_Publication_Sources_Page::update_sources()); } /** * This class contains functions for generating the publication sources page. * @since 9.0.0 */ class TP_Publication_Sources_Page { /** * Returns current sources. */ public static function get_current_sources() { global $wpdb; $source_urls = $wpdb->get_results("SELECT * FROM " . TEACHPRESS_MONITORED_SOURCES); $result = array(); foreach ($source_urls as $src_url) { $result[] = array("src_url" => $src_url->name, "last_res" => $src_url->last_res, "update_time" => $src_url->update_time); } return $result; } /** * Returns the table rows for sources rendering */ public static function get_pages_rows($current_pages) { $result = ""; $alternate = true; foreach ($current_pages as $src_url) { $last_res = $src_url['last_res']; if (strlen($last_res) == 0) { $last_res = esc_html__("URL not scanned yet.", "teachpress"); } $result .= sprintf("%s%s%s", $alternate ? "alternate" : "", $src_url['src_url'], esc_html__($last_res, "teachpress"), $src_url['update_time']); $alternate = ! $alternate; } return $result; } /** * Shows the sources * @since 9.0.0 * @access public */ public static function sources_tab () { ?>



zotero://group/<group_id>/, where group_id is the group id (numerical) found on zotero.org.", "teachpress");?>

URL

0; }); $sources_to_monitor = array_map(function ($k) { return trim($k); }, $sources_to_monitor); $new_freq = isset($post['tp_source_freq']) ? trim($post['tp_source_freq']) : 'hourly'; $installed = TP_Publication_Sources_Page::install_sources($sources_to_monitor); // manage cron hook if (count($sources_to_monitor) == 0 || $new_freq == 'never') { TP_Publication_Sources_Page::uninstall_cron(); // not needed anymore } else { TP_Publication_Sources_Page::install_cron($new_freq); // no problem if cron already installed } $new_freq = TP_Publication_Sources_Page::get_update_freq(); get_tp_message( sprintf(esc_html__('Configuration updated with %d URL(s) at frequency "%s".', "teachpress"), count($sources_to_monitor), $new_freq) ); } /** * Finds the current frequency of schedule. * @return Current frequency, or 'never' if none scheduled. * @since 9.0.0 * @access public */ public static function get_update_freq() { $result = wp_get_schedule(TEACHPRESS_CRON_SOURCES_HOOK); if ($result === false) { $result = 'never'; } return $result; } /** * This function installs monitored bibtex sources. Sources present in the db but not * in the sources specified as a parameter are removed from the db. * @global object $wpdb * @param array $sources An array of source URL strings. * @return Only the newly added URLs to monitor - can be the empty array. * @since 9.0.0 * @access public */ public static function install_sources($sources) { // find current sources already installed so as not to install them uselessly $cur_sources = TP_Publication_Sources_Page::get_current_sources(); $cur_source_names = array(); foreach ($cur_sources as $cur_src) { $cur_source_names[] = $cur_src['src_url']; } // start installing sources not present in database global $wpdb; $toremove = array(); foreach ( $cur_source_names as $existing_source ) { if (!in_array($existing_source, $sources)) { $toremove[] = $existing_source; } } // create the filter set for the delete instruction $filter_set = "''"; // empty set if (count($toremove) > 0) { $filter_set = implode(",", array_map(function ($k) { return "'" . esc_sql($k) . "'"; }, $toremove)); } // remove useless entries $wpdb->query( "DELETE FROM " . TEACHPRESS_MONITORED_SOURCES . " WHERE name IN ( " . $filter_set . " )"); // write new entries -- could be done in a single statement $result = array(); foreach( $sources as $element ) { if (! in_array($element, $cur_source_names) ) { $wpdb->insert(TEACHPRESS_MONITORED_SOURCES, array('name' => $element, 'md5' => 0), array('%s', '%d')); $result[] = $element; } } return $result; } /** * This function installs the cron hook. * @param string $freq Frequency of cron. * @since 9.0.0 * @access public */ public static function install_cron($freq) { // install action if not alreay installed if ( ! has_action( TEACHPRESS_CRON_SOURCES_HOOK, 'TP_Publication_Sources_Page::tp_cron_exec' ) ) { add_action( TEACHPRESS_CRON_SOURCES_HOOK, 'TP_Publication_Sources_Page::tp_cron_exec' ); } // schedule hook if freq has changed and freq is not never if ( TP_Publication_Sources_Page::get_update_freq() != $freq && $freq != 'never' ) { TP_Publication_Sources_Page::uninstall_cron(); wp_schedule_event( time(), $freq, TEACHPRESS_CRON_SOURCES_HOOK ); } } /** * This function uninstalls the cron hook. * @since 9.0.0 * @access public */ public static function uninstall_cron() { $timestamp = wp_next_scheduled( TEACHPRESS_CRON_SOURCES_HOOK ); wp_unschedule_event( $timestamp, TEACHPRESS_CRON_SOURCES_HOOK ); } /** * Execute the scheduled sources update. * @since 9.0.0 * @access public */ public static function tp_cron_exec() { TP_Publication_Sources_Page::update_sources(); } /** * Performs update for all sources registered in the db. * @since 9.0.0 */ public static function update_sources() { $result = array(); // list all sources global $wpdb; $source_urls = $wpdb->get_results("SELECT * FROM " . TEACHPRESS_MONITORED_SOURCES); foreach ($source_urls as $src_url) { $result[] = array_merge(TP_Publication_Sources_Page::update_source($src_url->name, $src_url->md5), array('src_id' => $src_url->src_id, 'src_name' => $src_url->name)); } foreach ($result as $cur_res) { $r = $wpdb->update(TEACHPRESS_MONITORED_SOURCES, array('md5' => $cur_res[0], 'last_res' => $cur_res[2], 'update_time' => current_time('mysql', 1)), array('src_id' => $cur_res['src_id'])); } return $result; } /** * Performs update for a single source. * @param $url The URL of the source. URL protocols supported: http://, https://. * @param previous_sig Digest the last time the file was polled, 0 if this is the first time. * @param this_req Http Request (callee assigned), by reference. * @return new_signature, nb_updates, status_message, success * @since 9.0.0 */ public static function update_source_http($url, $previous_sig, &$this_req) { $new_signature = ''; $nb_updates = 0; $status_message = 'Unknown error.'; $success = false; $req = wp_remote_get($url, array('sslverify' => false)); $this_req = $req; if (is_wp_error($req)) { $status_message = 'Error while retrieving URL.'; } else { $code = $req["response"]["code"]; if (!preg_match("#^2\d+$#", $code)) { $status_message = sprintf('Error code %s while connecting to URL %s.', $code, $url); } else { $body = wp_remote_retrieve_body($req); if ($body) { $new_signature = md5($body); if ($new_signature != $previous_sig) { if ( TP_Bibtex::is_utf8($body) === false ) { $body = utf8_encode($body); } if ( !TP_Bibtex::looks_like_bibtex($body) ) { $status_message = "Content does not look like BibTeX."; } else { $settings = array( 'keyword_separator' => ',', 'author_format' => 'dynamic', 'overwrite' => true, 'ignore_tags' => false, ); $entries = TP_Bibtex_Import::init($body, $settings); $status_message = 'Successfully read and imported.'; $nb_updates = count($entries); $success = true; } } else { $status_message = 'File unchanged.'; $new_signature = $previous_sig; $success = true; } } else { $status_message = 'Invalid body in server response.'; } } } return array($new_signature, $nb_updates, $status_message, $success); } /** * Performs update for a single source. * @param $url The URL of the source. URL protocols supported: zotero://group/ is special and downloads all group items in group * @param previous_sig String signature of the last file polled, 0 if this is the first time. * @return new_signature, nb_updates, status_message, success * @since 9.0.0 * @see Zotero api https://www.zotero.org/support/dev/web_api/v3/basics */ public static function update_source_zotero($url, $previous_sig) { $result = array('', 0, 'Zotero group download failed.', false); // be robust if (is_int($previous_sig)) { $previous_sig = strval($previous_sig); } // find group id $parts = explode("/", $url); if (count($parts) >= 3 && $parts[0] == "zotero:" && $parts[2] == "group") { $group_id = $parts[3]; // has the group changed since the last poll? $previous_version = 0; if (is_numeric($previous_sig)) { $previous_version = (int) $previous_sig; } $current_version = 0; $req = wp_remote_get('https://api.zotero.org/groups/' . $group_id . '/items?since=0&limit=1', array('sslverify' => false)); $headers = wp_remote_retrieve_headers($req); if (isset($headers['Last-Modified-Version'])) { $current_version = (int) $headers['Last-Modified-Version']; } $group_has_changed = $current_version > $previous_version || $current_version == 0; // main loop if (!$group_has_changed) { $result = array(strval($current_version), 0, 'Publications already synchronized with Zotero.', true); } else { // prepare pagination loop $has_more_results = true; $error_encountered = false; $current_offset = 0; $page_size = 30; $total_results = -1; while ($has_more_results && !$error_encountered) { // download a single page $page_url = sprintf("https://api.zotero.org/groups/%s/items?since=%d&format=bibtex&limit=%d&start=%d", $group_id, $previous_version, $page_size, $current_offset); $page_result = TP_Publication_Sources_Page::update_source_http($page_url, '', $req); if ($total_results == -1) { // set on first loop $total_results = intval($req["headers"]["total-results"]); } $result[3] = $page_result[3]; $error_encountered = !$result[3]; if ($error_encountered) { $result[2] = 'Zotero group download failed. Error was: ' . $page_result[2]; $result[0] = $previous_version; } else { $result[1] += $page_result[1]; $result[2] = $page_result[2]; usleep(100000); // stay awhile and listen $current_offset += $page_size; $has_more_results = $current_offset < $total_results; } } if (!$error_encountered) { $result[0] = strval($current_version); } } } else { $result = array('', 0, 'Zotero URL format is incorrect.', false); } return $result; } /** * Performs update for a single source. * @param $url The URL of the source. URL protocols supported: http://, https://, zotero:// zotero://group/ is special and downloads all group items in group * @param previous_sig Digest the last time the file was polled, 0 if this is the first time. * @return new_signature, nb_updates, status_message, success * @since 9.0.0 */ public static function update_source($url, $previous_sig) { // what is the protocol? $url_parts = explode("://", strtolower(trim($url))); $result = false; if (count($url_parts) > 1) { switch ($url_parts[0]) { case "http": case "https": $http_req = NULL; $result = TP_Publication_Sources_Page::update_source_http($url, $previous_sig, $http_req); break; case "zotero": $result = TP_Publication_Sources_Page::update_source_zotero($url, $previous_sig); break; default: $result = array('', 0, 'Invalid protocol.', false); break; } } return $result; } /** * Checks the nonce field of the form. If the check fails wp_die() will be executed * @since 9.0.5 */ private static function check_nonce_field () { if ( ! isset( $_POST['tp_nonce'] ) || ! wp_verify_nonce( $_POST['tp_nonce'], 'verify_teachpress_auto_publish' ) ) { wp_die('teachPress error: This request could not be verified!'); exit; } } }