460 lines
13 KiB
PHP
460 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* Things: GP_Project class
|
|
*
|
|
* @package GlotPress
|
|
* @subpackage Things
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
/**
|
|
* Core class used to implement the projects.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class GP_Project extends GP_Thing {
|
|
|
|
var $table_basename = 'gp_projects';
|
|
var $field_names = array( 'id', 'name', 'slug', 'path', 'description', 'parent_project_id', 'source_url_template', 'active' );
|
|
var $int_fields = array( 'id', 'parent_project_id', 'active' );
|
|
var $non_updatable_attributes = array( 'id' );
|
|
|
|
public $id;
|
|
public $name;
|
|
public $slug;
|
|
public $path;
|
|
public $description;
|
|
public $parent_project_id;
|
|
public $source_url_template;
|
|
public $active;
|
|
public $user_source_url_template;
|
|
|
|
/**
|
|
* Sets restriction rules for fields.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param GP_Validation_Rules $rules The validation rules instance.
|
|
*/
|
|
public function restrict_fields( $rules ) {
|
|
$rules->name_should_not_be( 'empty' );
|
|
$rules->slug_should_not_be( 'empty' );
|
|
}
|
|
|
|
// Additional queries
|
|
|
|
public function by_path( $path ) {
|
|
/**
|
|
* Filters the prefix for the locale glossary path.
|
|
*
|
|
* @since 2.3.1
|
|
*
|
|
* @param string $$locale_glossary_path_prefix Prefix for the locale glossary path.
|
|
*/
|
|
$locale_glossary_path_prefix = apply_filters( 'gp_locale_glossary_path_prefix', '/languages' );
|
|
|
|
if ( $locale_glossary_path_prefix === $path ) {
|
|
return GP::$glossary->get_locale_glossary_project();
|
|
}
|
|
|
|
$path = rawurlencode( urldecode( $path ) );
|
|
$path = str_replace( '%2F', '/', $path );
|
|
$path = str_replace( '%20', ' ', $path );
|
|
$path = trim( $path, '/' );
|
|
|
|
return $this->one( "SELECT * FROM $this->table WHERE path = %s", $path );
|
|
}
|
|
|
|
/**
|
|
* Fetches the project by ID or object.
|
|
*
|
|
* @since 2.3.0
|
|
*
|
|
* @param int|object $thing_or_id A project or the ID.
|
|
* @return GP_Project|false The project on success or false on failure.
|
|
*/
|
|
public function get( $thing_or_id ) {
|
|
if ( is_numeric( $thing_or_id ) && 0 === (int) $thing_or_id ) {
|
|
return GP::$glossary->get_locale_glossary_project();
|
|
}
|
|
|
|
return parent::get( $thing_or_id );
|
|
}
|
|
|
|
/**
|
|
* Retrieves the sub projects
|
|
*
|
|
* @return array Array of GP_Project
|
|
*/
|
|
public function sub_projects() {
|
|
$sub_projects = $this->many( "SELECT * FROM $this->table WHERE parent_project_id = %d ORDER BY active DESC, id ASC", $this->id );
|
|
|
|
/**
|
|
* Filter the list of sub-projects of a project.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param array $sub_projects An array of sub projects as GP_Project.
|
|
* @param string $project_id ID of the current project. Can be zero at the top level.
|
|
*/
|
|
$sub_projects = apply_filters( 'gp_projects', $sub_projects, $this->id );
|
|
|
|
return $sub_projects;
|
|
}
|
|
|
|
public function top_level() {
|
|
$projects = $this->many( "SELECT * FROM $this->table WHERE parent_project_id IS NULL OR parent_project_id < 1 ORDER BY name ASC" );
|
|
|
|
/** This filter is documented in gp-includes/things/project.php */
|
|
$projects = apply_filters( 'gp_projects', $projects, 0 );
|
|
|
|
return $projects;
|
|
}
|
|
|
|
// Triggers
|
|
|
|
/**
|
|
* Executes after creating a project.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function after_create() {
|
|
/**
|
|
* Fires after creating a project.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param GP_Project $project The project that was created.
|
|
*/
|
|
do_action( 'gp_project_created', $this );
|
|
|
|
// TODO: pass some args to pre/after_create?
|
|
if ( is_null( $this->update_path() ) ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Executes after saving a project.
|
|
*
|
|
* @since 1.0.0
|
|
* @since 3.0.0 Added the `$project_before` parameter.
|
|
*
|
|
* @param GP_Project $project_before Project before the update.
|
|
* @return bool
|
|
*/
|
|
public function after_save( $project_before ) {
|
|
/**
|
|
* Fires after saving a project.
|
|
*
|
|
* @since 1.0.0
|
|
* @since 3.0.0 Added the `$project_before` parameter.
|
|
*
|
|
* @param GP_Project $project Project following the update.
|
|
* @param GP_Project $project_before Project before the update.
|
|
*/
|
|
do_action( 'gp_project_saved', $this, $project_before );
|
|
|
|
// TODO: pass the update args to after/pre_save?
|
|
// TODO: only call it if the slug or parent project were changed
|
|
return ! is_null( $this->update_path() );
|
|
}
|
|
|
|
/**
|
|
* Executes after deleting a project.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function after_delete() {
|
|
/**
|
|
* Fires after deleting a project.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param GP_Project $project The project that was deleted.
|
|
*/
|
|
do_action( 'gp_project_deleted', $this );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Normalizes an array with key-value pairs representing
|
|
* a GP_Project object.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param array $args Arguments for a GP_Project object.
|
|
* @return array Normalized arguments for a GP_Project object.
|
|
*/
|
|
public function normalize_fields( $args ) {
|
|
if ( isset( $args['parent_project_id'] ) ) {
|
|
$args['parent_project_id'] = $this->force_false_to_null( $args['parent_project_id'] );
|
|
}
|
|
|
|
if ( isset( $args['slug'] ) && ! $args['slug'] ) {
|
|
$args['slug'] = $args['name'];
|
|
}
|
|
|
|
if ( ! empty( $args['slug'] ) ) {
|
|
$args['slug'] = gp_sanitize_slug( $args['slug'] );
|
|
}
|
|
|
|
if ( ( isset( $args['path'] ) && ! $args['path'] ) || ! isset( $args['path'] ) || is_null( $args['path'] ) ) {
|
|
unset( $args['path'] );
|
|
}
|
|
|
|
if ( isset( $args['active'] ) ) {
|
|
if ( 'on' === $args['active'] ) {
|
|
$args['active'] = 1;
|
|
}
|
|
|
|
if ( ! $args['active'] ) {
|
|
$args['active'] = 0;
|
|
}
|
|
}
|
|
|
|
$args = parent::normalize_fields( $args );
|
|
|
|
return $args;
|
|
}
|
|
|
|
// Helpers
|
|
|
|
/**
|
|
* Updates this project's and its children's paths, according to its current slug.
|
|
*/
|
|
public function update_path() {
|
|
global $wpdb;
|
|
$old_path = isset( $this->path ) ? $this->path : '';
|
|
$parent_project = $this->get( $this->parent_project_id );
|
|
if ( $parent_project ) {
|
|
$path = gp_url_join( $parent_project->path, $this->slug );
|
|
} elseif ( ! $wpdb->last_error ) {
|
|
$path = $this->slug;
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
$path = trim( $path, '/' );
|
|
|
|
$this->path = $path;
|
|
$res_self = $this->update( array( 'path' => $path ) );
|
|
if ( is_null( $res_self ) ) {
|
|
return $res_self;
|
|
}
|
|
// Update children's paths, too.
|
|
if ( $old_path ) {
|
|
$query = "UPDATE $this->table SET path = CONCAT(%s, SUBSTRING(path, %d)) WHERE path LIKE %s";
|
|
return $this->query( $query, $path, strlen( $old_path ) + 1, $wpdb->esc_like( $old_path ) . '%' );
|
|
} else {
|
|
return $res_self;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Regenerate the paths of all projects from its parents slugs
|
|
*/
|
|
public function regenerate_paths( $parent_project_id = null ) {
|
|
// TODO: do it with one query. Use the tree generation code from GP_Route_Main::_options_from_projects()
|
|
if ( $parent_project_id ) {
|
|
$parent_project = $this->get( $parent_project_id );
|
|
$path = $parent_project->path;
|
|
} else {
|
|
$path = '';
|
|
$parent_project_id = null;
|
|
}
|
|
|
|
$projects = $this->find( array( 'parent_project_id' => $parent_project_id ) );
|
|
foreach ( (array) $projects as $project ) {
|
|
$project->update( array( 'path' => trim( gp_url_join( $path, $project->slug ), '/' ) ) );
|
|
$this->regenerate_paths( $project->id );
|
|
}
|
|
}
|
|
|
|
public function source_url( $file, $line ) {
|
|
$source_url = false;
|
|
if ( $source_url_template = $this->source_url_template() ) {
|
|
$source_url = str_replace( array( '%file%', '%line%' ), array( $file, $line ), $source_url_template );
|
|
}
|
|
|
|
/**
|
|
* Allows per-reference overriding of the source URL defined as project setting.
|
|
*
|
|
* @since 2.2.0
|
|
*
|
|
* @param string|false $source_url The originally generated source URL, or false if no URL is available.
|
|
* @param GP_Project $project The current project.
|
|
* @param string $file The referenced file name.
|
|
* @param string $line The line number in the referenced file.
|
|
*/
|
|
return apply_filters( 'gp_reference_source_url', $source_url, $this, $file, $line );
|
|
}
|
|
|
|
public function source_url_template() {
|
|
if ( isset( $this->user_source_url_template ) ) {
|
|
return $this->user_source_url_template;
|
|
} else {
|
|
if ( $this->id && is_user_logged_in() && ( $templates = get_user_meta( get_current_user_id(), 'gp_source_url_templates', true ) )
|
|
&& isset( $templates[ $this->id ] ) ) {
|
|
$this->user_source_url_template = $templates[ $this->id ];
|
|
return $this->user_source_url_template;
|
|
} else {
|
|
return $this->source_url_template;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gives an array of project objects starting from the current project
|
|
* then its parent, its parent and up to the root.
|
|
*
|
|
* @todo Cache the results. Invalidation is tricky, because on each project update we need to invalidate the cache
|
|
* for all of its children.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function path_to_root() {
|
|
$path = array();
|
|
if ( $this->parent_project_id ) {
|
|
$parent_project = $this->get( $this->parent_project_id );
|
|
|
|
if ( $parent_project ) {
|
|
$path = $parent_project->path_to_root();
|
|
}
|
|
}
|
|
return array_merge( array( &$this ), $path );
|
|
}
|
|
|
|
public function set_difference_from( $other_project ) {
|
|
$this_sets = (array) GP::$translation_set->by_project_id( $this->id );
|
|
$other_sets = (array) GP::$translation_set->by_project_id( $other_project->id );
|
|
$added = array();
|
|
$removed = array();
|
|
|
|
foreach ( $other_sets as $other_set ) {
|
|
if ( ! gp_array_any( array( $this, '_compare_set_item' ), $this_sets, $other_set ) ) {
|
|
$added[] = $other_set;
|
|
}
|
|
}
|
|
|
|
foreach ( $this_sets as $this_set ) {
|
|
if ( ! gp_array_any( array( $this, '_compare_set_item' ), $other_sets, $this_set ) ) {
|
|
$removed[] = $this_set;
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'added' => $added,
|
|
'removed' => $removed,
|
|
);
|
|
}
|
|
|
|
public function _compare_set_item( $set, $this_set ) {
|
|
return ( $set->locale == $this_set->locale && $set->slug = $this_set->slug );
|
|
}
|
|
|
|
public function copy_sets_and_translations_from( $source_project_id ) {
|
|
$sets = GP::$translation_set->by_project_id( $source_project_id );
|
|
|
|
foreach ( $sets as $to_add ) {
|
|
$new_set = GP::$translation_set->create(
|
|
array(
|
|
'project_id' => $this->id,
|
|
'name' => $to_add->name,
|
|
'locale' => $to_add->locale,
|
|
'slug' => $to_add->slug,
|
|
)
|
|
);
|
|
if ( ! $new_set ) {
|
|
$this->errors[] = sprintf(
|
|
/* translators: %s: Translation set. */
|
|
__( 'Couldn’t add translation set named %s', 'glotpress' ),
|
|
esc_html( $to_add->name )
|
|
);
|
|
} else {
|
|
// Duplicate translations.
|
|
$new_set->copy_translations_from( $to_add->id );
|
|
}
|
|
}
|
|
}
|
|
|
|
public function copy_originals_from( $source_project_id ) {
|
|
global $wpdb;
|
|
return $this->query(
|
|
"INSERT INTO $wpdb->gp_originals (
|
|
`project_id`, `context`, `singular`, `plural`, `references`, `comment`, `status`, `priority`, `date_added`
|
|
)
|
|
SELECT
|
|
%s AS `project_id`, `context`, `singular`, `plural`, `references`, `comment`, `status`, `priority`, `date_added`
|
|
FROM $wpdb->gp_originals WHERE project_id = %s",
|
|
$this->id,
|
|
$source_project_id
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gives an array of project objects starting from the current project children
|
|
* then its grand children etc.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function inclusive_sub_projects() {
|
|
$sub_projects = $this->sub_projects();
|
|
foreach ( $sub_projects as $sub ) {
|
|
$sub_projects = array_merge( $sub_projects, $sub->inclusive_sub_projects() );
|
|
}
|
|
|
|
return $sub_projects;
|
|
}
|
|
|
|
public function duplicate_project_contents_from( $source_project ) {
|
|
$source_sub_projects = $source_project->inclusive_sub_projects();
|
|
|
|
// Duplicate originals, translations sets and translations for the root project.
|
|
$this->copy_originals_from( $source_project->id );
|
|
$this->copy_sets_and_translations_from( $source_project->id );
|
|
|
|
// Keep a list of parents to preserve hierarchy.
|
|
$parents = array();
|
|
$parents[ $source_project->id ] = $this->id;
|
|
|
|
// Duplicate originals, translations sets and translations for the child projects.
|
|
foreach ( $source_sub_projects as $sub ) {
|
|
$copy_project = new GP_Project( $sub->fields() );
|
|
$copy_project->parent_project_id = $parents[ $sub->parent_project_id ];
|
|
$parent_project = $copy_project->get( $copy_project->parent_project_id );
|
|
|
|
$copy_project->path = gp_url_join( $parent_project->path, $copy_project->slug );
|
|
$copy = GP::$project->create( $copy_project );
|
|
$parents[ $sub->id ] = $copy->id;
|
|
|
|
$copy->copy_originals_from( $sub->id );
|
|
$copy->copy_sets_and_translations_from( $sub->id );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes a project and all of sub projects, translations, translation sets, originals and glossaries.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function delete() {
|
|
GP::$project->delete_many( array( 'parent_project_id' => $this->id ) );
|
|
|
|
GP::$translation_set->delete_many( array( 'project_id' => $this->id ) );
|
|
|
|
GP::$original->delete_many( array( 'project_id' => $this->id ) );
|
|
|
|
return parent::delete();
|
|
}
|
|
}
|
|
GP::$project = new GP_Project();
|