| | |
| | | <?php |
| | | namespace JVBase\rest; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\rest\RateLimiter; |
| | | use DateTime; |
| | | use DateTimeZone; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\managers\OperationQueue; |
| | | use JVBase\managers\CacheManager; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\NotificationManager; |
| | | use JVBase\base\Site; |
| | | use WP_REST_Request; |
| | | use WP_Error; |
| | | use Exception; |
| | |
| | | } |
| | | |
| | | /** |
| | | * @deprecated use Rest.php |
| | | * Handles route registration and high-level coordination |
| | | */ |
| | | abstract class RestRouteManager |
| | |
| | | protected string $route; |
| | | protected string $base; |
| | | protected string $content_type; //the registered post type |
| | | protected string $type; //post, user, term, for MetaManager |
| | | protected string $type; //post, user, term, for Meta |
| | | protected string $action = ''; //optional additional nonce to check |
| | | protected array $callback; //route->callback array |
| | | protected string $operation_type; // from QueueManager.js and OperationQueue.php |
| | | protected OperationQueue $queue; |
| | | protected CacheManager $cache; |
| | | protected Cache $cache; |
| | | protected NotificationManager $notifications; |
| | | protected string $cache_name =''; |
| | | protected int $cache_ttl = 3600; //1 hour default |
| | | |
| | | protected array $response_headers = []; |
| | | |
| | | // Error code constants for consistency |
| | | const ERROR_MISSING_PARAMS = 'missing_parameters'; |
| | |
| | | $this->base = BASE; |
| | | $this->rate_limiter = new RateLimiter(); |
| | | if ($this->cache_name !== '') { |
| | | $this->cache = CacheManager::for($this->cache_name, $this->cache_ttl); |
| | | $this->cache = Cache::for($this->cache_name, $this->cache_ttl); |
| | | } |
| | | add_action('rest_api_init', [$this, 'registerRoutes']); |
| | | } |
| | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Convert MySQL datetime to ISO 8601 timestamp with proper timezone |
| | | */ |
| | | public function formatTimestamp(?string $mysql_datetime): ?string |
| | | { |
| | | if (empty($mysql_datetime)) { |
| | | return null; |
| | | } |
| | | |
| | | try { |
| | | // Get WordPress timezone - dates are stored in this timezone |
| | | $wp_timezone = wp_timezone(); |
| | | |
| | | // Parse the datetime in WordPress timezone |
| | | $date = new DateTime($mysql_datetime, $wp_timezone); |
| | | |
| | | // Convert to UTC for API consistency |
| | | $date->setTimezone(new DateTimeZone('UTC')); |
| | | |
| | | // Return ISO 8601 format |
| | | return $date->format('c'); |
| | | } catch (Exception $e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | protected function checkContent(string $content, bool $bool = false):string|bool |
| | | { |
| | | $result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??''; |
| | |
| | | */ |
| | | protected function checkUser(int $userID): bool |
| | | { |
| | | $cache = CacheManager::for('users'); |
| | | $cache = Cache::for('checkUser', DAY_IN_SECONDS)->connect('user'); |
| | | |
| | | return $cache->remember("user_exists_{$userID}", function() use ($userID) { |
| | | return $cache->remember($userID, function() use ($userID) { |
| | | return (bool)get_userdata($userID); |
| | | }, DAY_IN_SECONDS); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | protected function checkShop(int $shopID): bool |
| | | { |
| | | $cache = CacheManager::for('shop'); |
| | | $cache = Cache::for('checkShop',DAY_IN_SECONDS)->connect('taxonomy'); |
| | | |
| | | return $cache->remember("shop_exists_{$shopID}", function() use ($shopID) { |
| | | return $cache->remember($shopID, function() use ($shopID) { |
| | | return (bool)term_exists($shopID, BASE . 'shop'); |
| | | }, DAY_IN_SECONDS); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | $cache = CacheManager::for($taxonomy); |
| | | $cache = Cache::for('checkTerm', DAY_IN_SECONDS)->connect('taxonomy'); |
| | | |
| | | return $cache->remember("term_exists_{$termID}", function() use ($termID, $taxonomy) { |
| | | return $cache->remember($termID, function() use ($termID, $taxonomy) { |
| | | return (bool)term_exists($termID, $taxonomy); |
| | | }, DAY_IN_SECONDS); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function isVerifiedUser(int $user_id): bool |
| | | { |
| | | $cache = CacheManager::forUser($user_id); |
| | | $cache = Cache::for('verifiedUsers', DAY_IN_SECONDS)->connect('user'); |
| | | |
| | | return $cache->remember('is_verified', function() use ($user_id) { |
| | | return $cache->remember($user_id, function() use ($user_id) { |
| | | return user_can($user_id, 'skip_moderation'); |
| | | }, DAY_IN_SECONDS); |
| | | }); |
| | | } |
| | | |
| | | protected function applyTaxonomyFilters(array $args, array $data):array |
| | | { |
| | | $taxQuery = []; |
| | | foreach($data['taxonomies']??[] as $taxonomy => $terms) { |
| | | if (array_key_exists(jvbNoBase($taxonomy), JVB_TAXONOMY)) { |
| | | $taxQuery[] = [ |
| | | 'taxonomy' => jvbCheckBase($taxonomy), |
| | | 'terms' => array_map( |
| | | 'absint', |
| | | is_array($terms) ? $terms : explode(',', $terms) |
| | | ) |
| | | ]; |
| | | } |
| | | /** |
| | | * @deprecated |
| | | * @param array $args |
| | | * @param array $data |
| | | * @return array |
| | | */ |
| | | protected function applyTaxonomyFilters(array $args, array $data):array |
| | | { |
| | | // Handle JSON-encoded taxonomy data |
| | | if (array_key_exists('taxonomy', $data) && is_string($data['taxonomy'])) { |
| | | $data['taxonomy'] = json_decode($data['taxonomy'], true); |
| | | } |
| | | |
| | | if (!empty($taxQuery)) { |
| | | $args['tax_query'] = array_merge([ |
| | | 'relation' => (array_key_exists('match', $data)) ? 'AND' : 'OR', |
| | | ], $taxQuery); |
| | | } |
| | | $taxonomies = $data['taxonomies'] ?? $data['taxonomy'] ?? []; |
| | | $taxQuery = []; |
| | | |
| | | $authorQuery = []; |
| | | foreach (jvbAuthorUsers() as $type) { |
| | | if (array_key_exists($type, $data)) { |
| | | $artist_ids = array_map( |
| | | 'absint', |
| | | is_array($data[$type]) ? |
| | | $data[$type] : |
| | | explode(',', $data[$type]) |
| | | ); |
| | | $authorQuery = array_merge($authorQuery, $artist_ids); |
| | | } |
| | | } |
| | | if (!empty($authorQuery)) { |
| | | $args['author__in'] = array_unique($authorQuery); |
| | | } |
| | | foreach($taxonomies as $taxonomy => $terms) { |
| | | // Better validation: check if taxonomy actually exists |
| | | if (!taxonomy_exists(jvbCheckBase($taxonomy))) { |
| | | continue; |
| | | } |
| | | |
| | | return $args; |
| | | } |
| | | $taxQuery[] = [ |
| | | 'taxonomy' => jvbCheckBase($taxonomy), |
| | | 'field' => 'term_id', |
| | | 'terms' => array_map( |
| | | 'absint', |
| | | is_array($terms) ? $terms : explode(',', $terms) |
| | | ), |
| | | 'operator' => 'IN' |
| | | ]; |
| | | } |
| | | |
| | | if (!empty($taxQuery)) { |
| | | // Match 'all' = AND, anything else = OR |
| | | $relation = ($data['match'] ?? 'all') === 'all' ? 'AND' : 'OR'; |
| | | |
| | | $args['tax_query'] = array_merge([ |
| | | 'relation' => $relation, |
| | | ], $taxQuery); |
| | | } |
| | | |
| | | // Keep existing author filtering logic |
| | | $authorQuery = []; |
| | | foreach (Registrar::getFeatured('can_create', 'user') as $type) { |
| | | if (array_key_exists($type, $data)) { |
| | | $artist_ids = array_map( |
| | | 'absint', |
| | | is_array($data[$type]) ? |
| | | $data[$type] : |
| | | explode(',', $data[$type]) |
| | | ); |
| | | $authorQuery = array_merge($authorQuery, $artist_ids); |
| | | } |
| | | } |
| | | if (!empty($authorQuery)) { |
| | | $args['author__in'] = array_unique($authorQuery); |
| | | } |
| | | |
| | | return $args; |
| | | } |
| | | |
| | | protected function applyOrderFilters(array $args, array $data):array |
| | | { |
| | | // Check for custom order first |
| | | $customArgs = $this->applyCustomOrder($args, $data); |
| | | if ($customArgs !== null) { |
| | | $order = (array_key_exists('order', $data)) ? strtoupper($data['order']) : 'DESC'; |
| | | $customArgs['order'] = (in_array($order, ['ASC', 'DESC'])) ? $order : 'DESC'; |
| | | return $customArgs; |
| | | } |
| | | |
| | | //Handle random |
| | | if (array_key_exists('orderby', $data) && $data['orderby'] === 'random') { |
| | | $current_seed = jvbGetRandomSeed(); |
| | | $current_seed = floor(time() / 1800); |
| | | $args['orderby'] = 'RAND(' . $current_seed . ')'; |
| | | unset($args['order']); |
| | | return $args; |
| | | } |
| | | if (in_array($data['orderby'], ['date', 'title', 'alphabetical'])) { |
| | | $args['orderby'] = ($data['orderby'] === 'alphabetical') ? 'title' : $data['orderby']; |
| | | |
| | | if (in_array($data['orderby'], ['date', 'modified', 'title', 'alphabetical'])) { |
| | | if ($data['orderby'] === 'date' && $this->isTimeline($args, $data)) { |
| | | $args['meta_key'] = BASE . 'latest_date'; |
| | | $args['orderby'] = 'meta_value_num'; |
| | | } else { |
| | | $args['orderby'] = ($data['orderby'] === 'alphabetical') ? 'title' : $data['orderby']; |
| | | } |
| | | |
| | | } else { |
| | | switch ($data['orderby']) { |
| | | case 'popularity': |
| | |
| | | $args['meta_key'] = BASE.'karma'; |
| | | $args['orderby'] = 'meta_value_num'; |
| | | break; |
| | | default: |
| | | $args['orderby'] = 'date'; |
| | | case 'unpopularity': |
| | | $args['meta_key'] = BASE.'downvotes'; |
| | | $args['orderby'] = 'meta_value_num'; |
| | | break; |
| | | case 'favourites': |
| | | $args['meta_key'] = BASE.'total_favourites'; |
| | | $args['orderby'] = 'meta_value_num'; |
| | | break; |
| | | case 'date': |
| | | default: |
| | | $args['orderby'] = 'date'; |
| | | break; |
| | | } |
| | | } |
| | | $order = (array_key_exists('order', $data)) ? strtoupper($data['order']) : 'DESC'; |
| | |
| | | return $args; |
| | | } |
| | | |
| | | /** |
| | | * Apply custom order if defined in content/taxonomy/user config |
| | | * |
| | | * @param array $args WP_Query args |
| | | * @param array $data Request data |
| | | * @return array|null Modified args if custom order found, null otherwise |
| | | */ |
| | | protected function applyCustomOrder(array $args, array $data): ?array |
| | | { |
| | | $orderby = $data['orderby'] ?? ''; |
| | | |
| | | // Skip if no orderby or it's a standard order |
| | | if (empty($orderby) || in_array($orderby, ['date', 'modified', 'title', 'alphabetical', 'random', 'popularity', 'karma', 'unpopularity', 'favourites'])) { |
| | | return null; |
| | | } |
| | | |
| | | // Determine content type |
| | | $post_type = is_array($args['post_type']) ? $args['post_type'][0] : $args['post_type']; |
| | | $content = jvbNoBase($post_type); |
| | | |
| | | // Get config for this content type |
| | | $config = Registrar::getInstance($content); |
| | | if (!$config) { |
| | | return null; |
| | | } |
| | | |
| | | // Check if this orderby is a custom order |
| | | $customOrders = $config->custom_order ?? []; |
| | | if (empty($customOrders) || !isset($customOrders[$orderby])) { |
| | | return null; |
| | | } |
| | | |
| | | // Get field definition |
| | | $fields = $config->getFields()??[]; |
| | | if (!isset($fields[$orderby])) { |
| | | return null; |
| | | } |
| | | |
| | | $field = $fields[$orderby]; |
| | | |
| | | // Set meta_key |
| | | $args['meta_key'] = BASE . $orderby; |
| | | |
| | | // Determine orderby and meta_type based on field type |
| | | $fieldType = $field['type'] ?? 'text'; |
| | | $subtype = $field['subtype'] ?? ''; |
| | | |
| | | switch ($fieldType) { |
| | | case 'number': |
| | | $args['orderby'] = 'meta_value_num'; |
| | | break; |
| | | |
| | | case 'text': |
| | | $args['orderby'] = ($subtype === 'number') ? 'meta_value_num' : 'meta_value'; |
| | | break; |
| | | |
| | | case 'date': |
| | | $args['orderby'] = 'meta_value'; |
| | | $args['meta_type'] = 'DATE'; |
| | | break; |
| | | |
| | | case 'datetime': |
| | | $args['orderby'] = 'meta_value'; |
| | | $args['meta_type'] = 'DATETIME'; |
| | | break; |
| | | |
| | | case 'true_false': |
| | | case 'checkbox': |
| | | $args['orderby'] = 'meta_value'; |
| | | $args['meta_type'] = 'BINARY'; |
| | | break; |
| | | |
| | | default: |
| | | $args['orderby'] = 'meta_value'; |
| | | } |
| | | |
| | | return $args; |
| | | } |
| | | |
| | | protected function isTimeline($args, $data):bool |
| | | { |
| | | $post_types = is_array($args['post_type']) ? $args['post_type'] : [$args['post_type']]; |
| | | $areTimeline = array_map(function($type) { return BASE.$type; },Registrar::getFeatured('is_timeline', 'post')); |
| | | return !empty(array_intersect($post_types, $areTimeline)); |
| | | } |
| | | |
| | | protected function applyDateFilters(array $args, array $data):array |
| | | { |
| | | if (!array_key_exists('date-filter', $data) && !array_key_exists('dateFrom', $data)) { |
| | |
| | | */ |
| | | protected function checkHeaders( |
| | | WP_REST_Request $request, |
| | | string|array $content_types, |
| | | array $additional_params = [] |
| | | ): WP_REST_Response|null { |
| | | |
| | | // Get latest timestamp for the content type(s) |
| | | $last_modified = CacheManager::getTimestamp($content_types); |
| | | |
| | | // Generate ETag from request params + timestamp |
| | | $etag = $this->generateETag($request->get_params(), $additional_params, $last_modified); |
| | | |
| | | // Check If-None-Match (ETag) header |
| | | $if_none_match = $request->get_header('If-None-Match'); |
| | | if ($if_none_match === $etag) { |
| | | return $this->createNotModifiedResponse($etag, $last_modified); |
| | | } |
| | | |
| | | // Check If-Modified-Since header |
| | | $if_modified_since = $request->get_header('If-Modified-Since'); |
| | | if ($if_modified_since) { |
| | | $if_modified_timestamp = strtotime($if_modified_since); |
| | | if ($last_modified <= $if_modified_timestamp) { |
| | | return $this->createNotModifiedResponse($etag, $last_modified); |
| | | } |
| | | } |
| | | |
| | | // Content has changed - store headers to add to successful response |
| | | $this->response_headers = $this->buildCacheHeaders($etag, $last_modified); |
| | | |
| | | return null; // Continue processing |
| | | } |
| | | |
| | | /** |
| | | * Generate ETag from request parameters and timestamp |
| | | * |
| | | * @param array $params Request parameters |
| | | * @param array $additional Additional parameters for uniqueness |
| | | * @param int $timestamp Last modified timestamp |
| | | * @return string ETag value with quotes |
| | | */ |
| | | private function generateETag(array $params, array $additional, int $timestamp): string |
| | | int|string|array $key, |
| | | string|array $group = '' |
| | | ): WP_REST_Response|false |
| | | { |
| | | // Combine all data that makes this response unique |
| | | $etag_data = array_merge( |
| | | $params, |
| | | $additional, |
| | | ['t' => $timestamp] |
| | | ); |
| | | $group = ($group!=='') ? $group : $this->cache_name; |
| | | $cache = $this->cache_name !== $group ? Cache::for($group) : $this->cache; |
| | | if (!$cache) { |
| | | return false; |
| | | } |
| | | if (is_array($key)) { |
| | | $key = $cache->generateKey($key); |
| | | } |
| | | |
| | | return '"' . md5(serialize($etag_data)) . '"'; |
| | | // Prefer tag freshness if available |
| | | $tags = $cache->getTags(); |
| | | |
| | | $lastModified = $tags |
| | | ? $cache->getLastModifiedForTags($tags) |
| | | : $cache::lastModified($group); |
| | | |
| | | if (!$lastModified) { |
| | | return false; |
| | | } |
| | | |
| | | |
| | | $etag = '"' . sha1($group . ':' . $key . ':' . $lastModified) . '"'; |
| | | |
| | | // ETag check |
| | | $ifNoneMatch = $request->get_header('if-none-match'); |
| | | if ($ifNoneMatch && trim($ifNoneMatch) === $etag) { |
| | | return new WP_REST_Response(null, 304); |
| | | } |
| | | |
| | | // Last-Modified check |
| | | $ifModifiedSince = $request->get_header('if-modified-since'); |
| | | if ($ifModifiedSince && strtotime($ifModifiedSince) >= $lastModified) { |
| | | return new WP_REST_Response(null, 304); |
| | | } |
| | | |
| | | // Store headers for response phase |
| | | $this->response_headers = [ |
| | | 'ETag' => $etag, |
| | | 'Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT', |
| | | ]; |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | |
| | | protected function addCacheHeaders(WP_REST_Response $response): WP_REST_Response |
| | | { |
| | | if (!empty($this->response_headers)) { |
| | | $response->set_headers($this->response_headers); |
| | | $this->response_headers = []; // Clear after use |
| | | foreach ($this->response_headers as $name => $value) { |
| | | $response->header($name, $value); |
| | | } |
| | | $this->response_headers = []; |
| | | } |
| | | |
| | | return $response; |
| | | } |
| | | |
| | | /** |
| | | * Helper: Check headers for user-specific endpoints |
| | | * Automatically includes user_id in ETag |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @param int $user_id User ID |
| | | * @param string|array $content_types Content type(s) |
| | | * @return WP_REST_Response|null |
| | | */ |
| | | protected function checkUserHeaders( |
| | | WP_REST_Request $request, |
| | | int $user_id, |
| | | string|array $content_types = 'user' |
| | | ): WP_REST_Response|null { |
| | | |
| | | // Include user-specific timestamp |
| | | $types = is_array($content_types) ? $content_types : [$content_types]; |
| | | $types[] = "user_{$user_id}"; |
| | | |
| | | return $this->checkHeaders($request, $types, ['user_id' => $user_id]); |
| | | } |
| | | |
| | | /** |
| | | * Helper to return error response |
| | | */ |
| | | protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response |
| | | { |
| | | $data = [ |
| | | 'success' => false, |
| | | 'message' => $message, |
| | | 'code' => $code |
| | | ]; |
| | |
| | | */ |
| | | protected function success(array $data, int $status = 200): WP_REST_Response |
| | | { |
| | | $data['success'] = true; |
| | | return new WP_REST_Response($data, $status); |
| | | } |
| | | |
| | |
| | | $lock_key = 'op_lock_' . md5($operation_key); |
| | | delete_transient($lock_key); |
| | | } |
| | | |
| | | protected function verifyTurnstile(string $token): bool |
| | | { |
| | | if (!Site::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) { |
| | | return true; |
| | | } |
| | | |
| | | if (empty($token)) { |
| | | return false; |
| | | } |
| | | |
| | | return JVB()->connect('cloudflare')->verifyTurnstile($token); |
| | | } |
| | | } |
| | | // |
| | | //Simple example: |
| | | //public function getTattoos(WP_REST_Request $request): WP_REST_Response |
| | | //{ |
| | | // // Check HTTP cache headers first |
| | | // $cache_check = $this->checkHeaders($request, 'tattoo'); |
| | | // if ($cache_check) { |
| | | // return $cache_check; // Returns 304 Not Modified |
| | | // } |
| | | // |
| | | // // Get data (use CacheManager for data caching too!) |
| | | // $filters = $request->get_params(); |
| | | // $cache = CacheManager::for('tattoo'); |
| | | // |
| | | // $tattoos = $cache->remember($filters, function() use ($filters) { |
| | | // return $this->queryTattoos($filters); |
| | | // }, 300); |
| | | // |
| | | // $response = new WP_REST_Response(['items' => $tattoos]); |
| | | // return $this->addCacheHeaders($response); // Add ETag and Last-Modified |
| | | //} |
| | | // |
| | | //Multiple Content Types: |
| | | //public function getTermsWithContent(WP_REST_Request $request): WP_REST_Response |
| | | //{ |
| | | // $taxonomy = $request->get_param('taxonomy'); |
| | | // |
| | | // // Check both taxonomy and its content types |
| | | // $cache_check = $this->checkHeaders($request, [$taxonomy, 'tattoo', 'artwork']); |
| | | // if ($cache_check) { |
| | | // return $cache_check; |
| | | // } |
| | | // |
| | | // // ... fetch data ... |
| | | // |
| | | // $response = new WP_REST_Response($data); |
| | | // return $this->addCacheHeaders($response); |
| | | //} |
| | | // |
| | | //User-specific: |
| | | //public function getUserFavorites(WP_REST_Request $request): WP_REST_Response |
| | | //{ |
| | | // $user_id = $request->get_param('user'); |
| | | // |
| | | // // Automatically checks user_{$user_id} timestamp + includes user_id in ETag |
| | | // $cache_check = $this->checkUserHeaders($request, $user_id); |
| | | // if ($cache_check) { |
| | | // return $cache_check; |
| | | // } |
| | | // |
| | | // // Get user's favorites (cached per user) |
| | | // $favorites = CacheManager::forUser($user_id)->remember('favorites', function() use ($user_id) { |
| | | // return $this->getUserFavorites($user_id); |
| | | // }, 1800); |
| | | // |
| | | // $response = new WP_REST_Response(['items' => $favorites]); |
| | | // return $this->addCacheHeaders($response); |
| | | //} |
| | | // |
| | | //Complex with additional params: |
| | | //public function getFilteredContent(WP_REST_Request $request): WP_REST_Response |
| | | //{ |
| | | // $user_id = get_current_user_id(); |
| | | // $filters = $request->get_params(); |
| | | // |
| | | // // Include custom params in ETag for uniqueness |
| | | // $cache_check = $this->checkHeaders( |
| | | // $request, |
| | | // 'tattoo', |
| | | // [ |
| | | // 'user_id' => $user_id, |
| | | // 'is_verified' => $this->isVerifiedUser($user_id) |
| | | // ] |
| | | // ); |
| | | // |
| | | // if ($cache_check) { |
| | | // return $cache_check; |
| | | // } |
| | | // |
| | | // // ... fetch filtered data ... |
| | | // |
| | | // $response = new WP_REST_Response($data); |
| | | // return $this->addCacheHeaders($response); |
| | | //} |
| | | |
| | | |
| | | |
| | | /** |
| | | * Use operation lock in your methods like this: |
| | | * |
| | | * public function updateContent(WP_REST_Request $request): WP_REST_Response |
| | | * { |
| | | * $user_id = get_current_user_id(); |
| | | * $content_id = $request->get_param('content_id'); |
| | | * |
| | | * // Prevent concurrent updates |
| | | * $lock_key = "update_{$user_id}_{$content_id}"; |
| | | * if (!$this->acquireOperationLock($lock_key)) { |
| | | * return $this->error( |
| | | * 'An update is already in progress. Please wait.', |
| | | * 'concurrent_operation', |
| | | * 409 |
| | | * ); |
| | | * } |
| | | * |
| | | * try { |
| | | * // Do your operation |
| | | * $result = $this->doUpdate($content_id); |
| | | * |
| | | * $this->releaseOperationLock($lock_key); |
| | | * return $this->success($result); |
| | | * |
| | | * } catch (\Exception $e) { |
| | | * $this->releaseOperationLock($lock_key); |
| | | * return $this->error($e->getMessage(), 'operation_failed'); |
| | | * } |
| | | * } |
| | | */ |