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");?>
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;
}
}
}